Skip to content

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.


  1. If we enable the TupleSections extension of GHC, we can implement this more succinctly as

    instance MonadTrans (WriterT w) where
        lift f = WriterT $ (,mempty) <$> f
    

    You can read (,mempty) as a pair whose second component is mempty but whose first component is still undefined. It's a function of type a -> (a, w). By fmapping this function over f, we turn f into an action that returns a pair consisting of f's return value and mempty, just as our more verbose implementation using do-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 and pure mempty and combines their return values using (,), which is a two-argument function that combines its two arguments into a pair. 

  2. 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