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.
The reader monad lets us encode dependencies directly into the type, while providing composability.
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 {
<- Reader(showNetworkConfig)
l1 <- Reader(showDatabaseConfig)
l2 } 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"))
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