26.4 Function flatMap on Futures
In functionmultiply, 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 tomultiplyas a future. In that case, you need to combinefutureString, of typeFuture[String], andfutureCount, of typeFuture[Int], into aFuture[String]. The expression
futureString.map(str => futureCount.map(count => str * count))
would have typeFuture[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 operationflatMap. Scala futures also have aflatMapmethod:
Scala
defmultiply(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—futureStringandfutureCount—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 aFuture[(String,Int)]usingzip, then usemapto transform the pair:
Scala
defmultiply(futureString: Future[String], futureCount: Future[Int]): Future[String] = futureString.zip(futureCount).map((str, count) => str * count)
You can even usezipWith, which combineszipandmapinto a single method:
Scala
defmultiply(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 theflatMap/mapvariant. Nevertheless, you should keep in mind the fundamental nature offlatMap. Indeed,zipandzipWithcan be implemented usingflatMap.
An experienced Scala programmer might writemultiplyas follows:
Scala
defmultiply(futureString: Future[String], futureCount: Future[Int]): Future[String] =forstr <- futureString; count <- futureCountyieldstr *数
Recall from Section 10.9 thatfor-yieldin Scala is implemented as a combination ofmapandflatMap(andwithFilter). This code would be transformed by the compiler into the earlierflatMap/mapversion. Thefor-yieldsyntax 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 ofmapandflatMapin this book’s examples for pedagogical reasons.
By combining two futures into one—usingflatMap,zip, orzipWith—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.
defquickSort(list: List[Int])(usingExecutionContext): Future[List[Int]] = listmatch caseNil => Future.successful(list)casepivot :: others =>val(low, high) = others.partition(_ < pivot)vallowFuture = Future.delegate(quickSort(low))valhighFuture =快速排序lowFuture.flatMap(低(高)Sorted => highFuture.map(highSorted => lowSorted ::: pivot :: highSorted) )
To avoid blocking, the return type of the function is changed fromList[Int]toFuture[List[Int]]. As before, the task of sorting the low values is delegated to the thread pool. You could equivalently write it aslowFuture = 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 functionmultiply. This variant of quick-sort involves no blocking and, in contrast to Listing 26.1, cannot possibly deadlock.1