Skip to content

Reader

As a reminder, here's the definition of the Reader functor:

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

Let's verify that Reader is indeed a functor. Remember that I said before that we can think of a function—and Reader is just a thin newtype wrapper around a function—as a container that stores values of type a indexed by values of type r. Now consider a container xs = Reader g, where g :: r -> a, and assume we have a function f :: a -> b. Then fmap f xs = Reader h, for some function h. What should this function look like? First, it should have the type h :: r -> b. Second, not changing the "shape" of the container means that, for every index element i :: r, h i should return the result of applying f to g i: the element at "position" i in xs is replaced with the result of applying f to it. Let me say this one more time: For every i :: r, we want that

h i = f (g i)

Looks like function composition to me:

h = f . g

This gives us our Functor instance for the Reader functor:

instance Functor (Reader r) where
    fmap f r = Reader $ f . runReader r

Well, this is maybe a bit terse. So let's spell out what we really want. We want that

fmap f (Reader g) = Reader (f . g)

Let's rewrite this until we obtain the definition in the Functor instance for Reader r:

fmap f (Reader g) = Reader (f . g)
                  = Reader $ f . g                       -- Definition of ($)
                  = Reader $ f . runReader (Reader g)    -- Definition of runReader
fmap f r          = Reader $ f . runReader r             -- Substitute r for (Reader g)

We have to check the functor laws:

  • Identity:

    fmap id (Reader g) = Reader $ id . runReader (Reader g)    -- Definition of fmap
                       = Reader $ runReader (Reader g)         -- Definition of id
                       = Reader g                              -- Definition of runReader
                       = id (Reader g)                         -- Definition of id
    
  • Composition:

    fmap (h . f) (Reader g)
        = Reader $ (h . f) . runReader (Reader g)       -- Definition of fmap
        = Reader $ (h . f) . g                          -- Definition of runReader
        = Reader $ h . (f . g)                          -- Associativity of (.)
        = Reader $ h . runReader (Reader (f . g))       -- Definition of runReader
        = fmap h (Reader (f . g))                       -- Definition of fmap
        = fmap h (Reader $ f . runReader (Reader g))    -- Definition of runReader
        = fmap h (fmap f (Reader g))                    -- Definition of fmap
        = (fmap h . fmap f) (Reader g)                  -- Definition of (.)
    

To make this a little less abstract, here is a function that multiplies its argument by 2, only we express it as a Reader:

GHCi
>>> :{
  | newtype Reader r a = Reader { runReader :: r -> a }
  |
  | instance Functor (Reader r) where
  |     fmap f r = Reader $ f . runReader r
  | :}
>>> double = Reader (* 2)

Remember, viewed as a container, double stores one value for every possible argument of type Int. The value that it stores at index i is 2 * i:

GHCi
>>> runReader double 2
4
>>> runReader double 10
20

Now we want to turn this into a container where each value x is replaced with its string representation:

GHCi
>>> doubleString = fmap show double

Given that we had the values 4 and 20 at indices 2 and 10 in double, we expect the values at indices 2 and 10 in doubleString to be "4" and "20". That's exactly what we get:

GHCi
>>> runReader doubleString 2
"4"
>>> runReader doubleString 10
"20"

All this may seem like some really weird and unnecessary acrobatics. After all, Reader is just a wrapper around a function:

GHCi
>>> double = (*) 2
>>> double 2
4
>>> double 10
20

and fmap is just function composition:

GHCi
>>> doubleString = show . double
>>> doubleString 2
"4"
>>> doubleString 10
"20"

All we have done is to add a whole lot of boilerplate to our code, and boilerplate is usually never good. The Reader functor is nevertheless very useful, because we will see that we can use it to express computations that depend on some read-only context. Reader helps us to thread this context through the entire computation rather conveniently. We'll discuss this in the chapter on monads.