Day 4: Reader, Writer, State — Wiring Together Functions

Ben Clifford
Twelve Monads of Christmas
3 min readDec 11, 2016

Reader, Writer and State usually come presented together, because they’re very similar: these monads wire together functions by adding in extra parameters or return values. There is no non-determinism, change in control flow, or container-like behaviour (like [] or Maybe); and there are effects on the real world (like IO); and all three can be evaluated by pure functions (like runReader).

  • Reader: Every action is a function with an extra parameter: the environment. An action can read the environment by simply looking at that parameter; but it cannot change the environment that is seen by subsequent actions.
  • Writer: every action is a function which returns an extra value to be written. An action cannot see what other actions have already written. Instead, the written output is accumulated using Monoid and can be seen by the caller of runWriter.
  • State: every action is a function which takes an extra value (the old state) and returns an extra value of that same type (the new state). Actions can see the inbound state and modify it as they please before passing it on to the next action.

Although these monads work with “mostly pure” actions, sequencing still happens. Most obviously, with State we can’t pass in the state to an action until we’ve got it from the previous action (although it can be lazily evaluated — we only need a thunk). The sequencing in Writer is the sequence in which values are accumulated in the output monoid. Reader seems the most subtle to me: although all actions read the same immutable environment, you can still choose which actions to execute later on based on that value. So you might choose to run an expensive computation, or a cheap computation, based on the environment supplied right at the start.

Variation: “Querying” monads

Reader has a law m >> n = n.

If we do some “reading” computation m, and then ignore the result, then we might as well have not bothered with m at all. n cannot observe what m has done — the only way n could know would be if it received a value from m — but >> does not pass that value on.

Other seemingly quite different monads might also strive to satisfy that law — for example Haxl and the unique supply monad.

Variation: ST

ST s is a state monad, like State, but it exposes access to that state in a very different way. Rather than there being a single state value, individual pieces of state, STRefs, can be created, read and write — quite like IORefs but without needing IO.

This way, you can write programs that have mutable variables, while constraining where that mutability can effect things, and also avoiding a single giant state data structure.

That s in the ST s type signature is a phantom type: you don’t usually put a concrete type in there, but it serves to stop you passing references from one state run into another state run (or at least, if you do, you can’t read/write those references).

The implementation of this is quite different too, and involves a suspicious GHC.Magic module.

Variation: Identity

This is an even simpler wiring-together monad. It doesn’t add anything to the functions, so all we do is wrap up a pure computation in monad syntax.

This seem pretty useless. It seems pointless to write:

runIdentity $ do
x <- return 64
return (sqrt x)

when we could write this more naturally:

let x = 64
in sqrt x

Identity turns out to be useful when using monad transformers:

A monad transformer always needs a monad to transform. So do we need to implement a transformer and it’s base equivalent (e.g. ReaderT and Reader) separately? One with a hole for another monad below and one without? NO! We can implement just ReaderT and then stick Identity in the hole to give us Reader.

(Side note: monad transformers give monad morphisms, mentioned yesterday — lift is a monad morphism that takes an action in the underlying monad and turns it into an action in the transformed monad)

--

--