developer blog

Back to Originate.com

Scrap Your Cake Pattern Boilerplate: Dependency Injection Using the Reader Monad

There are a number of ways to do dependency injection in Scala without adding a framework. The cake pattern is probably the most popular approach and is used in the Scala compiler itself. Using implicit parameters is less popular and is used in the Scala concurrency libraries.

An approach that doesn’t get as much mention in the blogosphere but gets much love from those who have tried it is dependency injection via the Reader Monad.

To illustrate the differences between the Reader Monad approach and the more popular approaches, let’s look at examples of using each to solve the same dependency problem. These examples will all be injecting a dependency for getting User objects from a repository:

1
2
3
4
trait UserRepository {
  def get(id: Int): User
  def find(username: String): User
}

The Cake Pattern

In the cake pattern we represent the dependency as a component trait. We put the thing that is depended on into the trait along with an abstract method to return an instance.

1
2
3
4
5
6
7
8
9
trait UserRepositoryComponent {

  def userRepository: UserRepository

  trait UserRepository {
    def get(id: Int): User
    def find(username: String): User
  }
}

Then we declare the dependency in abstract classes using self-type declarations.

1
2
3
4
5
6
7
8
9
10
11
trait Users {
  this: UserRepositoryComponent =>

  def getUser(id: Int): User = {
    userRepository.get(id)
  }

  def findUser(username: String): User = {
    userRepository.find(username)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
trait UserInfo extends Users {
  this: UserRepositoryComponent =>

  def userEmail(id: Int): String = {
    getUser(id).email
  }

  def userInfo(username: String): Map[String, String] = {
    val user = findUser(username)
    val boss = getUser(user.supervisorId)
    Map(
      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"
    )
  }
}

We extend the component trait to provide concrete implementations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait UserRepositoryComponentImpl extends UserRepositoryComponent {

  def userRepository = new UserRepositoryImpl

  class UserRepositoryImpl extends UserRepository {

    def get(id: Int) = {
      ...
    }

    def find(username: String) = {
      ...
    }
  }
}

Finally we can create an instance of our class with the dependency by mixing in a concrete implementation.

1
2
3
object UserInfoImpl extends
  UserInfo with
  UserRepositoryComponentImpl

For testing we can create a test instance of our class and mix in a mock implementation.

1
2
3
4
5
6
object TestUserInfo extends
  UserInfo with
  UserRepositoryComponent {

  lazy val userRepository = mock[UserRepository]
}

Implicits

The cake pattern works really well but tends to require a lot of boilerplate. Another approach is to add implicit parameters to the methods that require the dependency.

1
2
3
4
5
6
7
8
9
10
trait Users {

  def getUser(id)(implicit userRepository: UserRepository) = {
    userRepository.get(id)
  }

  def findUser(username)(implicit userRepository: UserRepository) = {
    userRepository.find(username)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object UserInfo extends Users {

  def userEmail(id: Int)(implicit userRepository: UserRepository) = {
    getUser(id).email
  }

  def userInfo(username: String)(implicit userRepository: UserRepository) = {
    val user = findUser(username)
    val boss = getUser(user.supervisorId)
    Map(
      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"
    )
  }
}

This is a lot less code and has the advantage of allowing us to substitute a different UserRepository at any point either by defining our own implicit value or by passing it as an explicit argument.

A downside of this approach is that it clutters up the method signatures. Every method that depends on a UserRepository (userEmail and userInfo in this example) has to declare that implicit parameter.

The Reader Monad

In Scala, a unary function (a function with one parameter) is an object of type Function1. For example, we can define a function triple that takes an Int and triples it:

1
2
3
val triple = (i: Int) => i * 3

triple(3)   // => 9

The type of triple is Int => Int, which is just a fancy way of saying Function1[Int, Int].

Function1 lets us create new functions from existing ones using andThen:

1
2
3
val thricePlus2 = triple andThen (i => i + 2)

thricePlus2(3)  // => 11

The andThen method combines two unary functions into a third function that applies the first function, then applies the second function to the result. The parameter type of the second function has to match the result type of the first function, but the result type of the second function can be anything:

1
2
3
val f = thricePlus2 andThen (i => i.toString)

f(3)  // => "11"

The type of f here is Int => String. Using andThen we can change the result type as much as we want but the parameter type is always the same as in the initial function.

The Reader Monad is a monad defined for unary functions, using andThen as the map operation. A Reader, then, is just a Function1. We can wrap the function in a scalaz.Reader to get the map and flatMap methods:

1
2
3
4
5
6
7
8
9
import scalaz.Reader

val triple = Reader((i: Int) => i * 3)

triple(3)   // => 9

val thricePlus2 = triple map (i => i + 2)

thricePlus2(3)  // => 11

The map and flatMap methods let us use for comprehensions to define new Readers:

1
2
3
val f = for (i <- thricePlus2) yield i.toString

f(3)  // => "11"

If the above example looks strange, remember that it’s just a fancy way of writing:

1
2
3
val f = thricePlus2 map (i => i.toString)

f(3)  // => "11"

Dependency Injection with the Reader Monad

To use the Reader Monad for dependency injection we just need to define functions with a UserRepository parameter. We can wrap each of these functions in a scalaz.Reader to get the full monady goodness.

We define a “primitive” Reader for each operation defined in the UserRepository trait:

1
2
3
4
5
6
7
8
9
10
11
trait Users {
  import scalaz.Reader

  def getUser(id: Int) = Reader((userRepository: UserRepository) =>
    userRepository.get(id)
  )

  def findUser(username: String) = Reader((userRepository: UserRepository) =>
    userRepository.find(username)
  )
}

Note that these primitives return Reader[UserRepository, User] and not User. It’s a decorated UserRepository => User that you can use with for comprehensions. It’s a function that will eventually return a User when given a UserRepository. The actual injection of the dependency is deferred.

We can now define all the other operations (as Readers) in terms of the primitive Readers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object UserInfo extends Users {

  def userEmail(id: Int) = {
    getUser(id) map (_.email)
  }

  def userInfo(username: String) =
    for {
      user <- findUser(username)
      boss <- getUser(user.supervisorId)
    } yield Map(
      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"
    )
}

Because these methods use map and flatMap on the primitives (for comprehensions use map and flatMap), the methods return new higher-level Readers. The userEmail method in this example returns Reader[UserRepository, String]. All of the higher-level methods dealing with users return higher-level Readers built directly or indirectly from the primitives.

Unlike in the implicits example, we don’t have UserRepository anywhere in the signatures of userEmail and userInfo. We don’t have to mention UserRepository anywhere other than our primitives. If we gain additional dependencies we can encapsulate them all in a single Config object and we only have to change the primitives.

For example, say we needed to add a mail service:

1
2
3
4
trait Config {
  def userRepository: UserRepository
  def mailService: MailService
}

We would only have to change our primitives to take Config instead of UserRepository:

1
2
3
4
5
6
7
8
9
10
11
trait Users {
  import scalaz.Reader

  def getUser(id: Int) = Reader((config: Config) =>
    config.userRepository.get(id)
  )

  def findUser(username: String) = Reader((config: Config) =>
    config.userRepository.find(username)
  )
}

Our UserInfo object doesn’t need to change. This may seem like a small win in this example but it’s a huge win in a large application that has many times more higher-level Readers than primitives.

Injecting the dependency

So where does the actual UserRepository get injected? All of these methods return Readers that can get User-related stuff out of a UserRepository. The actual dependency injection keeps getting deferred up to higher layers.

At some point we need to apply one of these Readers to a concrete UserRepository to actually get the stuff. Normally this would be at the outer edges of our application. In the case of a web application, this would be the controller actions.

Assuming we have a concrete implementation of UserRepository as an object called UserRepositoryImpl, we might define a controller like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object Application extends Application(UserRepositoryImpl)

class Application(userRepository: UserRepository) extends Controller with Users {

  def userEmail(id: Int) = Action {
    Ok(run(UserInfo.userEmail(id)))
  }

  def userInfo(username: String) = Action {
    Ok(run(UserInfo.userInfo(username)))
  }

  private def run[A](reader: Reader[UserRepository, A]): JsValue = {
    Json.toJson(reader(userRepository))
  }
}

The object Application uses the default concrete implementation, and we can instantiate a test version using the class Application with a mock repository for testing. In this example we’ve also defined a convenience method run that injects the UserRepository into a Reader and converts the result to JSON.

But I can’t choose!

There’s no rule that says we have to use just one dependency injection strategy. Why not use the Reader Monad throughout our application’s core, and the cake pattern at the outer edge?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
object Application extends Application with UserRepositoryComponentImpl

trait Application extends Controller with Users {
  this: UserRepositoryComponent =>

  def userEmail(id: Int) = Action {
    Ok(run(UserInfo.userEmail(id)))
  }

  def userInfo(username: String) = Action {
    Ok(run(UserInfo.userInfo(username)))
  }

  private def run[A](reader: Reader[UserRepository, A]): JsValue = {
    Json.toJson(reader(userRepository))
  }
}

This way we get the benefit of the cake pattern but we only have to apply it to our controllers. The Reader Monad lets us push the injection out to the edges of our application where it belongs.

Comments