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
ReaderT
onWriterT
onStateT
, but such deep monad stacks are often cumbersome to work with. Thus, there exists a monad that combines the capabilities ofReader
,Writer
, andState
without 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
ReaderT
on 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, sinceReaderT
is the topmost monad in our stack, our monad doesn't haveget
andput
methods to access the state. They are hidden one layer deeper, in theStateT
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 calledlift
. In our stack ofReaderT
on top ofStateT
, for example, we manipulate the state of the monad usinglift get
andlift . put
. This "lifts" the actions in the underlying monad into the outer monad. If we had a stack ofMaybeT
on top ofReaderT
on top ofStateT
, then we'd have to reach two layers deep to get at the state, usinglift (lift g)
andlift . lift . put
.lift
is a method supported by any monad transformer. It is defined in theMonadTrans
class.ReaderT
,WriterT
,StateT
, andMaybeT
are all instances ofMonadTrans
. -
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 writelift . lift . lift $ putStrLn "Hello, world!"
to perform some I/O in a monad that has three transformers stacked onIO
at the bottom of the whole stack. To make working with monad stacks easier, we have subclasses ofMonad
calledMonadIO
,MonadState
,MonadReader
, andMonadWriter
.get
andput
are supported not only byState
orRWS
but by every monad that is an instance ofMonadState
.ask
is supported not only byReader
but 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 byReaderT
and the state provided byStateT
directly usingask
,get
, andput
, without the need to explicitly uselift
. TheMonadIO
class is a bit of an outlier here.IO
actions, such asputStrLn
, are not automatically available in any monad that is an instance ofMonadIO
. We still need to lift these actions using theliftIO
method of theMonadIO
class. I suspect that this is a wart with historical roots. Still,liftIO $ putStrLn "Hello, world!"
sure beatslift . lift . lift $ putStrLn "Hello, world!"
if theIO
monad is at the bottom of a deep monad stack. We conclude our discussion of monad transformers by looking at theMonadIO
,MonadState
,MonadReader
, andMonadWriter
classes.