Published 2014-09-22.
Last modified 2015-01-08.
Time to read: 4 minutes.
We first discussed for-comprehensions in the For-Loops and For-Comprehensions lecture earlier in this course. This lecture continues the discussion of Scala Futures by exploring how for-comprehensions work with Futures.
The sample code for this lecture is provided in
courseNotes/
and uses the factorial and readUrlFuture methods introduced in previous lectures, found in
courseNotes/.
To get the results of a Future without blocking,
use a combinator like map and flatMap as we will discuss in this lecture and the next,
or use a for-expression.
When a Future is used as a generator in a for-expression,
the value of the successful Future will be assigned to the left-hand variable,
and will remain available for the remainder of the for-expression.
This is true for for-loops and for-comprehensions.
We'll discuss how to handle failed Futures in the next lecture.
Let's first try printing out the first 200 characters of a URL using a for-loop and a Future.
scala> for { value <- readUrlFuture("https://scalacourses.com", 200) } println(s"For loop: value=$value")
<!DOCTYPE html> <html lang="en"> <head> <title>Welcome to ScalaCourses.com</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <script src='/webjars/jquery/1.9.1/jqu
Notice that the REPL prompt appeared, then a pause, then the output of the future appeared. This is not a bug.
Notice what happens with a for-comprehension instead of a for-loop:
scala> for { value <- readUrlFuture("https://scalacourses.com", 200) } yield value res0: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@5523cc24
As is the case with all monads, a for-comprehension returns the same monadic type as was supplied by the generators.
We learned in the For-Loops and For-Comprehensions
lecture that is how for-comprehensions and map/flatmap works.
This is why a completed Future is returned from the for-comprehension, containing the value,
implemented as a Promise subclass known as DefaultPromise.
Inside the for-comprehension, the Future's value is available.
Let’s explore all of this in much more detail.
Sequential Execution
As a quick reminder, here is a for-comprehension that uses Lists instead of Futures:
scala> val list: List[BigInt] = for { x <- List(factorial(123)) y <- List(factorial(234)) if y/10>x z <- List(factorial(345)) x%2==BigInt(0) } yield x + y + z list=List(242156386507923 ...really long output...)
The expression to the right of the <- symbol, known as a generator,
creates bindings for the variable on its left.
This for-comprehension has 3 generators, one for each of x, y and z.
If any generator fails the following generators will be suppressed.
The definition of what constitutes failure is dependant on the monadic type of the generator.
Lists 'fail' when they consist of the empty list (Nil),
so if x evaluates to Nil,
the generators for y and z will not be computed and Nil
will be yielded as the value of the for-comprehension.
Similarly, if y evaluates to Nil or the guard y/10>x evaluates false,
the generator for z will not be computed for that value of y
and that iteration of the for-comprehension will not yield a value.
Also, if z evaluates to Nil or x is an even number,
the iteration for that value of z will not yield a value.
Here is another for-comprehension that uses Options:
scala> val option: Option[BigInt] = for { x <- Option(factorial(123)) y <- Option(factorial(234)) if y/10>x z <- Option(factorial(345)) if x%2==BigInt(0) } yield x + y + z option=Some(242156386507923 ...really long output...)
Options fail when they return None, so:
-
If
xevaluates toNone, the generators foryandzwill not be computed andoptionwill be set toNil. -
Similarly, if
yevaluates toNoneor the guardy/10>xevaluatesfalse, the generator forzwill not be computed and the loop for that value ofywill be suppressed. Once again,optionwill be set toNil. -
Finally, if
zevaluates toNone, orxis an even number, the iteration for that value ofzwill not yield a value, andoptionwill be set toNil.
Following is an example of a Scala for-comprehension that uses Futures.
This for-comprehension evaluates each of the terms x, y and z sequentially.
I'll show you a similar for-comprehension that computes the value of the terms in parallel shortly.
scala> val futureResult: Future[BigInt] = for { x <- Future(factorial(123)) y <- Future(factorial(234)) if y/10>x z <- Future(factorial(345)) x%2==BigInt(0) } yield x + y + z futureResult andThen { case Success(r) ⇒ println(s"Success: $r")
case Failure(ex) ⇒ println(s"Failure: ${ex.getMessage}") } Future value=242156386507923 ...really long output...
This example is like the preceding example, but creates a future for each generator. Each generator runs sequentially! The generators do not run simultaneously! Using the futures in this way probably does not do what you want. See the Parallel Execution section below to see how to run the futures simultaneously.
As you know, Futures fail when they complete with a Throwable.
The result of this for-comprehension is a new Future, called future1,
containing the result of adding the values of the other Future values together after they each run to completion,
one after the other.
If the Future that generates x fails,
the Future that would have generated the value for y will not be invoked.
Similarly, if the Future that generates y fails,
the Future that would have generated the value for z will not be invoked.
If any generator fails the yield statement will not be executed,
and the result of the for-comprehension will be the Throwable from the failed Future
wrapped inside a new failed Future.
We can run this program by typing:
$ sbt "runMain multi.futures.ForCompSerial1" Success: 4272888918078 ... really long output
Here is another way to write the above, without the intermediate futureResult variable.
(for { x <- Future(factorial(123)) y <- Future(factorial(234)) if y/10>x z <- Future(factorial(345)) x%2==BigInt(0) } yield x + y + z) andThen { case Success(r) ⇒ println(s"Success: $r") case Failure(ex) ⇒ println(s"Failure: ${ex.getMessage}") }
Output is the same.
You can run this program by typing:
$ sbt "runMain multi.futures.ForCompSerial2"
Output is the same as the previous example.
Equivalent flatMap/map Code
The Scala compiler rewrites the above as follows; the complete program is provided as ForComp3.
This version is somewhat simpler because it does not have any guards,
which would be implemented via filterWith method invocations.
Future(factorial(123)).flatMap { x ⇒
Future(factorial(234)).flatMap { y ⇒
Future(factorial(345)).map { z ⇒ x + y + z }
}
} andThen {
case Success(value) ⇒
println(s"Success: $value")
case Failure(ex) ⇒
println(s"Failure: ${ex.getMessage}")
}
As you can see, the body of the yield statement in the for-comprehension is used as the body of a
function passed to map, and the preceding assignments are used to form enclosing flatMaps.
This formulation is probably a little harder to read,
so I encourage you to use for-comprehensions instead of nested flatMaps / map.
You can run this program by typing:
$ sbt "runMain multi.futures.ForCompSerial3"
Parallel Execution
All that is necessary in order to cause multiple futures to be candidates for simultaneous execution is to
initiate them prior to referencing them as generators in a for-comprehension.
They will all execute as soon as free threads are available to be allocated to the Futures so they can begin evaluation.
val futureX: Future[BigInt] = Future(factorial(123)) val futureY: Future[BigInt] = Future(factorial(234)) val futureZ: Future[BigInt] = Future(factorial(345))
val futureResult: Future[BigInt] = for { x <- futureX y <- futureY if y / 10 > x z <- futureZ if x % 2 == BigInt(0) } yield x + y + z
futureResult andThen { case Success(value) ⇒ println(s"Success: $value")
case Failure(ex) ⇒ println(s"Failure: ${ex.getMessage}") }
You can run this program by typing:
$ sbt "runMain multi.futures.ForCompParallel"
Output is the same as was produced by the previous examples.
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.