Let’s say you are a typical scala programmer, making plenty of use of Future
s in your code. Sooner or later you end up having APIs like the following:
case class Article(id: Int, ..., metaInformationId : Int)
case class MetaInformation(id: Int, key: String, value: String)
// get an Article from the DB if it there.
def getArticle(id: Int) : Future[Option[Article]] = ???
// retrieve MetaInformation. If it exists in an article it must be in the DB.
def getMetaInformation(id: Int) : Future[List[MetaInformation]] = ???
And for starters, let’s say you want to retrieve 3 articles and return something like Future[Option((Article, Article, Article))]
which implies that you want some tuple if you could retrieve all three articles, “none” tuple if any of the articles could not be found, and a failure if any of the database accesses failed.
A sample (and deliberately easy) implementation could look like this:
def tupleArticles(): Future[Option[(Article, Article, Article)]] = {
for {
optA1 <- getArticle(1)
optA2 <- getArticle(2)
optA3 <- getArticle(3)
} yield {
for {
a1 <- optA1
a2 <- optA2
a3 <- optA3
} yield (a1, a2, a3)
}
}
The outer for comprehension will enter all the Futures in sequence (Monad!), return the value captured within the Future, which in our case are optional articles or “return” with a failed Future. The inner for comprehension will then look at all the optional values (returning none, when one is none) and yield the tuple, which is after exiting the inner for comprehension an optional triple. We return again from the outer for comprehension and the optional triple will be an Future[Option[(…)]].
Monad Transformers
In our case both, Future
and Option
are monads, and in the example the future is not really something we are interested in. It is just some technical detail of the API. It would be much nicer, if we could treat the Future[Option[A]]
construct as just a Option. And we can do this using Monad Transformer
s. Here we use the OptionT
monad transformer. As is so often the case, a monad transformer will at first just wrap a value for us:
case class OptionT[F[_], A](run : F[Option[A]]) {
def getOrElse(o: A) = run.map(option => option.getOrElse(o))
def map[B](f: A => B) : OptionT[F, B] = OptionT(run.map(option => option map f))
}
Before we discuss this, let’s see it in action:
def tupleArticlesMT(): Future[Option[(Article, Article, Article)]] = {
val result: OptionT[Future, (Article, Article, Article)] = for {
a1 <- OptionT(getArticle(1))
a2 <- OptionT(getArticle(2))
a3 <- OptionT(getArticle(3))
} yield {
(a1, a2, a3)
}
result.run
}
So it saved a couple of lines, which could be condensed further, I just happened to add some type information of the intermediary results. Let’s take the code apart. The apply function of OptionT just takes any F[Option[A]] (that we deal with F = Future can be derived by scalac) and returns an OptionT. It is now the job of the OptionT to transform the Future[Option[A]] into something akin to an Option. Hence the name: transform the outer monad in such a way, that it momentarily feels like an Option. So the return values in the for comprehension will now be Article
s not Option[Article]
s as in the first try. We yield the triple, which results in an OptionT. To desugar the OptionT we simply call run
, which is the attribute of the OptionT monad transformer.
Cool. Let’s do that again.
Imagine we get a list of metainformation and we just want to get all the ids in a list. So the normal implementation would look like this:
def getIds() : Future[List[Int]] = {
for {
listMeta <- getMetaInformation(1)
} yield {
for {
meta <- listMeta
} yield {
meta.id
}
}
}
And we can have the same procedure as every year, this time wrapping our Future[List[A]]
in a ListT
:
def getIdsMT() : Future[List[Int]] = {
(for {
meta <- ListT(getMetaInformation(1))
} yield {
meta.id
}).run
}
So the principle is always the same. But Monad Transformers usually do more than just provide map
/flatMap
constructs, so that they can be used in for comprehensions. They also provide access functions which are typical for the Monad we are transforming into. Some examples to show the principle:
val r : Future[Article] = OptionT(getArticle(1)).getOrElse(Article(0, ...))
val s : ListT[Future, MetaInformation] = MetaInformation(1, "key", "value") :: ListT(getMetaInformation(1))
val t : Future[Option[MetaInformation]] = ListT(getMetaInformation(1)).headOption
Some practical hints:
- each time you have nested for comprehensions, you might be able to use Monad Transformers for clarity
- each time you deal with M[N[A]] constructs, where M and N are Monads you might be able to use Monad Transformers
- as with any Monad try to leave them as late as possible
- you can stack Monad Transformers, e.g. transform one transformer with another. This quickly leads to complex types and problems. Handle your tool with care
- many of the “simple” Monads in
scalaz
are implemented in terms of their Monad Transformers, e.g. State[S, A] is simply StateT[Id, S, A] - ListT, OptionT and EitherT (for scalaz Either replacement \/) are easy to deal with
- instead of pattern matching, you can very often use
fold
instead - check the docs and the companion objects - they contain plenty of useful transformer functions
Further ideas
Once you get used to the idea of a Monad Transformer, you can of course also exploit the fact that you can traverse and sequence over monads and to solve more complex problems.
For further articles in this series: TypeClass101
Kommentare