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 type F[A] into an OptionT[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)

  1. map() actually comes from applicatives, but every monad is also an applicative so must also define it. ↩

  2. You might think these examples are ridiculous, but I doubt they’re beyond the abilities of a 21st Century Corporation. ↩