Monad transformers

by Taylor Fausak on

In my post about building a JSON REST API in Haskell, I glossed over monad transformers. That was unfortunate. Monad transformers are both useful and hard for me to wrap my head around. To help my solidify my understanding (and to fill the gap left by my other post) I’ll try to explain them here.

Before I can do that, I need to introduce a few monads. They are all simple and together they can be used to show how transformers work.

The first is Identity. As the name implies, it does almost nothing. Like the identity function, it isn’t too useful by itself but becomes it can be handy in certain situations. Here’s an example of how to use it:

import Data.Functor.Identity (Identity, runIdentity)

type Output = Integer

anIdentity :: Identity Output
anIdentity = do
    x <- return 3
    let y = x * 2
    return y

>>> runIdentity anIdentity
6

The next monad we’ll look at is Reader. It adds some read-only data. It can be a convenient way to add configuration information to an otherwise pure function. Here’s how it looks in action:

import Control.Monad.Trans.Reader (Reader, ask, runReader)

type Input = Integer
type Output = String

aReader :: Reader Input Output
aReader = do
    x <- ask
    let s = "The input was " ++ show x
    return s

>>> runReader aReader 3
"The input was 3"

Finally we’ll take a look at the Writer monad. It is the opposite of the reader monad. It adds write-only data. It is most often used to add logging to a function. Like the reader, it is easy to use:

import Control.Monad.Trans.Writer (Writer, tell, runWriter)

type Output = [String]
type Result = Integer

aWriter :: Writer Output Result
aWriter = do
    let x = 3
    tell ["The number was " ++ show x]
    return x

>>> runWriter aWriter
(3,["The number was 3"])

Now that we’ve covered all the monads, there’s one more piece we need for transformers. The lift function takes a function that works in one monad and allows you to use it in another. You can ask for something inside a Reader, but if you’re in a transformer stack that contains a reader, you have to lift ask for it. It’s easier to understand through an example. So let’s build up a monad transformer stack.

At the bottom will be an Identity monad. It doesn’t really do anything, but it lets us put more stuff on top of it. (Another popular base is the IO monad.) Above that, we’ll layer a Reader monad. In real life this would probably contain some kind of application configuration. For our purposes, it’ll hold a number. And at the top we’ll have a Writer monad. We’ll have it accumulate a list of strings, which may be what you’d use it for in real life.

import Control.Monad.Trans.Class (lift)
import Data.Functor.Identity (Identity, runIdentity)
import Control.Monad.Trans.Reader (ReaderT, ask, runReader)
import Control.Monad.Trans.Writer (WriterT, tell, runWriter)

type Input = Integer
type Output = [String]
type Result = Integer

stack :: WriterT Output (ReaderT Input Identity) Result
stack = do
    x <- lift ask
    tell ["The input was " ++ show x]
    return x

So that’s the whole stack. We can use the outer Writer monad directly with tell. But we have to wrap calls to the inner Reader monad with lift. This is the crux of the power of monad transformers. They allow you to use any monad in the stack with a single call to lift. Even if you have a stack of 10 monads, one lift is all you need to get all the way to the one you want.

The only downside is that you need to run all of these monads. Doing so can be a little tedious.

>>> let newReader = runWriterT stack
>>> let newIdentity = runReaderT newReader 3
>>> runIdentity newIdentity
(3,["The number was 3"])

This example is trivial, but I hope it shows how monad transformers work. They are a powerful way to combine monadic actions.