In Scala, two interesting concepts you’ll often work with are Futures and Options. A Future is an abstraction for some value that might not be available yet. An Option abstracts over the possibility of a value.
Both Futures and Options are monads, meaning you can chain them together without explicitly unwrapping them. More formally, every monad F[A]
has the following three operations [1]:
map[B](f: A => B): F[B]
flatMap[B](f: A => F[B]): F[B]
pure(x: A) => F[A]
With these, you can easily combine Futures and Options. As a dummy example:
Future("hello").map(_ + " world").flatMap(x => Future(Some(x)))
// returns Future(Success(Some(hello world)))
However, what if you had a Future of an Option, i.e. Future[Option[A]]
? Perhaps a potentially long-running database fetch.
def getUserIdOptF: Future[Option[String]] = {
// ...some potentially long running database fetch...
}
def getSSNOptF(s: String): Future[Option[String]] = {
// ...get ssn if it exists...
}
Your code would unfortunately become more verbose:
getUserIdOptF.flatMap(_.map(num => getSSNOptF(num)).getOrElse(Future(None)))
Explanation: the map()
call returns a Option[Future[Option[A]]]
so we need the extra getOrElse()
call so that the function passed to flatMap()
obeys its signature, i.e. it returns a Future[Option[A]]
. This seems rather cumbersome, especially if many your methods share the same pattern of returning a Future[Option[A]]
.
To reduce the boilerplate, we can instead make Future[Option[A]]
itself a monad, so that we can simply call flatMap()
and pass in a function with type A => Future[Option[B]]
.
class FutureOption[A](futureOpt: Future[Option[A]]) {
def map[B](f: A => B): Future[Option[B]] = futureOpt.map(_.map(f))
def flatMap[B](f: A => Future[Option[B]]): Future[Option[B]] = {
futureOpt.flatMap(_.map(f).getOrElse(Future(None)))
}
def pure(a: A) = Future(Some(a))
}
Now this works:
val userIdOptF = FutureOption[String](getUserIdOptF)
userIdOptF.flatMap(getSSNOptF)
We can generalize this idea even further into something that can take any monadic type and wrap it around an Option type. This will allow us to define List[Option[A]]
for the List
type, Either[Option[A]]
for the Either
type, and similarly for every other monad that exists!
This type of thing is called a monad transformer, which basically lets you stick a monad inside another monad. We’ll use the OptionT
type in the cats package, which is a handy implementation of this concept. The same idea above can now be written much more succinctly:
import cats.data.OptionT
import cats.syntax.all._
def wrappedGetUserIdOptF: OptionT[Future, String] = OptionT(getUserIdOptF)
def wrappedGetSSNOptF(s: String): OptionT[Future, String] = OptionT(getSSNOptF(s))
wrappedGetUserIdOptF.flatMap(wrappedGetSSNOptF)
Note that while the outer monad can be generic, the inner is fixed to an Option. As far as I know, there’s no easy way to take in two arbitrary monads A
and B
and compose another monad A[B]
. The reasoning for this is beyond my understanding.
Lastly, you can imagine dealing with functions that return Futures and Options as well.
def getSSNOfSpouseOpt(s: String): Option[String] = {
// ...given someone's ssn, get their spouse's ssn...
}
def scrambleDigitsF(s: String): Future[String] {
// ... scramble digits of a ssn...
}
[2]
It would be nice to be able to combine those using the same general pattern. Fortunately, cats provides two functions that allow you to “lift” both Options and some type F
(in our case a Future) into an OptionT
.
liftF()
lifts a function returning typeF[A]
into anOptionT[F, A]
fromOption[F]()
does the same to functions returning Options
def wrappedGetSSNOfSpouseOpt(s: String) =
OptionT.fromOption[Future](getSSNOfSpouseOpt(s))
def wrappedScrambleDigitsF(s: String) = OptionT.liftF(scrambleDigitsF(s))
And we’re back to where we started - one operation to rule them all:
wrappedGetUserIdOptF
.flatMap(wrappedGetSSNOptF)
.flatMap(wrappedGetSSNOfSpouseOpt)
.flatMap(wrappedScrambleDigitsF)
-
map()
actually comes from applicatives, but every monad is also an applicative so must also define it. ↩ -
You might think these examples are ridiculous, but I doubt they’re beyond the abilities of a 21st Century Corporation. ↩