Chaining futures in Scala
Suppose I want to make coffee. This involves 4 steps:
- 1a. grind coffee beans
- 1b. heat water
- combine
- filter
All these steps take time, so they return a Future. This is our domain:
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global //We need an executionContext to run futures
import scala.concurrent.duration._ //This provides the "1 second" syntax
class CoffeeBeans()
class GroundCoffee()
class ColdWater()
class WarmWater()
class UnfilteredCoffee()
class FilteredCoffee()
//we start out with beans and cold water
val beans = new CoffeeBeans()
val water = new ColdWater()
def grindBeans(beans: CoffeeBeans) = Future { new GroundCoffee() }
def heatWater(water: ColdWater) = Future { new WarmWater() }
def combine(groundCoffee: GroundCoffee, water: WarmWater) = Future { new UnfilteredCoffee() }
def filter(unfilteredCoffee: UnfilteredCoffee) = Future { new FilteredCoffee() }
One way we could chain the futures together is with onSuccess:
val fGrind: Future[GroundCoffee] = grindBeans(beans)
val fHeat: Future[WarmWater] = heatWater(water)
val fStep1: Future[(GroundCoffee, WarmWater)] = fGrind.zip(fHeat)
fStep1.onSuccess { case (groundCoffee, warmWater) =>
val fCombine: Future[UnfilteredCoffee] = combine(groundCoffee, warmWater)
fCombine.onSuccess { case unfilteredCoffee =>
val fFilter: Future[FilteredCoffee] = filter(unfilteredCoffee)
fFilter.onSuccess { case successCoffee =>
println(s"$successCoffee is ready!")
}
}
}
Thread.sleep(1000)//wait for the coffee to be ready
Using flatMap we can simplify this to:
val fGrind: Future[GroundCoffee] = grindBeans(beans)
val fHeat: Future[WarmWater] = heatWater(water)
val fStep1: Future[(GroundCoffee, WarmWater)] = fGrind.zip(fHeat)
val fCombine: Future[UnfilteredCoffee] = fStep1.flatMap {
case (groundCoffee, warmWater) => combine(groundCoffee, warmWater)
}
val fFilter: Future[FilteredCoffee] = fCombine.flatMap {
case unfilteredCoffee => filter(unfilteredCoffee)
}
val flatmapCoffee: FilteredCoffee = Await.result(fFilter, 1 second)
Notice that we have the future of our filtered coffee at the highest level. This is nice! We can return it, or call Await.Result on it to wait for our coffee to be ready. Since a for comprehension is a shorter way of writing flatMaps, we can simplify it to this:
val fFor = for {
groundCoffee <- grindBeans(beans)
warmWater <- heatWater(water)
unfilteredCoffee <- combine(groundCoffee, warmWater)
} yield filter(unfilteredCoffee)
val forCoffee: FilteredCoffee = Await.result(fFor, 1 second)
cheers!