Composing monadic effects

Published on

In Haskell, monad transformers are used to combine effects provided by multiple monads.

Boring example: Writer, Reader, State

For instance, if we want to have a read-only environment plus a write-only logging facility, we can start with the Reader monad and add a logging facility to it with the WriterT monad transformer:

Or, we could start with the Writer monad and put the ReaderT transformer on top of it:

In the end, it doesn’t matter which way we choose, as both are isomorphic to

If we want to have a state as well, we can plug in the StateT transformer anywhere in the stack, and again the order doesn’t matter. The resulting monad is now isomorphic to the standard RWS (Reader-Writer-State) monad.

So, we say about those transformers that they commute (recall that Reader is just the ReaderT transformer applied to the Identity monad).

More interesting example: Either + Writer

Not every two transformers commute, though.

Suppose we want to write log messages, plus to be able to abort the computation.

There are two quite different ways to compose these effects.


isomorphic to (Either e a, w), and then there’s

isomorphic to Either r (a, w).

We can already see the difference from the types. In the first case, the w is there independently of the value of Either. In the second case, if the computation failed, the whole thing is Left msg, and we don’t get our log w back.

Let’s try the following to confirm our expectations:

Developing intuition

When reasoning about a stack of monad transformers, it’s useful to keep in mind that a transformer knows nothing about its inner monad. It has to work “inside” that monad. It has no ability to alter or cancel the effects that happened before in that monad. (It can prevent those effects from happening, though – see tell "bar" above.)

In the first example above, which corresponds to ErrorT e (Writer w), the Error monad had to work inside the Writer monad. It simply had no way to tell the writer monad to “forget” what had been told to it before.

In the second example, corresponding to WriterT w (Either e) a, the main monad is now Either e. If it decides to fail, it will return just the error, and nothing else.

The same logic answers the question why there’s no IOT, an IO monad transformer. Imagine this:

Since throwError happens in the base monad, the whole thing is just Left "oops I did it again" – nothing mentions any IO. But what about the missiles?

If this shallow explanation is not enough to make you comfortable with transformers, I’d advise to read (or even to write) the implementation of some standard transformers.

Even more interesting example: Parsec + State

The Parsec monad already allows us to have our own state, but let’s see how we can add one independently.

Again, we have two ways to layer ParsecT and StateT transformers on top of Identity: ParsecT s () (State u) a and StateT u (Parsec s ()) a. Is there a difference?

Recall that Parsec is a backtracking monad. It can execute one branch, fail, and then execute another branch. Do we observe the state changes that happened in the aborted branch?

This will be our test code:

We’ll try to run it with the input "a" and initial state 0. First, Parsec executes the left branch of <|>, fails, and proceeds with the right branch. If the state is “global”, then the final result will be 1. If the state is subject to backtracking, then we’ll see 0.

In the ParsecT s () (State u) a case, the base monad is State. ParsecT works inside that monad and cannot revert the effects that happened there. (It may seem that it could remember the state of a branch using get and then restore it using put; but a monad transformer has to be polymorphic in the underlying monad and thus cannot rely on existence of get and put.)

And indeed, the code

prints Right 1.

Let’s now try the second option: layering StateT on top of Parsec. We cannot use the code above as is, because types do not match: e.g. char 'b' has type Parsec s () Char, but we need StateT u (Parsec s ()) Char. Such an upgrade is done by the lift function.

Sometimes, however, we need to “downgrade” computations from StateT u (Parsec s ()) a to Parsec s () a.

For example, to implement a <|> operator of type

we need first to downgrade the arguments to plain Parsec computation, invoke Parsec’s own <|> and then upgrade the result back to StateT.

Notice how we explicitly run both branches of <|> with the same state. It’s no surprise now that the state will be “backtracked” and the result will be Right 0. By the way, this is the same result as would be achieved using Parsec’s internal state.