A primer on Readers

The dream

Imagine we have functions:

def f: A => B = ???
def g: B => C = ???
def h: C => D = ???

The implementation may be whatever we want it to be, these functions might parse some data, perform calculations, format strings et cetera. Now, often we want to chain multiple operations together — ie. parse a JSON with sales data, then extract monthly sales figures and finally output an average. After all, coding is all about gluing blocks together to create a useful program. We could, of course, write h(g(f(x))) each time we want the whole functionality, but if it’s used frequently, giving it a proper name is a good idea. Scala gives us many different ways of gluing functions together, but I’m going to stick to the andThen from Function1 trait — because it behaves almost like the mathematical operator, just in reverse order1. With it creating a function i: A => D is as simple as writing:

def i: A => D = f andThen g andThen h

The reality

This kind of composition, obviously, works only for functions with one argument — with multi-argument functions we quickly run into a world of trouble. Quite unfortunate, since the vast majority of those we encounter on a daily basis need more than one input. But what if all of our functions look like this?:

def f: E => A => B = ???
def g: E => B => C = ???
def h: E => C => D = ???

This is again a common thing — oftentimes we need to pass some sort of a context (or other dependency) to our methods. It would be useful to be able to compose them, even if just to save us some writing. They share the type of the first argument — it must have made our task easier. What do we need to do to be able to compose them in a sensible way?

The promise

First of all, let’s simplify things a bit and imagine a function that magically reads some value V from an environment E. It’s going to have a type E => V. We’re going to call it Reader for precisely that reason, and we’re going to wrap it in a case class for convenience:

case class Reader[E, V](read: E => V)

We’re halfway there, we just need to notice that function parameters should be swappable (with possible minor tweaks to the logic inside — but usually we don’t depend on their order anyway). Then we can rewrite our f, g, and h as:

def f: A => E => B = ???
def g: B => E => C = ???
def h: C => E => D = ???

There’s a lot of Readers just waiting in line. We can now rewrite our functions again:

def f: A => Reader[E, B] = ???
def g: B => Reader[E, C] = ???
def h: C => Reader[E, D] = ???

The solution

Now we just need to compose these somehow. First of all, let’s give our Reader a helper method:

case class Reader[E, V](read: E => V) {
  def pipeTo[W](other: V => Reader[E, W]): Reader[E, W] = Reader { env =>
    val result1 = read(env)
    other(result1).read(env)
  }
}

Then we can make ourselves a helper:

object Reader {
  implicit class ReaderOps[E, A, B](f1: A => Reader[E, B]) {
    def andThenR[C](f2: B => Reader[E, C]): A => Reader[E, C] =
      a => f1(a).pipeTo(f2)
  }
}

Et voilà, we can now write this:

def i: A => Reader[E, D] = f andThenR g andThenR h

And it works!

A bit of black magic

We can do one more trick. Let’s first refactor the Reader to be able to use Scala’s wonderful for-comprehensions by renaming pipeTo to flatMap and adding a map method2:

case class Reader[E, V](read: E => V) {
  def map[W](f: V => W): Reader[E, W] = Reader(env => f(read(env)))
  def flatMap[W](f: V => Reader[E, W]): Reader[E, W] = Reader { env =>
    val result1 = read(env)
    f(result1).read(env)
  }
}

and then define:

def ask[E]: Reader[E, E] = Reader(identity)

Now we can write stuff like this:

val readIncrementAndStringify = for {
  intFromEnvironment <- ask[Int]
  incremented = intFromEnvironment + 1
} yield s"$incremented"

This yields a Reader[Int, String] value, which we can run afterwards to retrieve the final String when we need it — but the logic itself looks like pulling Ints out of thin air. Beautiful, isn’t it?

The use

One might ask: why does it matter at all? Aside of the obvious, this can serve as a dependency injection mechanism — functional and type safe. The E we plug into the Reader can be whatever we want — a default value for a given type, an input to a whole computation, a validator or a service with various utilities. We can even stuff multiple things into it!

type Env = (UserDeserializer, UserValidator)

def createUser(input: String): Reader[Env, Option[User]] = for {
  (deserializer, validator) <- ask[Env] //assuming we use better-monadic-for plugin. Who doesn't?
  deserialized = deserializer.deserialize(input)
  isCorrect = validator.validate(deserialized)
} yield if (isCorrect) Some(deserialized) else None

This idea is quite similar to how ZIO handles dependency injection3, and how it’s often done in the wide functional world. Of course this implementation is quite limited — but its purpose is to give the reader (pun intended) an intuition on how Readers work.

So to sum up: Reader is simply a functional programming pattern for composing two-argument functions that share one of its arguments, and it can be used for anything from passing common values around with less clutter to full-blown dependency injection. Go forth and read!


  1. Scala also defines the compose method that behaves exactly like mathematical function composition, but andThen is usually easier to follow, as it reflects the exact order of function application. 

  2. Now it becomes clearer why we decided to wrap our E => V function instead of relying on plain Function1 — we want to have map and flatMap methods available. 

  3. ZIO defines the ZIO[R, E, A] type, which uses the R type parameter to pass dependencies around in a Reader-esque fashion. For detailed explanation see:


Author | Straszydlo

Making you scared with bad code.