Skip to content

Understand Monad Transformers(2)

This is a reading notes from the great book named book of monads. It helps me a lot and I quite recommend you to read it if you have any plan to read books about haskell monads.

This is the following article after understand monad transformers as there are some confusions and missing points in the previous blog.

Previous blog focused on when to use monad transformers, and how we understand the monad transformer creates a new powerful monad which supports both monadT and underlying monad features.

After several months, I would rather to say it didn't point out the core part of monad transformer, which is how the monad transformer supports the underlying monad functionalities via type class.

  • what's the problem of stacking monads(the reason why we need monad transformer)
  • why we can use features from both monad and monad transformer under the scope of monad transformer.

Problem of Stacking Monads

When writing code, you need to put the Reader and Maybe together to express a computation reads configuration from environment might fail. For example, you will define the code like this:

eval' :: Expr -> Reader Assignment (Maybe Int)
eval' (Op o x y) = do
  u <- eval' x
  v <- eval' y
  case (u, v) of
    (Nothing, _) -> return Nothing
    (_, Nothing) -> return Nothing
    (Just u', Just v') ->
      case o of
        Add -> return (Just (u' + v'))
        Subtract -> return (Just (u' - v'))
        Multiply -> return (Just (u' * v'))
        Divide ->
          if v' == 0
            then return Nothing
            else return (Just (u' `div` v'))
eval' (Literal n) = return $ Just n
eval' (Var v) = asks $ lookup v

However, to make the quick fail, you're likely to implement the Monad typeclass for it. Then, of course you need to write a lot of dummy and tedious code to achieve your simple composition goal.

data Evaluator a = Evaluator (Reader Assignment (Maybe a))

-- the fmap applies to the result of maybe
instance Functor Evaluator where
  fmap :: (a -> b) -> Evaluator a -> Evaluator b
  fmap f (Evaluator ea) = Evaluator $ reader $ \assignment -> do
    let d = runReader ea assignment
    fmap f d

instance Applicative Evaluator where
  pure :: a -> Evaluator a
  pure v = Evaluator $ reader $ \_ -> return v
  (<*>) :: Evaluator (a -> b) -> Evaluator a -> Evaluator b
  (<*>) (Evaluator f) (Evaluator ea) = Evaluator $ reader $ \assignment -> do
    let d = runReader ea assignment
    let f' = runReader f assignment
    f' <*> d

instance Monad Evaluator where
  return :: a -> Evaluator a
  return x = Evaluator $ reader $ \_ -> return x
  (>>=) :: Evaluator a -> (a -> Evaluator b) -> Evaluator b
  (Evaluator x) >>= f = Evaluator $ reader $ \a ->
    case runReader x a of
      Nothing -> Nothing
      Just y ->
        let Evaluator e = f y
         in runReader e a

evalFail :: Evaluator a
evalFail = Evaluator $ reader $ const Nothing

After defining the monad for the stacked monads, you could define the eval'' as below, looks nice and brief comparing to the eval' above, if we ignore the code we write to implement Evaluator as a monad.

eval'' :: Expr -> Evaluator Int
eval'' (Op o x y) = do
  u <- eval'' x
  v <- eval'' y
  case o of
    Add -> return (u + v)
    Subtract -> return (u - v)
    Multiply -> return (u * v)
    Divide -> if v == 0 then evalFail else return (u `div` v)
eval'' (Literal n) = return n
eval'' (Var v) = do
  a <- Evaluator (asks Just)
  maybe evalFail return (lookup v a)

Hand-writing these instances is definitely not the correct way, thus we need to use monad transformer instead. The monad transformer receives a monad type and then generates a new monad type which contains the features from both the provided monad and the monad transformer itself at the same layer. Hence, you needn't write any code to implement monad for Evaluator.

New Monad Spawned by Monad Transformer

The monad transformer receives a monad to generate a new monad with features of them. For example, when you use ReaderT for a Maybe monad, the generated monad owns both features of ReaderT and Maybe.

Monad Transformer and Identity Monad

Several monads are generated by its corresponding monad transformer with monad Identity, for example:

type State s = StateT s Identity
type Reader r = ReaderT r Identity
type Writer w = WriterT w Identity

Namely, the Identity stands a do-nothing computation. Hence, for the monads derives from the respective monad transformer, it has the features provided by the transformer only.

MonadX Allows to Use Underlying Functionalities in MonadT

During the computation of MaybeT (Reader Assignment) a, it's convenient to use both ask feature from reader and return Nothing from the maybe. However, let's take a further look on the details to realize the complexity hidden from the facade.

When you can ask in the MaybeT (Reader Assignment) a, the type of ask looks different:

  • ask in Reader: Reader r r
  • ask in MaybeT: MaybeT (Reader r) r

Even though the type system is different, but in real practice, no additional operations is needed to access the operations from all combined monads underlying. So there must be a place to do the conversion to overcome the type differences.

This is done via the type class MonadX provided by the monad implementation(TODO: check whether it's true), where X is the name of the monad we are abstracting over. For example, monad Reader defines the corresponding MonadX named MonadReader as well to support the possible monad transformer:

class Monad m => MonadReader r m | m -> r where
    -- | Retrieves the monad environment.
    ask   :: m r
    ask = reader id
    -- | Executes a computation in a modified environment.
    local :: (r -> r) -- ^ The function to modify the environment.
          -> m a      -- ^ @Reader@ to run in the modified environment.
          -> m a

The MonadX type class asks you to implement an instance for you monad if it requires the features from X. So if you need to implement a type class for an instance, let's say implement MonadT m for MonadReader, you need to do the following things. But note that this is done by the mtl package, usually users needn't do this tedious work.

  • check whether the MonadT instance really implements the MonadReader, if so you needn't to much work:
instance Monad m => MonadReader r (ReaderT r m) where
     ask = ReaderT return
     local f m = ReaderT $ runReaderT m . f
  • if the MonadT doesn't provide the MonadReader, you need to pass through the layer and find it below, namely monad m here:
instance MonadReader r m => MonadReader r (MaybeT m) where
     ask = MaybeT $ runMaybeT ask
     local f m = MaybeT $ runMaybeT (local f m)

At this point you have discovered how boring and tedious this matter is. You just want to use the functionalities from an underlying monad, but you need to implement its type class which is fairly simple wrapper. Luckily, this problem has been solved by the package mtl so users won't write these kinds of instances.

In short, to make the monad transformer accesses to the underlying monad functionalities, the underlying monad must define the corresponding MonadX for the transformer to lift. Without the MonadX typeclass, it's impossible for monad transformer to access the underlying monad in the constructed one.

Moreover, the instances implementation is done by the mtl library instead of compiler. Usually, the instances implementation lives in the same package of the type class MonadX. For example, MaybeT implements an instance for MonadReader in the Control.Monad.Reader.

instance MonadReader r m => MonadReader r (MaybeT m) where
    ask   = lift ask
    local = mapMaybeT . local
    reader = lift . reader

Conclusion of MonadX

The topic above explained the pain-points of the hand-writing instances during transforming, and introduced the typeclass MonadX is defined for the monad transformer. The key points are listed below.

  • MonadX is used by monad transformer to implement
  • the implementation is done by the package, not the compiler at the definition of typeclass MonadX.

Conclusion

This blog introduces the problems when you stacking monads together, such as you need to keep defining instances for the monads. This urges us to use monad transformer to avoid such tedious and dummy work. Then, we have learned the typeclass MonadX helps the transformer to support the underlying monad functionalities by implements the typeclass instances for the transformer inside the mtl package.

I believe after reading this, you can understand why the monad transformer allows you to use the functionalities from underlying monads.