26.3 Higher-Order Functions on Futures
By using a callback to assemble and send the page, you avoid blocking, and thus eliminate the risk of deadlock. However, because the function argument of onComplete is an action, which is executed for its side effects, some of the earlier functional flavor is lost. Recall that in the full version of the server in Listing 25.2, the assembled page is also used in a statistics-keeping function. If a value produced as a future is needed in multiple places, handling these uses with callbacks can get complicated. If the value is used asynchronously, you may even need callbacks within callbacks, which are difficult to write and even more difficult to debug. A better solution would be to bring the non-blocking nature of callbacks into code that maintains a more functional style.
Before we revisit the ad-fetching example in Section 26.5, consider this callback-based function:
Scala
def multiplyAndWrite(futureString: Future[String], count: Int): Unit = futureString.onComplete { case Success(str) => write(str * count) case Failure(e) => write(s"exception: $ {e.getMessage}") }
Somewhere, an input string is being produced asynchronously. It is passed to function multiplyAndWrite as a future. This function uses a callback to repeat the string multiple times and to write the result—in Scala, "A" * 3 is "AAA". This approach requires that you specify in the callback action everything you want to do with the string str * count, which does not exist outside this callback. This is a source of possible complexity and loss of modularity. It could be more advantageous to replace multiplyAndWrite with a multiply function that somehow returns the string str * count and makes it available to any code that needs it.
Within this multiply function, however, the string to multiply may not yet be available—its computation could still be ongoing. You also cannot wait for it because you want to avoid blocking. Instead, you need to return the multiplied string itself as a future. Accordingly, the return type of multiply is not String, but rather Future[String]. The future to be returned can be created using a promise, as in Listing 25.4:
Scala
def multiply(futureString: Future[String], count: Int): Future[String] = val promise = Promise[String]() futureString.onComplete { case Success(str) => promise.success(str * count) case Failure(e) => promise.failure(e) } promise.future
You create a promise to hold the multiplied string and a callback action to fulfill the promise. If the future futureString produces a string, the callback multiplies it and fulfills the promise successfully. Otherwise, the promise is failed, since no string was available for multiplication.
Now comes the interesting part. Conceptually, the preceding code has little to do with strings and multiplication. What it really does is transform the value produced by a future so as to create a new future. Of course, we have seen this pattern before—for instance, to apply a function to the contents of an option—in the form of the higher-order function map. Instead of focusing on the special case of string multiplication, you could write a generic map function on futures:
Scala
def map[A, B](future: Future[A], f: A => B): Future[B] = val promise = Promise[B]() future.onComplete { case Success(value) => promise.complete(Try(f(value))) case Failure(e) => promise.failure(e) } promise.future
This function is defined for generic types A and B instead of strings. The only meaningful difference with function multiply is that f might fail and is invoked inside Try. Consequently, the promise could be failed for one of two reasons: No value of type A is produced on which to apply f, or the invocation of function f itself fails.
The beauty of bringing up map is that it takes us to a familiar world, that of higher-order functions, as discussed in Chapters 9 and 10 and throughout Part I. Indeed, the Try type itself has a method map, which you can use to simplify the implementation of map on futures:
Scala
Listing 26.4: Reimplementing higher-order function map on futures.
def map[A, B](future: Future[A], f: A => B): Future[B] = val promise = Promise[B]() future.onComplete(tryValue => promise.complete(tryValue.map(f))) promise.future
Note how error cases are handled transparently, thanks to the Try type (see Section 13.3 for the behavior of map on Try).
Functions as values and higher-order functions constitute a fundamental aspect of functional programming. Chapter 25 introduced futures as a way to start making concurrent programming more functional by relying on a mechanism adapted to value-producing tasks. Once you choose to be functional, you should not be surprised to see higher-order patterns begin to emerge. But the story doesn’t end with map. Various higher-order functions on futures give rise to a powerful concurrent programming style that is both functional and non-blocking.
You don’t need to reimplement map on futures: Scala futures already have a map method. You can simply write function multiply as follows:
Scala
def multiply(futureString: Future[String], count: Int): Future[String] = futureString.map(str => str * count)
A thread that calls multiply does not block to wait for the input string to become available. Nor does it create any new string itself. It only makes sure that the input string will be multiplied once it is ready, typically by a worker from a thread pool.