Skip to content

Instantly share code, notes, and snippets.

@noelwelsh
Last active October 23, 2020 14:27
Show Gist options
  • Save noelwelsh/7255b92a6f1faa1d77d277a1ac5b1595 to your computer and use it in GitHub Desktop.
Save noelwelsh/7255b92a6f1faa1d77d277a1ac5b1595 to your computer and use it in GitHub Desktop.
Discovering the Reader Monad

The Reader Monad

The reader monad is one solution to "dependency injection". This document shows how we might discover the reader monad when we attempt to solve this problem.

Dependency Injection

Our working definition of the dependency injection problem will be this: we have a method or function that takes certain parameters that we don't want to specify every time we call it. For example, we might have a function that gets a user given an ID and also requires a database connection.

def getUser(db: DbConnection, id: Id): User =
  ???

As the database connection usually doesn't change throughout the program we might want to be able to drop it from the parameter list of getUser. We could hard-code the database connection, but that loses flexibility we'll probably want in testing if not elsewhere. What we can do instead is have a method to which we pass a database connection, and this method returns us a function to get a user.

def getUser(db: DbConnection): Id => User =
  (id: Id) => ???

The idea is 1) to put the database connection in scope of the "real" getUser function and 2) to make that scope parameterisable.

In OO languages we have special syntax for this: classes and constructor parameters. So we can instead write

class WithDb(db: DbConnection) {
  def getUser(id: Id): User =
    ???
}

A class provides a scope that is shared by a number of methods / functions, and constructor parameters are the way we parameterise this scope.

Looking at the method-returning-a-function again, we started with

def getUser(db: DbConnection): Id => User =
  (id: Id) => ???

What we can instead do is flip this around. Instead of passing the DbConnection first we pass it second. This gives us

def getUser(id: Id): DbConnection => User =
  (db: DbConnection) => ???

This is the reader monad. All it means to use the reader monad is to write a function that returns a function that takes the dependencies as a parameter. The function that takes the dependency is the reader monad. More precisely, the reader monad is a function of type D => B, where D is the (fixed) type of the dependency, along with definitions of flatMap and pure.

Why might we want to do this? Imagine that we have more functions that have a database connection as a dependency. In an OO language we can just make them all methods on a class that accepts the connection as a constructor parameter. In an FP language we don't have classes so we need to go the function-returning-function route. If we take the dependency as the parameter to the first function the results will different types of parameters. For example, if we have functions to load and save a user we might have

def getUser(db: DbConnection): Id => User =
  (id: Id) => ???

def saveUser(db: DbConnection): User => Unit =
  (user: User) => ???

The functions that result from calling getUser and saveUser have different parameter types.

If we make the dependency the parameter of the second function (i.e. use the reader monad) the results of getUser and saveUser have the same parameter type.

def getUser(id: Id): DbConnection => User =
  (db: DbConnection) => ???

def saveUser(user: User): DbConnection => Unit =
  (db: DbConnection) => ???

What this means is we can "hide" this parameter in the reader monad, by composing together instances of DbConnection => A functions using flatMap and friends, and only supply it once. In the first, non-reader monad, we need to supply the dependency everywhere in our program. In the reader monad world we only supply the dependency once, at the "end of the world", and the magic of flatMap passes that dependency to everywhere it is needed.

What The Reader Monad Constructs

So what does the reader monad construct? In this section we give an operational view of the reader monad: that is, we (informally) trace through a simple application to see how it works in terms of underlying concepts.

Imagine we didn't need the dependency. We have getUser and saveUser and, say, some global database connection. We could write code like

saveUser(getUser(someId))

Since there is sequencing here (we save after we get) we can express this sequencing in terms of flatMap. (Specifically, using the Id monad.)

getUser(someId).flatMap{ user => saveUser(user) }

The result has type Unit.

Now if we add in the dependency (the DbConnection) we can write exactly the same code to construct a function DbConnection => Unit.

So assume we have:

  • a bunch of functions that return a function taking a dependency to a result (i.e. return the reader monad); and
  • we want to sequence calls to these functions, where "sequence" means to express some order in which they are called.

If this is the case then flatMap on the reader monad will construct one function that takes the dependency and applies that dependency to the individual reader monad functions in the order that we have specified, thereby executing the code that depends on the dependency in the order we have specified.

More concretely, if D is the type of our dependency and we have:

  • a: D => A and b: A => D => B; and
  • we want to evaluate a and then b

then we can write

val final = a.flatMap{ a => b(a) }

which constructs a function D => B.

When we apply the dependency to final (final(d)) it runs a with the dependency and then runs b(a) with the dependency.

So in summary the reader monad gives a big function that takes a dependency constructed out of little functions that take that dependency, and applies them to the dependency in the order specified by flatMap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment