Skip to content

Understand Monad Transformers

Monad transformer(monadT) is another significant concept in Haskell. To understand the monadT, it's compulsory to know how monad works and the mechanism of >>=(bind). You can view the blog Understand Monad or any popular monads to learn about them.

In this blog, I will convey how we use monad transformer while how we recognize the idea that monad transformer creates a new monad. I will explain the function lift as well.

How We Use Monad Transformer

All monads are computations. Here function guardNum is a simple computation for Maybe monad. It returns Nothing if the input is negative.

guardNum :: Int -> Maybe Int
guardNum a
    | a < 0 = Nothing
    | otherwise = Just a

As time goes by, we might improve the guardNum whereby pass the sentry value from caller instead of fixing at 0. We can either add a new parameter or use Reader monad to do so:

  • add a new parameter
  • use Reader monad

The problem of adding a new parameter is that the new parameter is usually passed across the sub-calls as a kind of environment for all computation. In the other word, it shouldn't be put as a function parameter.

Reader monad is convenient to carry the data among all sub-calls, while it facilitates code change in the future. Integrating the behavior of monad Reader into the guardNum is beneficial

Then, another problem is that we should use Reader on the top of Maybe, or a ReaderT to embed them together to a new monad? The typical way is to stack the Reader on the top of Maybe, while it's also possible to integrate Reader via transformer ReaderT.

example :: Int -> Reader Int (Maybe Int)
example a = do
    sentry <- ask
    if a < sentry then return Nothing
    else return $ Just a

guardNumWithSentry :: Int -> Int -> Maybe Int
guardNumWithSentry a sentry
    | a < sentry = Nothing
    | otherwise = Just a

demo :: Int -> ReaderT Int Maybe Int
demo a = do
  sentry <- ask
  lift $ guardNumWithSentry a sentry

For the simple example here, it doesn't matter because both of them are simple. However, we prefer the monad transformer due to its flexibility.

Then, we have a look at lift $ guardNumWithSentry a sentry in function demo. Because Maybe and ReaderT Int Maybe are not the same type, we need to convert Maybe from the ReaderT Int Maybe. Given the fact that Maybe is the underlying type of the transformer, we can use lift function to convert from the underlying type to the transformer.

Monad Transformer: Construct a More Powerful Monad

MonadT creates a more powerful monad which supports the all features from monadT and underlying monad. The hardest blocker to understand monadT is that how monad transformer creates a new monad that supports both monadT and monad?. I need to highlight here if you try to understand it by the wrapping way, that's a wrong direction and will make you feel quite confused.

The transformer allows the new monad supports the underlying monad because it implements its typeclass. More details are mentioned in understand monad transformer(2).

Transformer just wraps the underlying monad

Given the fact that monad transformer implements the typeclass of underlying monad, it means that the monad transformer actual works as a proxy which helps to interact between outside and the internal underlying monad. In other word, the real computation is still done by the underlying monad, and the only difference is the monad transformer hides it from users.

If you don't want to understand how the monad transformer works, from a pure user perspective, that's quite easy because the monad transformer just allows you to mix the different monads together and then use all of their features. For example, we can mix the ask from reader and Nothing/Just from maybe as below.

demo' :: Int -> ReaderT Int Maybe Int
demo' a = do
  sentry <- ask
  if a< sentry then lift Nothing
  else lift $ Just a 

Similar to the Reader monad, the ask gets the value during runReaderT with the help of >>. When runReaderT is called, it will construct a reader via return function, and then the >> binding will propagate the data in the following ReaderT. We can define a custom customAsk to demonstrate it.

customAsk:: ReaderT Int Maybe Int
customAsk =  ReaderT return

customAskDemo :: Int -> ReaderT Int Maybe Int
customAskDemo a = do
    sentry <- customAsk
    if a < sentry then lift Nothing
    else lift $ Just a
main :: IO ()
main =
    print (runReaderT (demo 2) 3) -- Nothing
    >> print (runReaderT (demo 4) 3) -- Just 4