Reader T

This seems like a remarkably basic/uncreative thing to write about, but I almost never use the Reader monad in most of my personal projects or my day job. The former cases are usually too small to feature any configuration parameters, and the latter arrangement forbids fanciness in favor of readability. So both situations see me juggling IO, Either, and little else, with any "dependency injection" (wank term) handled in the most obvious manner: as a function parameter.

With that said, I have come to appreciate Reader for slightly more complex work. It lends itself nicely to a library like Halogen, which is usually written MTL style anyway, so I might as well throw a MonadAsk constraint onto my type signatures. A tutorial does not follow: just some notes that I can refer to later, since I reach for this construct so rarely. I used Purescript as a justification, but the examples will be in Haskell.


Always more important than "what?"

A function in the context of a Reader a has read-only access to a type a at any time. This is very useful for accessing env vars, network configuration, user settings, etc without requiring specific parameters to be passed to the function. When a is a record, new fields can be added, and new values can be accessed deep within the program without a massive refactor.

Simply passing a record as a function parameter mostly provides the same benefits. This is what we do at work. So the answer to "why?" is often "no good reason." But if you're already working in terms of a generic monad m, then it's cleaner to just add MonadReader a m to the function signature. With this in mind, it seems rare that one would want a Reader without other monads, so it's better to consider ReaderT.


Seek out a more explicit tutorial if needed, but the type ReaderT r m a is a newtype for a function

runReaderT ∷ r → m a

Where r is a read-only environment (usually some sort of config record) and m a is an output value a wrapped in an arbitrary monadic context m, like IO.

So running

runReaderT (f α) ω

Where f α is an operation in terms of monad m, and ω is a config object, will give f α and all its children read-only access to ω. That is, if m also satisfies the MonadReader constraint.


I've never read a good example of ReaderT, because anything concise enough to fit in a post also seems pointless. Here's another such example. Like I said, this lends itself best to generic monads with typeclass constraints, rather than explicit layers.

First consider a non-generic monadic operation.

print ∷ String → IO ()
print α = putStrLn α

This could be generalized, allowing it to compose better with other contexts.

print' ∷ MonadIO m ⇒ String → m ()
print' α = liftIO $ putStrLn α

Adding a MonadReader a m constraint to this signature gives m access to value a without the need for a specific parameter. Invoking the ask function returns the read-only environment: a string here. Even though ask is from the Reader monad, and print' from IO, both satisfy the constraints of m, and can be bound together in a singular context.

print'' ∷ (MonadReader String m, MonadIO m) ⇒ m ()
print'' = ask >>= print'

And now to go full circle. runReaderT calls print'' with a string environment α. Its output is the return value of print'' in its Reader-less context: here IO.

print''' ∷ String → IO ()
print''' α = runReaderT print'' α

If one wanted to provide an environment to a function that operated in terms of IO and Either, he or she could do something like

runReaderT (runExceptT $ f α) ω

An example as stupid as any, but the utility hopeful reveals itself when one considers a very deep m function that needs a value specified at startup time.


Not as often as you'd think, I said at first, but also more often than you'd think. Monad transformers are often spoken of as "stacks," but a ReaderT may replace many layers you don't actually need. "Read-only" doesn't even mean read-only; chuck a TVar into the environment. Haskell turns out to be great for statefully manipulating global values.