Search code examples
scalapromisefuturefor-comprehension

Scala Future[Option[T]] Un Packing


In the following snippet,

trait MyType1; trait MyType2
import scala.concurrent.Promise

val p1 = Promise[Option[MyType1]]()
val p2 = Promise[MyType2]()

I pass in p1 and p2 to another function where I complete the Promise using a successful Future. After the call to this function, I try to read the value in the Promise:

trait Test {
  // get the Future from the promise
  val f1 = p1.future
  val f2 = p2.future

  for {
    someF1Elem <- f1
    f1Elem     <- someF1Elem
    f2Elem     <- f1Elem
  } yield {
    // do something with f1Elem and f2Elem
    "..."
  }
}

When I try to compile this, I get some compiler issues.

Error:(52, 19) type mismatch;
 found   : Option[Nothing]
 required: scala.concurrent.Future[?]
      flElem     <- someF1Elem
                  ^

IntelliJ shows no errors or what-so-ever and the types look aligned. But I'm not sure why the compiler is unhappy! Any clues?


Solution

  • Your for comprehension types must be consistent, so you cannot freely mix Option and Future in the way you do.

    In your example, f1 returns a Future[Option[MyType1]] while f2 returns a Future[MyType2]

    Remember that a for comprehension desugars to a series of flatMap/map and potentially withFilter.

    Also the (simplified) signatures of flatMap for Future[A] and Option[A] are

    def flatMap[B](f: A => Future[B]): Future[B]
    def flatMap[B](f: A => Option[B]): Option[B]
    

    The first two steps of the for-comprehension desugar to

    f1.flatMap { someF1Elem =>
      someF1Elem.flatMap { f1Elem => // this returns a `Option[MyType1]`
         ...
      }
    }
    

    see the error now?


    Now, to fix this there's a few approaches you can follow. One very handy is to use Monad Transformers, which allow you to combine (for instance) Future and Option into a single monad type OptionT.

    Specifically you can go back and forth from Future[Option[A]] to OptionT[Future, A]. The basic idea is that you can flatMap on the latter and extract a value of type A.

    But before that, you need to make your types of the "right shape", so that both are a Future[Option[Something]]

    Here's an example using scalaz

    import scalaz._; import Scalaz._ ; import scalaz.OptionT._
    import scala.concurrent.{ Promise, Future }
    import scala.concurrent.ExecutionContext.Implicits.global
    
    trait MyType1
    trait MyType2
    
    val p1 = Promise[Option[MyType1]]()
    val p2 = Promise[MyType2]()
    
    val f1 = p1.future
    val f2 = p2.future 
    
    val res = for {
      f1Elem <- optionT(f1)
      f2Elem <- f2.liftM[OptionT]
    } yield {
      println(s"$f1Elem $f2Elem")
    }
    

    optionT builds an OptionT[Future, A] given a Future[Option[A]], while liftM[OptionT] achieves the same when you have a Future[A]

    The type of res is OptionT[Future, Unit] in this case, and you can get a Future[Option[Unit]] by calling run on it.