26.4 Function flatMap on Futures
In function multiply, the string argument is given asynchronously, as a future, but the count is already known at call time and is passed as an integer. In a more general variant, the multiplying count itself could be the result of an asynchronous computation and be passed to multiply as a future. In that case, you need to combine futureString, of type Future[String], and futureCount, of type Future[Int], into a Future[String]. The expression
futureString.map(str => futureCount.map(count => str * count))
would have type Future[Future[String]], which is not what you want. Of course, we have had this discussion before (with options, in Section 10.3) and used it to introduce the fundamental operation flatMap. Scala futures also have a flatMap method:
Scala
def multiply(futureString: Future[String], futureCount: Future[Int]): Future[String] = futureString.flatMap(str => futureCount.map(count => str * count))
There is nothing blocking in this function. Once both futures—futureString and futureCount—are completed, the new string will be created.
You can use other functions to achieve the same purpose. For instance, you can combine the two futures into a Future[(String,Int)] using zip, then use map to transform the pair:
Scala
def multiply(futureString: Future[String], futureCount: Future[Int]): Future[String] = futureString.zip(futureCount).map((str, count) => str * count)
You can even use zipWith, which combines zip and map into a single method:
Scala
def multiply(futureString: Future[String], futureCount: Future[Int]): Future[String] = futureString.zipWith(futureCount)((str, count) => str * count)
The last two functions may be easier to read than the flatMap/map variant. Nevertheless, you should keep in mind the fundamental nature of flatMap. Indeed, zip and zipWith can be implemented using flatMap.
An experienced Scala programmer might write multiply as follows:
Scala
def multiply(futureString: Future[String], futureCount: Future[Int]): Future[String] = for str <- futureString; count <- futureCount yield str * count
Recall from Section 10.9 that for-yield in Scala is implemented as a combination of map and flatMap (and withFilter). This code would be transformed by the compiler into the earlier flatMap/map version. The for-yield syntax is very nice, especially when working with futures. I encourage you to use it if you are programming in Scala. However, as mentioned in an earlier note, I will continue to favor the explicit use of map and flatMap in this book’s examples for pedagogical reasons.
By combining two futures into one—using flatMap, zip, or zipWith—you can rewrite the parallel quick-sort example of Listing 26.1 as a non-blocking function:
Scala
Listing 26.5: Non-blocking implementation of parallel quick-sort.
def quickSort(list: List[Int])(using ExecutionContext): Future[List[Int]] = list match case Nil => Future.successful(list) case pivot :: others => val (low, high) = others.partition(_ < pivot) val lowFuture = Future.delegate(quickSort(low)) val highFuture = quickSort(high) lowFuture.flatMap(lowSorted => highFuture.map(highSorted => lowSorted ::: pivot :: highSorted) )
To avoid blocking, the return type of the function is changed from List[Int] to Future[List[Int]]. As before, the task of sorting the low values is delegated to the thread pool. You could equivalently write it as lowFuture = Future(quickSort(low)) .flatten. A direct recursive call is used to sort the high values, and the two futures are combined, following the same pattern as in function multiply. This variant of quick-sort involves no blocking and, in contrast to Listing 26.1, cannot possibly deadlock.1