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, andMaybeT. -
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
ReaderTonWriterTonStateT, but such deep monad stacks are often cumbersome to work with. Thus, there exists a monad that combines the capabilities ofReader,Writer, andStatewithout the need for monad transformers. It is calledRWS(Reader-Writer-State). We'll discuss it briefly. -
Each monad transformer adds some capability to the underlying monad. If we stack
ReaderTon top ofStateT, then we obtain a monad that is both a reader monad and a state monad, but it isn't the same asState. In particular, sinceReaderTis the topmost monad in our stack, our monad doesn't havegetandputmethods to access the state. They are hidden one layer deeper, in theStateTtransformer. 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 calledlift. In our stack ofReaderTon top ofStateT, for example, we manipulate the state of the monad usinglift getandlift . put. This "lifts" the actions in the underlying monad into the outer monad. If we had a stack ofMaybeTon top ofReaderTon top ofStateT, then we'd have to reach two layers deep to get at the state, usinglift (lift g)andlift . lift . put.liftis a method supported by any monad transformer. It is defined in theMonadTransclass.ReaderT,WriterT,StateT, andMaybeTare all instances ofMonadTrans. -
Monad transformers give us infinite ways to stack monads on top of each other, and
liftprovides 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 writelift . lift . lift $ putStrLn "Hello, world!"to perform some I/O in a monad that has three transformers stacked onIOat the bottom of the whole stack. To make working with monad stacks easier, we have subclasses ofMonadcalledMonadIO,MonadState,MonadReader, andMonadWriter.getandputare supported not only byStateorRWSbut by every monad that is an instance ofMonadState.askis supported not only byReaderbut by any monad that is an instance ofMonadReader. 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 ofIO—is automatically an instance ofMonadIO,MonadReader, andMonadState. Thus, we can access the context provided byReaderTand the state provided byStateTdirectly usingask,get, andput, without the need to explicitly uselift. TheMonadIOclass is a bit of an outlier here.IOactions, such asputStrLn, are not automatically available in any monad that is an instance ofMonadIO. We still need to lift these actions using theliftIOmethod of theMonadIOclass. I suspect that this is a wart with historical roots. Still,liftIO $ putStrLn "Hello, world!"sure beatslift . lift . lift $ putStrLn "Hello, world!"if theIOmonad is at the bottom of a deep monad stack. We conclude our discussion of monad transformers by looking at theMonadIO,MonadState,MonadReader, andMonadWriterclasses.