Replace Dependency Injection with the Reader Monad

Summary

You have functions with different dependencies, and want to compose them.

Remove dependencies from function arguments, and remodel the functions as partially-curried on those dependencies.

Motivation

The reader monad lets us encode dependencies directly into the type, while providing composability.

Example

Reader makes functions composable via a common super-dependency:

case class Reader[E, A](run: E => A) {
  def map[E2 <: E, B](f: A => B): E2 => B =
    (e => f(run(e)))
  def flatMap[E2 <: E, B](f: A => E2 => B): E2 => B =
    (e => f(run(e))(e))
}

An example dependency, and a function that depends on it:

trait NetworkConfig {
  def networkHost: String
  def networkPort: Int
}

def showNetworkConfig(c: NetworkConfig): String =
  s"the networkHost can be found at ${c.networkHost}:${c.networkPort}"

Another example dependency, and a function that depends on it:

trait DatabaseConfig {
  def databaseUrl: String
}

def showDatabaseConfig(c: DatabaseConfig): String =
  s"the database can be found at ${c.databaseUrl}"

An example program with two different dependencies; NetworkConfig and DatabaseConfig:

val showConfigs: NetworkConfig with DatabaseConfig => List[String] =
  for {
    l1 <- Reader(showNetworkConfig)
    l2 <- Reader(showDatabaseConfig)
  } yield List(l1, l2)

Injecting the dependencies to run the program:

val output: List[String] =
  showConfigs(
    new NetworkConfig with DatabaseConfig {
      override val networkHost = "localhost"
      override val networkPort = 8080
      override val databaseUrl = "jdbc:h2:mem:db"
    }
  )

println(output.mkString("\n"))

Demo

This file is literate Scala, and can be run using Codedown:

$ curl https://earldouglas.com/posts/itof/di-to-reader.md |
  codedown scala |
  xargs -0 scala -nc -e
the networkHost can be found at localhost:8080
the database can be found at jdbc:h2:mem:db