The Reader monad is a simple and easy-to-use structure from the Haskell library that solves a common problem.
Very often, it's the case that you have a lot of functions that all need to use the same values. For instance, you might be laying out some text on a page, and you need to keep the details of the page size around in all your layout algorithms. Typically, you handle this situation by adding a new parameter to all your functions which represents the state. So,
layoutPage :: Text -> Page
becomes
layoutPage :: PageLayoutParams -> Text -> Page
This works just fine, but it can get really ugly. Every single function gets a new parameter, which must be threaded through to every related function that it ever calls. This causes visual clutter, particularly for functions that don't actually use the parameter themselves but just pass it on.
A function written in the Reader monad is augmented with a hidden parameter. For the hypothetical example above, we would write:
import Control.Monad.Reader
type LayoutFunc = Reader PageLayoutParams
layoutPage :: Text -> LayoutFunc Page
Note that the Reader monad is partially applied here. The result, LayoutFunc, is a new monad that adds a parameter of type PageLayoutParams to any code that runs inside it.
So, now that we have this monad, how do we use it? The "ask" operation retrieves the parameter that's hidden in the monad:
ask :: Reader a a
or, specialized to LayoutFunc:
ask :: LayoutFunc PageLayoutParams
In order to remain close to the example in question, I will provide specialized type signatures for the rest of the library functions. You can find the generic type signatures in
the Haskell library documentation for Control.Monad.Reader.
Suppose that layoutPage started out like this before it was moved into the monad:
layoutPage st txt =
let width = pageWidth st
...
In the Reader monad, this becomes:
layoutPage txt =
do st <- ask
let width = pageWidth st
...
As you can see, we've gone from one line of code to two. As this is clearly unacceptable for a clutter-reducing device, the Reader module provides a convenience function encapsulating the above pattern:
asks :: (PageLayoutParams -> a) -> PageLayoutFunc a
This allows us to simplify the monadic implementation to:
layoutPage txt =
do width <- asks pageWidth
...
It's quite common, when multiple functions share context information, to invoke a function in an altered context. For instance, we might have a layoutSubPage routine that changes the page width:
layoutSubPage st newWidth txt =
doLayout (st {pageWidth = newWidth} txt)
The Reader equivalent of this idiom is the function "local":
local :: (PageLayoutParams -> PageLayoutParams) -> LayoutFunc a -> LayoutFunc a
local executes an operation in a context modified by its first parameter. For instance,
layoutSubPage newWidth txt =
local (\st -> st { pageWidth = newWidth }) txt
Finally, to actually invoke a computation in the Reader monad, use runReader:
runReader :: LayoutFunc a -> PageLayoutParams -> a
main = showPage (runReader defaultPageParams someText)
For much more information on the Reader monad, see
All About Monads, or
the Haskell library documentation.