The Reader Monad — Part 1
This post will cover the foundations. It will mostly be an exercise in learning how to specialize types, simplify the substitutions and come up with the only reasonable implementation.
Motivation
The Reader
monad, or more generally the MonadReader
interface, solves the problem of threading the same configuration to many functions.
-- Imagine this is a directory
type Config = FilePathload :: Config -> String -> IO String
load config x = readFile (config ++ x)loadRevision :: Config -> Int -> IO String
loadRevision config x = load config ("history" ++ show x ++ ".txt")loadAll :: Config -> Int -> String -> IO (String, String)
loadAll config x y = do
a <- load config y
b <- loadRevision config x
return (a, b)
If you look at loadAll
you’ll see config
is not used, but is threaded through to the child functions. This is a common source of boilerplate and the reader monad attempts to ameliorate it.
So instead of threading the config
to each function, we can rewrite this using MonadReader
and the configuration will get passed implicitly. To retrieve the configuration, we call ask
:
-- Imagine this is a directory
type Config = FilePathload :: (MonadReader Config m, MonadIO m) => String -> m String
load x = do
config <- ask
liftIO $ readFile (config ++ x)loadRevision :: (MonadReader Config m, MonadIO m) => Int -> m String
loadRevision x = load ("history" ++ show x ++ ".txt")loadAll :: (MonadReader Config m, MonadIO m) => Int -> String -> m (String, String)
loadAll x y = do
a <- load y
b <- loadRevision x
return (a, b)
If you look at the intermediate functions loadRevision
and loadAll
we no longer have to take in and pass the config around. However the “leaf” function load
has gotten more complicated. We will later extend this example to make it reusable across concrete configurations and compare it to alternatives; but first some basics.
( (->) e), Reader, ReaderT and MonadReader
When Haskellers mention the “reader monad” they could be referring to one of four related things:
- The
Monad
instance for functions with the same first argument, which is written somewhat inscrutably as((->) e)
(which I think of as(e ->)
). type Reader = ReaderT Identity
- The
ReaderT
type - Anything that implements the
MonadReader
type class.
It’s worth understanding all four of these concepts.
What does a Monad for functions with the same first argument do?
Remember that a Monad
is also a Functor
and an Applicative
. To understand the monad for ((->) e)
we will try to guess the implementations for the Functor
, Applicative
and Monad
instances by looking at the types after substituting ((->) e)
into the type signatures.
Functor instance
First the Functor
instance. Let’s write out the type of fmap
.
Class Functor f where
fmap :: (a -> b) -> f a -> f b
It is not clear from looking at this type signature, what the Functor
instance for ((->) e)
will do.
One easy way to understand what the implementation of Functor
should be is to look at the implementation in base
. Another way is to infer it by writing out the specialized instance signature. This is a somewhat tedious process, but it is good practice for implementing instances and understanding how they must work.
The process starts by making a substitution for the type variable introduced in the type class, in this case f
.
So we substitute f = ((->) e)
:
fmap :: (a -> b) -> (((->) e) a) -> (((->) e) b)
Then we simplify
fmap :: (a -> b) -> (((->) e) a) -> (((->) e) b)
fmap :: (a -> b) -> ((->) e a) -> ((->) e b)
fmap :: (a -> b) -> (e -> a) -> (e -> b)
fmap :: (a -> b) -> (e -> a) -> e -> b
I am going to relabel the variables with the following substitutions, e = a
, a = b
, and b = c
(because I already know what to look for ;)).
fmap :: (b -> c) -> (a -> b) -> a -> c
And now we can see that fmap
for ((->) e)
is compose .
fmap :: (b -> c) -> (a -> b) -> a -> c
fmap f g x = f (g x)
There is no other non-evil implementation for that type signature.
This leads to the fun trick of trolling your coworker by writing fmap . fmap
as fmap fmap fmap
as in
> (fmap fmap fmap) (+1) [Just 1, Just 2, Nothing]
[Just 2, Just 3, Nothing]
Applicative instance
First let’s write out pure
.
pure :: a -> f a
substitute f = ((->) e)
pure :: a -> (((->) e) a)
simplify
pure :: a -> e -> a
So we end up with a function that takes in an a
and some random other argument e
and returns an a
. This must work for all e
s and a
s and there is no way to combine unknown types. Therefore, the only thing the function can do is return back the a
it was given. Hence it is const
:
pure :: a -> e -> a
pure x _ = x
(<*>) :: f (a -> b) -> f a -> f b
substitute f = ((->) e)
(<*>) :: (((->) e) (a -> b)) -> (((->) e) a) -> (((->) e) b)
Simplify
(<*>) :: ((e -> (a -> b)) -> (e -> a) -> (e -> b)
(<*>) :: (e -> a -> b) -> (e -> a) -> (e -> b)
(<*>) :: (e -> a -> b) -> (e -> a) -> e -> b
So the <*>
takes two functions that both have e
as the first argument and chains them to make a new function that takes an e
and gives the chained output.
(<*>) :: (e -> a -> b) -> (e -> a) -> e -> b
f <*> g = \e -> f e (g e)
Monad instance
We have already covered return
: it’s just pure
, which is just const
.
First, the type for bind:
(>>=) :: m a -> (a -> m b) -> m b
Substitute m = ((->) e)
(>>=) :: (((->) e) a) -> (a -> (((->) e) b)) -> (((->) e) b)
Simplify
(>>=) :: (e -> a) -> (a -> (e -> b)) -> (e -> b)
(>>=) :: (e -> a) -> (a -> e -> b) -> e -> b
Bind is basically a flipped-around <*>
(>>=) :: (e -> a) -> (a -> e -> b) -> e -> b
g >>= f = flip f <*> g
join
is more interesting. join
flattens a two layers of a monad to one.
join :: Monad m => m (m a) -> m a
Let’s substitute m = ((->) e)
join :: (((->) e) (((->) e) a))) -> (((->) e) a)
Simplify
join :: (((->) e) ((->) e a))) -> ((->) e a)
join :: ((->) e) (e -> a)) -> (e -> a)
join :: (e -> (e -> a)) -> e -> a
join :: (e -> e -> a) -> e -> a
There is only really one non-evil implementation for this type signature, and it is equivalent to the following:
join :: (e -> e -> a) -> e -> a
join f x = f x x
join
we get for free, but it is good to see how it could be implemented by hand. It’s sometimes used for creating a tuple with the same value for the first and second value.
> join (,) 1
(1, 1)
What is Reader?
You can think of Reader
as being a newtype
around (e -> a)
newtype Reader e a = Reader { runReader :: e -> a }
However, these days it is defined as a specialized version of ReaderT
.
type Reader = ReaderT Identity
For all intents and purposes, it works just like the Functor
, Applicative
and Monad
instances, ((->) e)
. There is really no reason to use it if ((->) e)
will suffice.
MonadReader
MonadReader
is the general interface for reader monads. The type class is essentially what follows:
class Monad m => MonadReader r m | m -> r where
ask :: m r
local :: (r -> r) -> m a -> m a
Let’s see what the implementation for ((->) e)
must be by substituting m = ((->) e)
and r = e
:
instance MonadReader e ((->) e) where
ask :: e -> e
ask = ?
local :: (e -> e) -> (e -> a) -> e -> a
local = ?
ask
can only really be one thing:
ask :: e -> e
ask = id
local
is a little trickier. It is not completely determined by the type. The documentation says it takes in a function e -> e
that modifies the environment and a e -> a
that uses the modified environment.
Here we go:
local :: (e -> e) -> (e -> a) -> e -> a
local f action = action . f
ReaderT
ReaderT
is the transformer version of Reader
. It allows you to add the “first argument threading” capabilities of “Reader” with another Monad
. A common choice is ReaderT e IO
. Our example at the beginning of the article could be rewritten with ReaderT e IO
instead of MonadReader
but little is gained by specifying the transformer stack directly. It is more flexible to write the functions using the reader monad interface MonadReader
.
One advantage of using ReaderT
directly is that we can take advantage of a more expressive version of local
, mainly withReaderT
which has the following type:
withReaderT :: (r' -> r) -> ReaderT r m a -> ReaderT r' m a
Unlike local
withReaderT
can change the type of the environment from r
to r'
.
Next Up
That’s all for now. In a future post I’ll discuss some enhancements and compare the Reader Monad against some alternatives.
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!