What is a Reader Monad?
The Reader Monad works within the context of a shared environment. But what does that mean? Say you needed some shared object to execute a bunch of functions. An example could be that you need a database connection in every query function you execute. Or it could be some configuration options read from a file that are needed across a number of functions.
When trying to learn about the Reader Monad I’ve found most examples are convoluted or unnecessarily long. I hope to change that by providing a simple example that you can try out without too much head-spinning.
The Reader Monad is defined as:
type Reader r = ReaderT r Identity
One of the time-consuming things about learning the Reader Monad is that it is defined in terms of the ReaderT transformer (which is also a Monad). So now you have to learn multiple monads just to understand the Reader Monad. Annoying.
Let’s ignore the ReaderT transformer for now and assume that Reader is defined as:
Reader r a
where r is some “environment” and a is some value you create from that environment. And thanks to the type alias above you can just about do that.
Because Reader is a Monad we can do stuff like this:
import Control.Monad.Reader let r1 = return 5 :: Reader String Int
We have created a simple Reader using the Monad’s return function.
If we check the type of r1:
:t r1 r1 :: Reader String Int
We see that we have created a Reader that takes in a String and returns an Int. The String is the “environment” of the Reader. So how can we get the Int value out of the reader? By running it of course! We can use the runReader function to do that:
(runReader r1) "this is your environment" 5
runReader is defined as:
runReader :: Reader r a -> r -> a
So runReader takes in a Reader and an environment (r) and returns a value (a).
Now notice that we didn’t really do anything with the environment supplied to us.
What if we had a bunch of Readers and we wanted to bind across them?
import Control.Monad.Reader tom :: Reader String String tom = do env <- ask -- gives you the environment which in this case is a String return (env ++ " This is Tom.") jerry :: Reader String String jerry = do env <- ask return (env ++ " This is Jerry.") tomAndJerry :: Reader String String tomAndJerry = do t <- tom j <- jerry return (t ++ "\n" ++ j) runJerryRun :: String runJerryRun = (runReader tomAndJerry) "Who is this?"
The ask function is defined on MonadReader.
class Monad m => MonadReader r m | m -> r where
Let’s ignore MonadReader for now and focus on the definition of the ask function:
ask :: m r
Basically the above gives you a Reader Monad with the environment in it. So if you need access to the environment you ask for it. :)
In the tom, jerry and tomAndJerry functions, we are working within the context of the Reader Monad. That allows us to bind to the environment within the Reader. It also means that we need to return all values within a new Reader as well.
The tomAndJerry function binds to values from each Reader and then returns them combined in another Reader. We then run the whole lot in the runJerryRun function with the help of runReader and get the following output:
Who is this? This is Tom. Who is this? This is Jerry.
I hope this simple example is useful in getting you started in using and thinking about the Reader Monad.