Learning Scala: Futures and For Comprehensions
If you’re just starting out with Scala, you’re probably a little confused with Futures, a little confused with the “for” keyword, and really confused about what happens when they come together. What follows is an explanation of Futures and for comprehensions, which (hopefully) clears up some of the unintuitive interactions between them.
Futures:Futures are Scala’s way of handling asynchronous programming. A Future is a wrapper; that is, a Future is of type Future[T], where T is some generic type. To create a Future, you can invoke the Future.apply() method and pass it a computation you want to run asynchronously, like so:
But how do you get the wrapped value out of the Future? You can do that using a callback; Futures have an onComplete method, usable like so:
The onComplete method passes a Try[T]; the Try can either look like Success(T) or Failure(Throwable). Using Scala’s pattern matching, you can succinctly handle both cases.
For Comprehensions:In Scala, for comprehensions aren’t for loops in an imperative sense; they’re syntactic sugar, shorthand for a more cumbersome way of writing things. For example, the top and bottom statements here are equivalent:
But what does this have to do with Futures? Well, Futures have concrete value members map, flatMap, and withFilter – so For comprehensions involving Futures desugar properly. Let’s take a look at what that looks like:
Again, these two statements are equivalent. Do note that the map and flatMap functions return a Future based on the result of the Future you apply the map to – so the new Product gets wrapped in a Future, and you can’t do something like this:
It’s easy to see why this is the case if we think about the types: Futures can only map to Futures, but most other maps don’t result in Futures – our name is a Future, but the final result of our map chain is a Product, which invalidates our Future map.
Generally, if you use a for to map on a Collection, you need to retain that Collection type. If you need to bind a non-Future inside a for, you can wrap your non-Future inside of a Future using Future.successful. Future.successful wraps a value inside of a Future that’s already completed successfully, like so:
Future.successful has a counterpart, Future.failed that wraps a value inside of a failed Future; this is useful if, say, you wanted to fail the price Future when the price is negative. It might seem frustrating having to preserve Future typing, but remember that Futures are how we denote work to be done in the future – it’s not just a cumbersome wrapper around the value you want.
Maintaining Collection types doesn’t mean that you can’t mix Futures and Collections. You just have to be clever about how you construct your maps. Let’s look at an example:
Say we have a sequence of product ids, and want to build a sequence of products using the names and prices we get from asynchronous lookups. How would we do that?
Let’s start with our sequence of product ids, call it seqOfProductIds. We have a sequence, and we want another sequence with the same number of elements – that’s a strong indicator for map usage.
What now? Well, to make a product we know we need to do two asynchronous computations, so we’d have two Futures – and the end result, a product, would most likely be wrapped in a Future. It sounds like we could use a for comprehension.
Does that work? Let’s see, our map returns a Sequence, and our for returns a Future – so now we’ve got a Seq[Future[Product]]. We could stop here, but then we’d have to define an onComplete for each Future in our sequence. Fortunately, there’s a way around this: Future.sequence. Future.sequence just turns a Seq[Future[T]] into a Future[Seq[T]]; we can use it like this:
And there you have it: An overview of Futures and for comprehensions. Hope this helps, and happy hacking!