Simon C
Simon C

Reputation: 445

Future[Option[Future[Option[Boolean]] Simplifying Futures and Options?

I've been trying to simplify the way I do futures in Scala. I got at one point a Future[Option[Future[Option[Boolean]] but I've simplified it further below. Is there a better way to simplify this?

Passing a future of "failed" doesn't seem like the best way to do this. i.e. in the sequential world I simply returned "FAIL!!" any time it failed rather than continuing to the end. Are there other ways?

val doSimpleWork = Future {
  //Do any arbitrary work (can be a different function)
  true //or false
}

val doComplexWork = Future {
  //Do any arbitrary work (can be a different function)
  Some("result") //or false
}

val failed = Future {
  //Do no work at all!!! Just return
  false
}

val fut1 = doSimpleWork
val fut2 = doSimpleWork

val fut3 = (fut1 zip fut2).map({
  case (true, true) => true
  case _ => false
})

val fut4 = fut3.flatMap({
  case true =>
    doComplexWork.flatMap({
      case Some("result") =>
        doSimpleWork
      case None =>
        failed
    })
  case false =>
    failed
})

fut4.map({
  case true =>
    "SUCCESS!!!"
  case _ =>
    "FAIL!!"
})

Upvotes: 5

Views: 2291

Answers (3)

stew
stew

Reputation: 11366

a "Monad transformer" is a construct which lets you combine the "effects" of two monads, the scalaz project provides several different monad transformers. My suggestion is that you can use the OptionT monad transformer to simplify your code if you also make use of the fact that Option[Unit] is isomorphic to Boolean (Some(()) == true and None == false). Here's a complete example:

import scalaz._
import Scalaz._
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._
object Foo {

  // We need a Monad instance for Future, here is a valid one, or you can use the implementation
  // in the scalaz-contrib project, see http://typelevel.org
  implicit def futureMonad(implicit executor: ExecutionContext): Monad[Future] = new Monad[Future]  {
    override def bind[A, B](fa: Future[A])(f: A ⇒ Future[B]) = fa flatMap f
    override def point[A](a: ⇒ A) = Future(a)
    override def map[A, B](fa: Future[A])(f: A ⇒ B) = fa map f
  }

  // OptionT allows you to combine the effects of the Future and Option monads
  // to more easily work with a Future[Option[A]]
  val doSimpleWork : OptionT[Future,Unit] = OptionT(Future {
    // Option[Unit] is isomorphic to Boolean
    Some(()) //or None
  })

  val simpleFail : OptionT[Future,Unit] = OptionT(Future {
    None
  })

  val doComplexWork: OptionT[Future,String] = OptionT(Future {
    Some("result") //or None
  })

  val f1 = doSimpleWork
  val f2 = doSimpleWork
  val f3 = doComplexWork
  val f4 = doSimpleWork

  def main(argv: Array[String]) {
    val result = for {
      _ <- f1
      // we don't get here unless both the future succeeded and the result was Some
      _ <- f2  
      _ <- f3
      r <- f4
    } yield(r)

    result.fold((_ => println("SUCCESS!!")),println("FAIL!!"))

    // "run" will get you to the Future inside the OptionT
    Await.result(result.run, 1 second)
  }
}

Upvotes: 3

cmbaxter
cmbaxter

Reputation: 35453

You could try something like this, using for comprehensions to clean up the code a bit:

  def doSimpleWork = Future{
    //do some simple work
    true
  }

  def doComplexWork = Future{
    //do something complex here
    Some("result")
  }

  val fut1 = doSimpleWork
  val fut2 = doSimpleWork

  val fut = for{
    f1Result <- fut1
    f2Result <- fut2
    if (f1Result && f2Result)
    f3Result <- doComplexWork
    if (f3Result.isDefined)
    f4Result <- doSimpleWork
  } yield "success"

  fut onComplete{
    case Success(value) => println("I succeeded")
    case Failure(ex) => println("I failed: " + ex.getMessage)
  }

And if you really just wanted to print out "success" or "failed" at the end, you could change that last piece of code to:

  fut.recover{case ex => "failed"} onSuccess{
    case value => println(value)
  }

Now, to explain what's going on. For starters, we've defined two functions that return Futures that are doing some async work. The doSimpleWork function will do some simple work and return a boolean success/fail indicator. The doComplexWork function will do something more complex (and time consuming) and return an Option[String] representing a result. We then kick off two parallel invocations of doSimpleWork before entering the for comprehension. In for for comp, we get the results of fut1 and fut2 (in that order) before checking if they were both successful. If not, we would stop here, and the fut val would be failed with a NoSuchElementException which is what you get when a condition like this fails in a for comp. If both were successful, we would continue on and invoke the doComplexWork function and wait for its result. We would then check its result and if it was Some, we would kick off one last invocation of doSimpleWork. If that succeeds, we would yield the string "success". If you check the type of the fut val, its of type Future[String].

From there, we use one of the async callback functions to check if the whole sequence of calls either made it all the way through (the Success case), or failed somewhere in the process (the Failure case), printing out something related to which case it hit. In the alternate final code block, we recover from any possible failure by returning the String "failed" "and then use just the onSuccess callback which will print either "success" or "failed" depending on what happened.

Upvotes: 1

Alex Yarmula
Alex Yarmula

Reputation: 10667

Note that in your example, because you're eagerly instantiating the Futures to a val, all of them will start executing as soon as you declare them (val x = Future {...}). Using methods instead will make the Futures execute only when they're requested by the chain of execution.

One way to avoid the further computation would be to throw an exception, then handle it with onFailure:

def one = future { println("one") ; Some(1) }
def two = future { println("two") ; throw new Exception("no!"); 2 }
def three = future { println("three") ; 3 }

val f = one flatMap {
  result1 => two flatMap {
    result2 => three
  }
}

f onFailure {
  case e: Exception =>
    println("failed somewhere in the chain")
}

You can see here that "three" isn't supposed to be printed out, because we fail on two. This is the case:

one 
two 
failed somewhere in the chain

Upvotes: 3

Related Questions