The MonadTrans class
Until you're ready to write your own monad transformer, you can safely skip this
section and simply rejoice of the fact that every monad transformer comes with a
lift
function that allows us to lift actions from the underlying monad into
the transformed monad. We used this function before to lift IO
actions into
the ReaderT Config IO
monad we constructed.
It would be nice if we could simply use IO
actions in the ReaderT Config IO
monad. After all, that's our intent here: We want a monad that allows us to do
both, perform I/O and read the config. However, this goes against the grain of
Haskell's strict type checking, which is generally a good thing because it
strengthens the compiler's ability to catch mistakes in our code. lift
is the
boilerplate that is necessary to tell the compiler that we do want to use an
IO
action as if it were a ReaderT Config IO
action.
To support this lift operation, a monad transformer must be an instance of the
MonadTrans
class:
class MonadTrans t where
lift :: Monad m => m a -> t m a
lift
takes an action of type m a
, that is, an action in the underlying monad
m
, and transforms it into an action in the transformed monad t m
. For
example, ReaderT r
is a monad transformer, so for any monad m
, lift
lifts
an action of type m a
into an action of type ReaderT r m a
.
We won't build any fancy monad transformers here. To illustrate this class a bit
more clearly though, let us took at the MonadTrans
instances for ReaderT
,
WriterT
, StateT
, and MaybeT
.
First ReaderT
:
instance MonadTrans (ReaderT r) where
lift f = ReaderT $ \_ -> f
This makes perfect sense. f
is an action in the underlying monad m
. It knows
nothing about any context provided by ReaderT r
. Thus, to turn it into an
action in the monad ReaderT r m
, we simply convert f
into a function that
ignores the provided context.
Let's try MaybeT
next:
instance MonadTrans MaybeT where
lift f = MaybeT $ Just <$> f
Again, f
is an action in the underlying monad m
. It knows nothing about the
ability to fail provided by MaybeT
. Thus, to turn it into an action in the
monad MaybeT m
, we simply turn it into an action that always succeeds, that
always returns Just
. In particular, if f
has the type m a
, then
lift f = MaybeT lf
, where lf
should have the type m (Maybe a)
. That's what
the definition of MaybeT
says. m
being a monad, it is also a functor, we can
turn f
into an action that returns Just
whatever f
returns using
fmap Just f
or, in infix notation Just <$> f
. And that's what our definition
of lift
does.
Next WriterT
:1
instance Monoid w => MonadTrans (WriterT w) where
lift f = WriterT $ do
x <- f
return (x, mempty)
Again, f
is an action in the underlying monad m
. It knows nothing about the
log provided by WriterT w
. Thus, if we lift f
into WriterT w m
, it shouldn't
add anything to the log. And that's exactly what we achieve by pairing f
's
return value x
with mempty
, which is the unit with respect to w
's monoid
multiplication.
And finally, StateT
:2
instance MonadTrans (StateT s) where
lift f = StateT $ \s -> do
x <- f
return (x, s)
f
knows nothing about the state provided by StateT s
, so it shouldn't modify
the state. Thus, we wrap f
into a function that takes the current state s
as
an argument and returns a pair consisting of f
's return value x
and the very
same state s
.
-
If we enable the
TupleSections
extension of GHC, we can implement this more succinctly asinstance MonadTrans (WriterT w) where lift f = WriterT $ (,mempty) <$> f
You can read
(,mempty)
as a pair whose second component ismempty
but whose first component is still undefined. It's a function of typea -> (a, w)
. Byfmap
ping this function overf
, we turnf
into an action that returns a pair consisting off
's return value andmempty
, just as our more verbose implementation usingdo
-notation does.If you want to avoid language extensions but you are comfortable with using applicative functors (which we haven't discussed in any detail in this book), then there's an only slightly more verbose implementation:
instance MonadTrans (WriterT w) where lift f = WriterT $ (,) <$> f <*> pure mempty
This runs the two actions
f
andpure mempty
and combines their return values using(,)
, which is a two-argument function that combines its two arguments into a pair. ↩ -
Once again, we can implement this more succinctly using operators for applicative functors,
instance MonadTrans (StateT s) where lift f = StateT $ \s -> (,) <$> f <*> pure s
or tuple sections,
↩instance MonadTrans (StateT s) where lift f = StateT $ \s -> (,s) <$> f