Skip to content

Reader: Computing with Context

The three monads we have discussed so far—Maybe, Either e, and lists—modify what it means to execute the steps in a do-block in sequence. The next three monads—Reader, Writer, and State—stick with a sequential execution model that we are used to from imperative programming languages. These monads are provided by either the transformers or mtl package. We'll use the latter because it makes working with monad transformers slightly easier. Neither of these two packages is technically part of the standard library, but they might as well be; many Haskell projects use them.

  • State is the most flexible of the three. It models functions that depend on some state s and, in addition to returning some value, may modify the current state. In particular, any such function can both read and write s.

  • Reader is like State but cannot modify the state. We can think about functions that use the Reader monad as functions that rely on some read-only context, such as configuration settings read from a config file.

  • Writer is like State but it cannot read the state. It assumes that the state type is some monoid. Functions using the Writer monad can add to the state using the monoid's multiplication operation. A commonly used monoid is the list monoid. In this case, the state is like a log, and functions can add to this log without ever reading it.

Before looking at the simplest of the three, the Reader monad, let's ask ourselves why it is useful to have three monads that do fairly similar things. Clearly, any computation that uses the Reader monad could also use the State monad. Our functions read the state and simply don't use their ability to also modify it. They effectively use State as a Reader. Similarly, a function in the State monad does not have to read the current state, and we can implement these functions so they append to the state instead of overwriting it, just what the Writer monad would do.

The reason why it's useful to have three monads instead of only the State monad is communication of intent to the reader of our code. If we see a computation that uses the State monad, we know that it cannot perform I/O, but we must assume that some of the functions in this computation both read and write the state. To understand the code, we thus need to understand how each function updates the state. On the other hand, if we have a computation that uses the Reader monad, we know that none of the functions can modify the state. Thus, we can focus on the return values that flow from function to function and on the shared, read-only context of the entire computation. The logic of such code is simpler, so it takes less effort to wrap our heads around it. Similarly, a computation that uses Writer does not really pass any state from function to function, not even read-only state. For the most part, these functions behave like pure functions. All we need to understand is how the functions add to the log.1 In the end, having to use monads to do things we can "just do" in imperative languages takes some getting used to, but once we are comfortable with monads, the specific monad used by some piece of code tells us exactly what this code can and cannot do, and this simplifies the mental model we need to use to understand this piece of code.

Now let's look at the Reader monad. Here is the Reader data type again:

newtype Reader r a = Reader { runReader :: r -> a }

When discussing that Reader r is a functor, that is, a well-behaved container, I mentioned that we can think of Reader r a as a container that stores values of type a indexed using values of type r. Here, it is more helpful to think of a value of type Reader r a as a value of type a that depends on some read-only context of type r. This is the decoration provided by the Reader r monad. To make Reader r a monad, we once again need to figure out how to implement return and (>>=). First return.

return x should take a pure value x :: a and decorate it to produce a value of type m a or, here, a value of type Reader r a. Since Reader r a is essentially simply a function of type r -> a, we somehow need to turn x into a function that maps a context of type r to a value of type a. Since we don't know anything about the types r and a, there isn't all that much we can do. Pretty much the only thing we can do is to ignore the context and simply return x. So this gives

return x = Reader (const x)

or in point-free form,

return = Reader . const

return x is a computation that produces x no matter the context. It ignores the context. But that's fine. Nobody says that every computation in the Reader monad absolutely must read the context. Taking the view of Reader r as a container indexed by the type r, return x is the container that stores x at every possible index.

Now what should x >>= f look like? If x :: Reader r a and f :: a -> Reader r b, then x >>= f :: Reader r b. So x >>= f should wrap a function of type r -> b:

x >>= f = Reader $ \r -> ...

How can we produce a value of type b from a value of type r having only x and f at our disposal? We have runReader x :: r -> a. For some value y :: a, we also have runReader (f y) :: r -> b. The only thing we can do to produce a value of type b from a value of type r is to apply runReader (f y) to r:

x >>= f = Reader $ \r -> let y = ...
                         in  runReader (f y) r

To do this, we first need a value y of type a, to which we can apply f. The only tool we have to produce such a value of type a is to apply runReader x to our context r. Thus, the only possible implementation of x >>= f is

x >>= f = Reader $ \r -> let y = runReader x r
                         in  runReader (f y) r

Remember that, for any monad m, x >>= f should somehow capture the idea of applying f to x, only x is not a pure value—it's a value decorated by the monad m. f's return value is also decorated. The decoration provided by Reader r is the ability to depend on the context r. If you squint hard enough at the definition of x >>= f, then you can see that the function \r -> let y = runReader x r in runReader (f y) r implements a whole family of function applications, one for each possible context r: Given a particular context r, we extract the value y that x has in this context, by using runReader x r. We then apply f to this value, but even the value of this application of f to y depends on the context. By passing the same value r to runReader x and to runReader (f y), we evaluate x and f y in the same context. Our implementation of x >>= f ensures that we thread the same read-only context through the entire computation.

The final Monad instance for Reader r is

instance Monad (Reader r) where
    return  = Reader . const
    x >>= f = Reader $ \r -> let y = runReader x r
                             in  runReader (f y) r

Accessing the Context

Inside a do-block, if f :: m a, then the statement x <- f assigns to x the value of type a returned by f. In other words, statements inside a do-block don't see the decoration provided by the monad m! Whatever additional information the monad carries is threaded through the computation behind the scenes via the monad's (>>=) operator. That was the whole point of do-notation: not having to deal with the decoration provided by the monad and pretending that we're working with pure undecorated values.

The point of the Reader r monad is to implement computations that depend on some context of type r. It would be silly if we had to abandon do-notation and use the (>>=) operator directly whenever we need to access this context. To avoid this, the Control.Monad.Reader module provides us with functions to access the context from within do-blocks.

The first one is

ask :: Reader r r
ask = Reader id

Given that the function wrapped by Reader is the indentity function r, the value returned by ask is simply the context of the monad. Thus, we can write ctxt <- ask inside a do-block to assign the context to the local variable ctxt, and then the rest of the computation inside the do-block can freely access ctxt to compute values that depend on this context.

It is also useful to be able to retrieve only part of the context. Given a context of type r, we can think of a function of type r -> p as extracting some part of the context of type p. Control.Monad.Reader gives us the function

asks :: (r -> p) -> Reader r p
asks f = Reader f

With this, we can write partOfCtxt <- asks f, and partOfCtxt becomes the part of the context accessed by f. For example, if our context is a collection of configuration settings read from a configuration file, we might represent our context as

data Config = Cfg
    { verbosity   :: Int
    , usePager    :: Bool
    , writeToFile :: Maybe FileName
    }

Few functions in our program depend on the entire context. If a function cares only about whether we should use a pager when printing our program's output on screen, for example, we could write

do usePgr <- asks usePager
   if usePgr then
       -- Do whatever we need to do to use a pager
   else
       -- Do whatever we need to do to proceed without using a pager

Pure Functions with Context

None of the monads we have discussed so far expresses anything we cannot model using pure functions—that's evident from the fact that we were able to implement the monad instances for Maybe, Either e, lists, and Reader r in plain Haskell. The only monad that we will discuss later that captures true side effects that we cannot implement using pure functions is the IO monad.

There is a difference between the Maybe, Either e, and list monads and the Reader r monad though. If a function f uses the Maybe or Either e monad, its type is

f :: a -> Maybe b

or

f :: a -> Either e b

We may be using the fact that Maybe and Either e are monads to implement such a function more elegantly, but from the outside, we can also simply view f as a pure function that returns a value of type Maybe b or Either e b, and this is presumably exactly the result type we want because we need to know whether f succeeded or not.

Similarly, when using the list monad to implement a function of type

f :: a -> [b]

then we may use the list monad to implement such a function, but we can also view this function as a pure function that maps a value of type a to a list of bs, and we are usually interested in all of those bs, so the return type is exactly the type we want.

Reader r is different. When we apply a function of type

f :: a -> Reader r b

to some argument of type a, we don't obtain any value of type b as a result. What we have, wrapped in Reader, is a function of type r -> b. To obtain a value of type b, we need to apply this function to some context of type r. What we really want is to be able to convert values of type Reader r b back into pure functions. For example, instead of the function f :: a -> Reader r b, we would really like to have a plain old function

runFWithContext :: a -> r -> b

So, while functions that use Maybe, Either e or lists can be viewed as pure functions, we need the ability to "run" any computation in the Reader monad somehow to obtain an actual value. That's exactly what the runReader accessor function of the Reader monad does—the name is no coincidence. Using this accessor function, we gain access to the function wrapped by Reader. Thus, we can implement runFWithContext simply as

runFWithContext a r = runReader (f a) r

f a has type Reader r b. Thus, it wraps a function of type r -> b. runReader (f a) accesses this function. And finally the whole expression runReader (f a) r applies this function to the context r.

The pattern is that we want to implement a computation composed of functions that all depend on some value of type r. Instead of passing this value around explicitly, as an argument to each function we call, we do so implicitly, by using the Reader monad. However, the Reader monad is merely a convience. In the end, we still have a pure function, and runReader allows us to convert our computation that uses the Reader monad back into a plain old function whose type doesn't even reveal that the implementation uses the Reader monad under the hood.

We will see the same pattern again when we look at the Writer and State monads next, which are also merely conveniences to implement computations that could have been implemented using plain old functions. The State s monad provides us with a computation that depends on and potentially modifies some state of type s. We will have a function

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

If f :: State s a is a computation that operates on some state s and returns a value of type a, then runState f init runs f with the state initialized to init, and the result is a pair consisting of the return value of f and the state left behind once f is finished. The function runState f is a plain function of type s -> (a, s). Nothing in this type signature reveals that a monad is involved; the State monad is used merely to implement this function more easily.

For the Writer monad, we will have the function

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

that will make sense once we discuss the Writer monad. Again, if f :: Writer w a, then runWriter f is a value of type (a, w). Nothing in this type reveals that a monad is involved in producing it.

Back to the Reader monad. Here's an example that is a bit less abstract:

main :: IO ()
main = do
    cfg <- readConfigFile

    let result = runReader programLogic cfg

    writeResultToFile result

readConfigFile :: IO Config
readConfigFile = ...

programLogic :: Reader Config String
programLogic = ...

writeResultToFile :: String -> IO ()
writeResultToFile = ...

This example uses both the Reader monad and the IO monad, which we discuss towards the end of this chapter. It provides the ability to perform I/O, such as reading and writing files or printing things on screen. main is the entry point of every Haskell program, just as the main function in C, C++ or Rust or the main method in a Java program. Its type is IO (), that is, it uses I/O to read its input and write its output, and it does not return anything: any result it produces needs to be communicated to the user by writing this result to the screen or to a file, or maybe by sending the result over a network connection, before the program exits.

In this example, I assume that we have some function readConfigFile that reads our program's configuration file and returns the result in the form of a Config object, where Config is a type we use to represent configuration settings of our program, just as the Config type above does. The main program logic is implemented by a function programLogic. This function uses the Reader Config monad to pass the configuration between function calls. The result of this computation is some string. To run this computation and collect the result, our main function calls runReader programLogic cfg, passing the configuration read by readConfigFile to our computation. The final step of our main function is to write the result to some file, using a function writeResultToFile whose implementation I also didn't provide here.


  1. The compiler also has more opportunities to optimize code in the Reader and Writer monads. Nowadays, GHC is really good at scheduling the evaluation of pure functions that don't depend on each other's return values so they get evaluated in parallel, in separate threads, on modern CPUs. This is one of the advantages of functional programming that proponents often cite: we get parallelism for free. We don't have to worry about expressing what can run in parallel ourselves. The compiler does this for us. Given that functions can only depend on each other via their arguments and return values in purely functional code, it is much easier for the compiler of a functional language to determine which parts of a program can be executed in parallel. Thus, automatically parallelizing functional code is much easier than automatically parallelizing imperative code.

    The State monad is the essence of sequentially threading state through a computation. Each function potentially depends on updates to the state made by a previous function. The compiler's opportunities to parallelize such code are extremely limited. For the Reader monad, there is no state to be modified. Just as pure functions, functions that use the Reader monad depend on each other only if the result of one gets passed to the other as an argument. The compiler can optimize such code just like ordinary pure functions. Functions in the Writer monad are essentially pure functions, except they also add to a log. However, the additions to the log are combined using monoid multiplication, which is associative. Thus, we can once again evaluate functions in the Writer monad in parallel and use the associativity of the monoid multiplication to combine the log messages efficiently.