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
TupleSectionsextension of GHC, we can implement this more succinctly asinstance MonadTrans (WriterT w) where lift f = WriterT $ (,mempty) <$> fYou can read
(,mempty)as a pair whose second component ismemptybut whose first component is still undefined. It's a function of typea -> (a, w). Byfmapping this function overf, we turnfinto 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 memptyThis runs the two actions
fandpure memptyand 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 sor tuple sections,
↩instance MonadTrans (StateT s) where lift f = StateT $ \s -> (,s) <$> f