tobold.org

correct • elegant • free

Heavy lifting

I'm trying to improve my skills with monad stacks. Till now, I've got away with quite a lot, but not grokked what I was doing. (Sorry, I'm British, and we double consonants before an -ed or -ing suffix, if the vowel is short.)

Here's some simple code:

module Main where

import Control.Monad.Reader

main = do
  putStrLn "Hello, world!"
  runReaderT mything 5

mything :: ReaderT Int IO ()
mything = do
  i <- ask
  lift $ putStrLn $ "i == " ++ show i

Of course main is in the IO monad; mything is in a stack consisting of ReaderT wrapped around IO. The interesting part is that we can use lift to run an action in the inner monad.

The definition of lift looks like this:

lift :: MonadTrans t => forall m a. Monad m => m a -> t m a

Lift a computation from the argument monad to the constructed monad.

Now, I'm a bit vague about what forall means there, but lets not worry about that now. Clearly here the "argument monad" is IO, and the "constructed monad" is ReaderT. It's the computation itself that is "lift"ed:

+---------+
| ReaderT |   ^                ^
+---------+   |                | lift
|   IO    |   |  putStrLn ...  |
+---------+

OK, so let's look at monad-control. This offers the control function which can be used like so:

module Main where

import Control.Monad.Reader
import Control.Monad.Trans.Control

main = do
  putStrLn "Hello, world!"
  runReaderT mything 5

mything :: ReaderT Int IO ()
mything = do
  i <- ask
  control $ \run -> do
    putStrLn $ "1. i == " ++ show i
    run $ do
      j <- ask
      lift $ putStrLn $ "2. j == " ++ show j
  lift $ putStrLn $ "3. i == " ++ show i

Within the control block, we are in IO (and hence can use putStrLn directly). The run function that is constructed by control gives us an escape hatch back to ReaderT (and, as you'd expect, once we're back in ReaderT we can use lift again for IO actions).

Now, it would be nice to split the control block off into a separate function, to get a better idea of the types involved. To make that work, though, we have to give it a fairly hairy type signature involving RunInBase. That utilizes rank N types, so we also need the relevant extension:

{-# LANGUAGE RankNTypes #-}

module Main where

import Control.Monad.Reader
import Control.Monad.Trans.Control

main = do
  putStrLn "Hello, world!"
  runReaderT mything 5

mything :: ReaderT Int IO ()
mything = do
  i <- ask
  lift $ putStrLn $ "1. i == " ++ show i
  control otherthing
  lift $ putStrLn $ "4. i == " ++ show i

otherthing :: RunInBase (ReaderT Int IO) IO -> IO (StM (ReaderT Int IO) ())
otherthing run = do
  putStrLn $ "2. in IO"
  run $ do
    j <- ask
    lift $ putStrLn $ "3. j == " ++ show j

In passing, it is curious that in the previous example GHC was happy to use a rank N type without the extension; it is only if we need to name it that the extension is required.

Anyway, let's liven things up again. Here's some code with a more exotic monad stack:

module Main where

import Control.Monad.Reader
import Control.Monad.Writer

main = do
  putStrLn "Hello, world!"
  cnt <- execWriterT $ runReaderT mything 5
  putStrLn $ "cnt is " ++ show (getSum cnt)

mything :: ReaderT Integer (WriterT (Sum Integer) IO) ()
mything = do
  i <- ask
  lift $ tell $ Sum 1
  tell $ Sum 1
  lift $ lift $ putStrLn $ "i is " ++ show i
  liftIO $ putStrLn $ "i is " ++ show i

I've used stacks like this before. It's always looked to me that in the function call ReaderT is on the "inside", whereas in the type it's on the "outside". After all, if I write Right $ Just 5 I get a value of type Either a (Maybe Int). Anyway, for reasons that elude me at the moment, with monads it's different. Each new monadic environment adds a new monad to the top of the stack, and the left of the type

This example demonstrates some further wrinkles. We can simply use ask from the ReaderT monad of coures. And we can lift tell from WriterT, but we can also write a plain tell, apparently unlifted. (There's some kind of magic going on under the hood that I don't understand to make this work.)

To run a computation in IO, we can, as you might expect, lift it twice. But as an alternative we can simply use liftIO which plucks IO from the bottom of any monadic stack in a single step, no matter how deep the stack. (Again, this is magic as far as my current level of understanding is concerned.)

Using monad-control with this more complex stack is not significantly different from the earlier example. I've used a type alias to stop the type of otherthing running off the side of the screen:

{-# LANGUAGE RankNTypes #-}

module Main where

import Control.Monad.Reader
import Control.Monad.Trans.Control
import Control.Monad.Writer

main = do
  putStrLn "Hello, world!"
  cnt <- execWriterT $ runReaderT mything 5
  putStrLn $ "cnt is " ++ show (getSum cnt)

type Stack = ReaderT Integer (WriterT (Sum Integer) IO)

mything :: Stack ()
mything = do
  i <- ask
  tell $ Sum 1
  control otherthing
  liftIO $ putStrLn $ "i is " ++ show i

otherthing :: RunInBase (Stack) IO -> IO (StM Stack ())
otherthing run = do
  putStrLn "in IO here"
  run $ do
    j <- ask
    liftIO $ putStrLn $ "j is " ++ show j
    tell $ Sum 1

I hope somebody out there finds these little examples useful. My style of learning revolves around lots and lots of tiny examples; each one (hopefully!) understood thoroughly. This predilection seems to put me in a minority of Haskell programmers; so many packages on hackage have not a single example. Yes, in theory I can work out how things fit together from the types, but I often feel that I'm struggling to reverse engineer how a package is intended to be used.

I like instant rewards, and it's nice to start with an example that will actually compile. (The alternative, starting by bouncing back and forth between a bunch of error messages and the package documentation, is much less appealing.) Once I have something that will build, I can continuously deform it till it does what I want. If any small step introduces an error, then I can focus on what I did wrong in that step.

Anyway, as a result of putting together this blog post, I have improved my understanding of monad stacks considerably. Although it's not reached the level of grokking yet, I was able to generalize runTCPClient :: ClientSettings -> (AppData -> IO a) -> IO a and runTCPServer :: ServerSettings -> (AppData -> IO ()) -> IO () from the streaming-commons package to work with any monad m for which (MonadIO m, MonadBaseControl IO m) => m.