Skip to content

Monad Transformers

We have learned that different monads provide different effects. What do we do if we want to combine the effects provided by different monads? What if we want a stateful computation that can fail, by combining Maybe and State, or a stateful computation that can also perform I/O, by combining State and IO? The monads we have discussed so far do not support this.

The tool we need is monad transformers. As the name suggests, they allow us to transform monads into different monads. More precisely, we can take a "base monad", and the monad transformer equips this base monad with an additional decoration. Monad transformers allow us to stack decorations.

The mtl package includes transformer versions of State, Reader, Writer, and Maybe. Since they're transformers, they are called StateT, ReaderT, WriterT, and MaybeT. The monad StateT s IO uses the IO monad as its underlying monad, in order to be able to perform I/O, but it also comes with a state of type s, just as in the State s monad. Thus, StateT s IO is the type of monad we want when we want to model stateful computations that can perform I/O. There is no transformer version of IO. If we build a whole stack of monad transformers, IO must always be at the bottom. This has a very good reason, but discussing it would lead us too far astray. The standard library doesn't have a transformer version of the Either e monad either, but the transformer ExceptT equips an existing monad with exception handling in the same way that Either e models exception handling. We will completely ignore the list monad in the discussion of monad transformers because the combination of the list monad with State, for example, really makes your head spin.

We'll build up our understanding of monad transformers gradually:

  • We start by looking at ReaderT, WriterT, StateT, and MaybeT.

  • It is not uncommon to have some computation that requires some read-only context, some read-write state, and a log. We could build such a computation by stacking ReaderT on WriterT on StateT, but such deep monad stacks are often cumbersome to work with. Thus, there exists a monad that combines the capabilities of Reader, Writer, and State without the need for monad transformers. It is called RWS (Reader-Writer-State). We'll discuss it briefly.

  • Each monad transformer adds some capability to the underlying monad. If we stack ReaderT on top of StateT, then we obtain a monad that is both a reader monad and a state monad, but it isn't the same as State. In particular, since ReaderT is the topmost monad in our stack, our monad doesn't have get and put methods to access the state. They are hidden one layer deeper, in the StateT transformer. To be able to work with the capabilities of all the monads in our monad stack, we need to be able to navigate the stack. The method that does this is called lift. In our stack of ReaderT on top of StateT, for example, we manipulate the state of the monad using lift get and lift . put. This "lifts" the actions in the underlying monad into the outer monad. If we had a stack of MaybeT on top of ReaderT on top of StateT, then we'd have to reach two layers deep to get at the state, using lift (lift g) and lift . lift . put.

    lift is a method supported by any monad transformer. It is defined in the MonadTrans class. ReaderT, WriterT, StateT, and MaybeT are all instances of MonadTrans.

  • Monad transformers give us infinite ways to stack monads on top of each other, and lift provides us with the means to navigate the stack to use the methods of each invidual monad in the stack. However, it gets tedious quite quickly to write lift . lift . lift $ putStrLn "Hello, world!" to perform some I/O in a monad that has three transformers stacked on IO at the bottom of the whole stack. To make working with monad stacks easier, we have subclasses of Monad called MonadIO, MonadState, MonadReader, and MonadWriter. get and put are supported not only by State or RWS but by every monad that is an instance of MonadState. ask is supported not only by Reader but by any monad that is an instance of MonadReader. Moreover, the standard monad transformers provide instance definitions that ensure that, for example, StateT s (ReaderT r IO)—a monad where we stack state on top of read-only context on top of IO—is automatically an instance of MonadIO, MonadReader, and MonadState. Thus, we can access the context provided by ReaderT and the state provided by StateT directly using ask, get, and put, without the need to explicitly use lift. The MonadIO class is a bit of an outlier here. IO actions, such as putStrLn, are not automatically available in any monad that is an instance of MonadIO. We still need to lift these actions using the liftIO method of the MonadIO class. I suspect that this is a wart with historical roots. Still, liftIO $ putStrLn "Hello, world!" sure beats lift . lift . lift $ putStrLn "Hello, world!" if the IO monad is at the bottom of a deep monad stack. We conclude our discussion of monad transformers by looking at the MonadIO, MonadState, MonadReader, and MonadWriter classes.