Mapping the Future
Scala like most other programming languages provides constructs to temporally decouple processing using asynchrony. As with most of the programming languages and API, Scala asynchrony constructs are built around futures and promises. These are generally implemented as callback-based API, that makes client code clumsy, cumbersome and verbose.
Let us have a look at the following code snippet for a fictitious Pizza ordering system,
type Ingredients = List[String] case class Pizza(i: Ingredients) case class Receipt(p: Pizza, time: Long) def fetchIngredients() = Future { List("Thin Crust", "Mozzarella", "Tomato Sauce") } def makePizza(i: Ingredients) = Future { new Pizza(i) } def deliverPizza(p: Pizza) = Future { Receipt(p, System.currentTimeMillis) }
In the sequential world, the sequence of events could be
- Take the order
- Fetch the ingredients
- Make the Pizza
- Deliver the Pizza
- Store the receipt for future reference
Let us assume, steps 2, 3 and 4 are processor intensive and will take an indefinite period of time, so we don’t want step 1, to hold up processing, rather it should be able to continue taking orders, while the subsequent steps are being processed from each order that was taken previously, in parallel.
In the Scala world, you can achieve steps 2 to 4 using futures. However, at some point when the delivery is receipted we would want to store it for future reference. This of course can be queued up on a slow lane. One obvious way to interact with the future based API is to use callbacks, as shown below,
fetchIngredients onSuccess { case i => makePizza(i) onSuccess { case p => deliverPizza(p) onSuccess { case r => // Store receipt } } }
Scala future API provides success callbacks using onSuccess and failure callbacks using onFailure or combining both using onComplete. As you can see, before you know the calls get deeply nested and access to results from callbacks deeply nested inside get extremely cumbersome.
However, the future API provides higher order combinator functions that allow you to map, flatMap and filter over the future. The code snippet below shows how to flatMap over the future to get the receipt of delivering the Pizza,
val r = fetchIngredients flatMap(i => makePizza(i) flatMap(p => deliverPizza(p)))
Since Future is a container type with combinatorial operations, you can also use it more idiomatically using for comprehensions as shown below,
val r = for { i <- fetchIngredients p <- makePizza(i) r <- deliverPizza(p) } yield r
Alhirgt alright alright that’s exactly what I needed!