Monad transformer ordering

Posted on 2025-01-10 by Ernesto Hernández-Novich
Tags: ,

Once people understand that Monads are useful for many more things other than I/O, their next stage towards enlightenment is using Monad Transformers. Sooner than later they will be building three-or-more layered stacks, and ask: what’s the best ordering for this stack?

Transformer Stacks and Haskell applications

There are two major ways to build Monad Transformer stacks: either you have IO or Identity (pure values) at the bottom. Yes, you can have stranger things 1 at the bottom of your stack, but let’s stick to IO for the rest of this discussion. It’s what your average Haskell enjoyer is trying to write, anyway.

There’s not much to think about when you have a two-layered stack: whatever transformer you want is on top, and then IO will be on the bottom, with a polymorphic resulting value. Let’s say we want a ReaderT on top, graphically

    +------------------------+
    | ReaderT AppEnvironment |
    |                 +------+
    |                 | IO a |
    +-----------------+------+

The lower box is the base IO monad. The encasing box is the transforming ReaderT monad, adding its behavior to the encased box. The whole box is a monad. You’ll write the above as

import Control.Monad.Reader

data Environment = ...

type MyApp = ReaderT Environment IO

and then «run it»2 by unwrapping the stack from the outside in like so

appEntryPoint :: MyApp a
appEntryPoint = ...

runMyApp :: Environment -> MyApp a -> IO a
runMyApp initialEnv app = runReaderT app initialEnv

main :: IO ()
main = do e0 <- buildInitialEnvironment
          runMyApp appEntryPoint e0

No biggie. The above is, by far, the most common way to structure Haskell applications, where Environment carries both read only configuration values, as well as references to mutable (yes, mutable) values in the form of Software Transactional Memory or even IO References (IORef from Haskell base library).

The State and Maybe monads, and their transformers, are the next things to learn in similar fashion. So things like state transformer on top of IO

import Control.Monad.State

data AppState = ...
type StatefulApp = StateT AppState IO

runMyApp :: AppState -> StatefulApp a -> IO (a,AppState)
runMyApp initialState app = runStateT initialState app

or synchronous exception handling on top of IO

import Control.Monad.Except

data Failures = Boom | Bang | Dang
type ExplodingApp = ExceptT Failures IO

runMyApp :: ExplodingApp a -> IO (Either Failures a)
runMyApp app = runExceptT app

have a similar diagram as the one above (try drawing them), and the mechanics of evaluation are similar. The only differences being the returning type:

  • State over IO returns a tuple with the resulting value and the final state.

  • Asynchronous exception over IO returns either an error or the resulting value.

More layers

What if we want to write and application that carries a mutable state and can fail? We should be able to combine the StateT and the ExceptT transformers to accomplish this, but… in what order?

Monads are polymorphic, meaning the actual type for the state and the errors are irrelevant to the stacking. Let’s use simple type aliases, having Char for the state, and String for errors. This will make our dealings with the type system easier to understand.

We have two options for stacking our transformers on top of IO:

import Control.Monad.State
import Control.Monad.Except

type TheState = Char
type TheError = String

-- StateT on top of ExceptT on top of IO
type OptionA = StateT TheState (ExceptT TheError IO)

-- ExceptT on top of StateT on top of IO
type OptionB = ExceptT TheError (StateT TheState IO)

graphically

            Option A                    Option B
    +---------------------+     +----------------------+
    | StateT TheState     |     | ExceptT TheError     |
    |  +------------------+     |    +-----------------+
    |  | ExceptT TheError |     |    | StateT TheState |
    |  |           +------+     |    |          +------+
    |  |           | IO ? |     |    |          | IO ? |
    +--+-----------+------+     +----+----------+------+

Which one is better? Well, it depends on the actual result value (those question marks there), and your intentions as an application developer. We know those question marks aren’t simply a, because StateT and ExceptT produce complex resulting values. To figure out their values, and the answer to our design question, we should, as always, follow the types.

Now, we can follow the types with pencil and paper, by merely substituting type variables. But even after having a lot of experience doing it, it’s better to let the language help us out with that. Few other languages are as helpful.

We know we have to unwrap the stack from the outside in, so let’s have GHCi do that for us…

Option A

In order to unwrap

type OptionA = StateT TheState (ExceptT TheError IO)

we have to apply runStateT and runExceptT in succession. Looking at their signatures:

ghci> :m Control.Monad.State Control.Monad.Except
ghci> :type runExceptT
runExceptT :: ExceptT e m a -> m (Either e a)
ghci> :type runStateT
runStateT :: StateT s m a -> s -> m (a, s)

Notice runExceptT only needs the underlying stack, but runStateT needs the underlying stack first and the initial state second. Let’s use flip to swap runStateT argument positions

ghci> :type flip runStateT
flip runStateT :: b -> StateT b m a -> m (a, b)

so that we can harcode any Char as initial state, and then compose runExceptT

ghci> :type runExceptT . (flip runStateT) '*'
runExceptT . (flip runStateT) '*'
  :: StateT Char (ExceptT e m) a -> m (Either e (a, Char))

This composition now takes a two-layer stack like the one shown for OptionA, where m ~ IO and e ~ String, producing a result within the base monad (IO). The result type is precisely the one that should be in place of the question mark in the Option A diagram above. Moreover, using the type aliases TheState and TheError, we can now write the corresponding «run» function like

runA :: TheState -> OptionA a -> IO (Either TheError (a,TheState))
runA initial opA = runExceptT $ runStateT opA initial

that takes an initial state and a complete OptionA stack, and produces a result in the IO monad. We don’t need to use flip because we are providing the arguments explicitly.

Now pay atenttion to the resulting value’s type

Either TheError (a,TheState)

this means that when you apply runA to run your application you will get, either

  • Left wrapping an error, or
  • Right wrapping a tuple with the resulting value and final state.

I argue this behavior is transactional in terms of the state: changes to the state are only relevant if the computation succeeds, yet ignored in case of an error.

Option B

In order to unwrap

type OptionB = ExceptT TheError (StateT TheState IO)

we have to apply runExceptT and runStateT in succession. Making the same observations as before on their signatures and the need to use (flip runStateT) we get

ghci> :type (flip runStateT) '*' . runExceptT
(flip runStateT) '*' . runExceptT
  :: ExceptT e (StateT Char m) a -> m (Either e a, Char)

This composition now takes a two-layer stack like the one show for OptionB, where m ~ IO and e ~ String, and producing a result within the base monad (IO). The result type is precisely the one that should be in place of the question mark in Option B diagram above. Moreover, we can now write the corresponding «run» function like

runB :: TheState -> OptionB a -> IO (Either TheError a,TheState)
runB initial opB = runStateT (runExceptT opB) initial

that takes an initial state and a complete OptionB stack, and produces a result on the IO monad. We don’t need to use flip because we are providing the arguments explicitly.

Now pay atenttion to the resulting value’s type

(Either TheError a,TheState)

this means that when you apply runA to run your application you will always get a tuple:

  • The first component will be either an error message or the resulting value.
  • The second component will be the final state.

I argue this behavior is imperative in terms of the state: state changes are always relevant up to the point when the computation fails or succeeds.

Which one is better, then?

Well, it depends on what you’re trying to accomplish: transactional or imperative behavior3. If you don’t care about the internal state whenever an error occurs, OptionA is what you need. If you always care about the internal state, regardless of the application succeeding or failing, OptionB is what you need.

This reasoning technique applies for stacks having more layers: analyze the types for particular stack orderings and decide which one is a better match for your application’s needs.

The astute reader will immediately realize that an n-layer stack will have n! possible combinations. You can definitely try them as an exercise to check your understanding or whatever, but experience, and a deeper knowledge of monads helps out:

  • There are monad transformes that can be placed at any layer of a stack and will not change the structure of the outcome for the computation: ReaderT and WriterT are two examples.

  • There are monad transformes whose position in the stack will change the structure of the outcome for the computation: StateT, ExceptT, and MaybeT are three examples.

  • Is better to use the RWST transformer, and combine it with ExceptT, rather than use StateT, WriterT and ReaderT separately. Even if you are only going to use two out of the three functionalities. Types become easier to manage: transactional RWST or imperative RWST will get your quite far.

  • Never, ever, under no circumstances, use the WriterT or the W part of RWST for logging. Using it to build a resulting Monoid may work. If you need logging, use LoggingT from monad-logger to your desired logging destination.

  • If you are using a different library, check to see if their provided monad allows the embedding of environments instead of using transformers. For instance, when using Parsec is better to use

    ParsecT String MyState IO

    than

    StateT MyState (ParsecT String () IO)

…and use type aliases liberally to make type signatures easier to follow.


  1. You can have a list ([a]) or continuations (Cont). But if you know why you need them, you also know why they are not the best choice for the top-level application.↩︎

  2. You are not running anything. It’s just a naming convention for the evaluation of the sequence of steps triggered by the top-level monad value.↩︎

  3. And then some: StateT can be runStateT, evalStateT, or execStateT for different results. Try and figure out the types, and their meanings when combined with ExceptT.↩︎