Tooling the Reader Monad
This post is part of a series where I incrementally builds up the Reader dependency injection tool, using different advanced concepts of functional programming:
- Dependency Injection for Configuring Play Framework Database Connection(s) part 1
- Dependency Injection for Configuring Play Framework Database Connection(s) part 2
- Curry and Cake without type indigestion -- Covariance, Contravariance and the Reader Monad
- Tooling the Reader Monad
- Generalizing the Reader Tooling, part 1
- Generalizing the Reader Tooling, part 2
In the previous posts, I have build up to a working implementation of the Reader Monad to simplify Dependency Injection in Scala.
This is a very useful pattern and I recommend that you read the previous posts to understand it if you don't know about it yet. If you already know what it's all about and just want an implementation, it's at the bottom of the previous post.
In this post I will provide some useful functions to compose and work with Reader monad that simplify some common pattern of usages.
Composing Readers
You often end up with two separate readers, coming from different dependencies (e.g. different connections). The first function to help, is the zip
that will take two readers and return a single reader with the results of the two readers composed in a tuple:
case class Reader[-C, +A](run: C => A) {
…
def zip[B, D <: C](other: Reader[D, B]): Reader[D, (A, B)] =
this.flatMap { a =>
other.map { b => (a, b) }
}
}
You can then use it in the following way:
val reader1: Reader[UserConnection, User] = …
val reader2: Reader[PostConnection, Seq[Post]] = …
reader1.zip(reader2).map {
case (user, posts) => ...
}
Another common use case where you have many separate readers that you might want to compose is when you deal with collections of items:
val readerList: Seq[Reader[UserConnection, User]] = Seq(1,2,3).map(id => readUser(id))
You don't really want a collection of Readers, but a Reader of a collection. This can be easily solved:
def sequence[C, R](list: TraversableOnce[Reader[C, R]]): Reader[C,TraversableOnce[R]] = reader { conn =>
for { r <- list } yield r(conn)
}
and you can now do:
val list: reReader[UserConnection, Seq[User]] = sequence(readerList)
Reader and Future
As explained in the previous posts, the Reader monad is good to abstract your data sources. In most of the case, if you have access to a DB or to a webservice providing the data, your connection will return the eventual result in a non blocking Future
wrapper. If you also use Readers, you will end up having a strange mix of the two monads.
The first issue that arises is that when you combine Readers returning futures, you end up having a return type of Future[Reader[A, B]]
or even Future[Reader[A, Future[B]]]
.
This is because the two monads do not compose automatically. In the same way as you do not want a collection of Readers, it's better to have a Reader of a Future that you can compose with other Readers than having loads of Futures of Readers that are complicated to compose with Readers of non future values.
Using an implicit conversion, we can move the reader about and simplify our code with just two declarations:
implicit def moveFuture[A, B](future: Future[Reader[A, B]])(implicit context: ExecutionContext): Reader[A, Future[B]] = (conn: A) => {
for (reader <- future) yield reader(conn)
}
implicit def moveFutureFuture[A, B](future: Future[Reader[A, Future[B]]])(implicit context: ExecutionContext): Reader[A, Future[B]] = {
val future1 = moveFuture(future)
future1.map(f => f.flatMap(inf => inf))
}
The function signatures are a bit complex, but the only thing they do is to unwrap the reader within the future with a dependency provided outside the future (conn
). The second function is just a shortcut to flatten consecutive futures.
Another basic issue that arises is to create a "pure" value for a Future within a Reader. That is, a result that is already computed, without being defered or without having dependencies injected. We can write this quite simply:
def pure[A, B](value: B) = Reader.pure[A, Future[B]](Future.successful(value))
Once we have a future within a reader, the main issue is not get readable code. Often, you do not want to work on the Reader or the Future, but on the value wrapped inside the two. That is, you want to use a map
. So you end up doing something akin of:
val reader: Reader[A, Future[String]] = …
val parsed: Reader[A, Future[Int]] = reader.map(_.map(_.toInt))
Your code will soon be full of flatMap(_.map(…))
and map(_.map(…))
. Which are pretty ugly and make the code unreadable without any particular reason.
We can simplify this by declaring functions just for the case of readers containing futures:
implicit class ReaderFuture[-C, +A](val reader: Reader[C, Future[A]]) {
/**
* shortcut for flatMap{ _.map {...} } when the inner block returns a normal result
*/
def flatMapMap[B, D <: C](f: A => Reader[D, B])(implicit context: ExecutionContext): Reader[D, Future[B]] = reader.flatMap { future => future.map(f) }
/**
* shortcut for map(_.map(...))
*/
def mapMap[B](f: A => B)(implicit context: ExecutionContext): Reader[C, Future[B]] = reader.map { future => future.map(f) }
/**
* shortcut for flatMap{ _.map {...} } when the inner block returns a Reader[_, Future[_]] (we also move the future around
*/
def flatMapMapF[B, D <: C](f: A => Reader[D, Future[B]])(implicit context: ExecutionContext): Reader[D, Future[B]] = reader.flatMap { future => future.map(f) }
}
We use scala 2.10 implicit classes to define new function for any Reader[C, Future[A]]
.
-
flatMapMap
is called when you have amap
on a future within aflatMap
on a Reader -
mapMap
follows the same pattern, but when you are within amap
on a Reader
Finally, just to help the compiler resolve the implicit conversion and flatten consecutive Futures properly, we can use flatMapMapF
when the innermost map
block returns a future. Actually, this is the case that I most often encounter when coding with a mix of dependency injections.
You can find the full code of the Reader with all these tools in this gist.
Note that the same kind of implicits could be declared to deal with Option within Readers as this happens quite often too.
Written by Pierre Andrews
Related protips
2 Responses
Cool stuffs here, just a quick note if you want to mention it, ReaderFuture has a generic representation that is used for composing monad, which is Monad Transformer.
And MT are actually forming the basis of Scalaz transformer since the 7th version.
But it's just for mentioning, not very worthy ;-)
Indeed, it is a tranformer, I didn't go that far as it would have made the post way too complicated. Thanks for pointing it here, I'd already put a link in the last article of the series