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. ↩