26.7 Summary
When used as a synchronizer, a future requires a thread to potentially block and wait to access the value being computed. Parking and unparking threads has a non-negligible performance cost. Worse yet, tasks that block to wait on other tasks running on the same group of threads can easily result in deadlocks. It is not always possible to avoid these deadlocks by increasing the number of threads in a pool, and larger pool sizes tend to cause inefficiencies even when it can be done.
Callbacks can be used as an alternative to blocking. They trigger a computation when a future is ready, without having to explicitly wait for this future to finish. Callbacks can be complex—future values can be used in arbitrary ways—and can lead to intricate code, especially when callbacks within callbacks are involved.
By defining non-blocking higher-order functions on futures, you can bring to the world of concurrent programming the same shift from actions to functions that is at the core of functional programming. Instead of using effect-based callbacks, future values are handled functionally, as when using functions, but asynchronously.
The resulting functional-concurrent programming style does not use futures as synchronizers—thereby avoiding many deadlock scenarios and performance costs associated with blocking—and also sidesteps the inherent complexity of callbacks.
Higher-order functions on futures can be used to transform values, combine multiple computations asynchronously, or recover from failures. The same higher-order functions that proved hugely beneficial to functional programming—particularly, map, flatMap, foreach, and filter—provide developers with tools to orchestrate complex concurrent computations according to patterns that maximize concurrency while avoiding blocking.
Functions flatMap and map, in particular, can be used to combine in a uniform way computations that may be synchronous or asynchronous, failed or successful. They can also be used to implement, without blocking threads, patterns that (conceptually) wait for tasks to finish, such as fork-join.
Adjusting to functional-concurrent programming requires a shift in program design, away from locks and synchronizers. This can initially require some effort, similar to casting aside assignments and loops when moving from imperative to functional programming. Once you become accustomed to it, though, this programming style is often easier and less error-prone than the alternatives.