Running with Monads

I had a big stumbling block in learning monads. Perhaps unsurprisingly, this occurred because I was trying to take a particular monadic analogy too far in the college class where I first learned about them. I got the idea in my head that, "Monads are like a treasure chest", and my mental model went something like this:

  1. Monads are like a treasure chest.
  2. You can't directly access what's inside (the a in IO a).
  3. That is, unless you are already in that monad.
  4. In that case, the "bind" (>>=) operator let's us pass in a function that accesses the "inner value".
  5. But the result we produce is re-wrapped in the monad!

And so my head was spinning in a loop trying to figure out how I could actually get into a monad in the first place.

However, the code I was looking at was not normal monadic code. It was IO code. And I was conflating the IO monad with all monads. Don't do this! The IO monad is, in fact, special! It does follow some of the same patterns as other monads. But this special property of "you can't access the inner value unless you're in IO" does not apply to other monads!

The majority of monads you will encounter can be accessed from pure code. The most common way of doing this is through a run function. Here are three of the most common examples with the Reader, Writer and State monads:

runReader :: Reader r a -> r -> a

runWriter :: Writer w a -> (a, w)

runState :: State s a -> s -> (a, s)

Most often, you'll need to supply an "initial" value, as we see with Reader and State. And many times you'll get the final stateful value as a second product of the function. This occurs in Writer and State.

Here's a simple example. We have a State computation that adds 1 to the stored value, and then adds the previous stored value to the input, returning that. We can call into this stateful function from a totally pure function using runState:

stateFunction :: Int -> State Int Int
stateFunction input = do
  prev <- get
  modify (+1)
  return $ prev + input

callState :: Int -> (Int, Int)
callState x = runState (stateFunction (x + 5)) 11

...

>> callState 3
(19, 12)
>> callState 7
(23, 12)

With monad transformers, the concept of the "run" function is very similar. The functions now end with the suffix T. The only difference is that it produces a value in the underlying monad m:

runReaderT :: ReaderT r m a -> r -> m a

runWriterT :: WriterT w m a -> m (a, w)

runStateT :: StateT s m a -> s -> m (a, s)

Of course, there are exceptions to this pattern of "run" functions. As we learned about last time, Either can be a monad, but we can also treat Either values as totally normal objects. To access the inner values, we just need a case statement or a pattern match. You don't need to take the "treasure box" approach. Or at least, with certain monads, the treasure box is very easy to unlock.

If we go back to IO for a second. There is no "run" function for IO. There is no runIO function, or runIO_T transformer. You can't conjure IO computations out of nothing (at least not safely). Your program's entrypoint is always a function that looks like:

main :: IO ()

To use any IO function in your program, you must have an unbroken chain of IO access going back to this main function, whether through the IO monad itself or a transformer like StateT IO. This pattern allows us to close off large parts of our program to IO computations. But no part of your program is firmly closed off to a State computation. As long as you can generate the "initial" state, you can then access the State monad via runState.

So if you were struggling with the same stumbling block I was, hopefully this clears things up for you! If you want more examples of how monads work and how to apply these run functions, take a look at our series Monads and Functional Structures!

Previous
Previous

Shorter Run Functions

Next
Next

Using Either as a Monad