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
= runReaderT app initialEnv
runMyApp initialEnv app
main :: IO ()
= do e0 <- buildInitialEnvironment
main 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)
= runStateT initialState app runMyApp 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)
= runExceptT app runMyApp 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:
> :m Control.Monad.State Control.Monad.Except
ghci> :type runExceptT
ghcirunExceptT :: ExceptT e m a -> m (Either e a)
> :type runStateT
ghcirunStateT :: 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
> :type flip runStateT
ghciflip runStateT :: b -> StateT b m a -> m (a, b)
so that we can harcode any Char
as initial state, and
then compose runExceptT
> :type runExceptT . (flip runStateT) '*'
ghci. (flip runStateT) '*'
runExceptT :: 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))
= runExceptT $ runStateT opA initial runA initial opA
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, orRight
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
> :type (flip runStateT) '*' . runExceptT
ghciflip 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)
= runStateT (runExceptT opB) initial runB initial opB
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
andWriterT
are two examples.There are monad transformes whose position in the stack will change the structure of the outcome for the computation:
StateT
,ExceptT
, andMaybeT
are three examples.Is better to use the
RWST
transformer, and combine it withExceptT
, rather than useStateT
,WriterT
andReaderT
separately. Even if you are only going to use two out of the three functionalities. Types become easier to manage: transactionalRWST
or imperativeRWST
will get your quite far.Never, ever, under no circumstances, use the
WriterT
or theW
part ofRWST
for logging. Using it to build a resultingMonoid
may work. If you need logging, useLoggingT
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.
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.↩︎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.↩︎
And then some:
StateT
can berunStateT
,evalStateT
, orexecStateT
for different results. Try and figure out the types, and their meanings when combined withExceptT
.↩︎