The Neophyte's Guide to Scala Part 8: Welcome to the Future

As an aspiring and enthusiastic Scala developer, you will likely have heard of Scala’s approach at dealing with concurrency – or maybe that was even what attracted you in the first place. Said approach makes reasoning about concurrency and writing well-behaved concurrent programs a lot easier than the rather low-level concurrency APIs you are confronted with in most other languages.

One of the two cornerstones of this approach is the Future, the other being the Actor. The former shall be the subject of this article. I will explain what futures are good for and how you can make use of them in a functional way.

Please make sure that you have version 2.9.3 or later if you want to get your hands dirty and try out the examples yourself. The futures we are discussing here were only incorporated into the Scala core distribution with the 2.10.0 release and later backported to Scala 2.9.3. Originally, with a slightly different API, they were part of the Akka concurrency toolkit.

Why sequential code can be bad

Suppose you want to prepare a cappuccino. You could simply execute the following steps, one after another:

  • Grind the required coffee beans
  • Heat some water
  • Brew an espresso using the ground coffee and the heated water
  • Froth some milk
  • Combine the espresso and the frothed milk to a cappuccinoTranslated to Scala code, you would do something like this:
  1. import scala.util.Try
  2. // Some type aliases, just for getting more meaningful method signatures:
  3. type CoffeeBeans = String
  4. type GroundCoffee = String
  5. case class Water(temperature: Int)
  6. type Milk = String
  7. type FrothedMilk = String
  8. type Espresso = String
  9. type Cappuccino = String
  10. // dummy implementations of the individual steps:
  11. def grind(beans: CoffeeBeans): GroundCoffee = s"ground coffee of $beans"
  12. def heatWater(water: Water): Water = water.copy(temperature = 85)
  13. def frothMilk(milk: Milk): FrothedMilk = s"frothed $milk"
  14. def brew(coffee: GroundCoffee, heatedWater: Water): Espresso = "espresso"
  15. def combine(espresso: Espresso, frothedMilk: FrothedMilk): Cappuccino = "cappuccino"
  16. // some exceptions for things that might go wrong in the individual steps
  17. // (we'll need some of them later, use the others when experimenting
  18. // with the code):
  19. case class GrindingException(msg: String) extends Exception(msg)
  20. case class FrothingException(msg: String) extends Exception(msg)
  21. case class WaterBoilingException(msg: String) extends Exception(msg)
  22. case class BrewingException(msg: String) extends Exception(msg)
  23. // going through these steps sequentially:
  24. def prepareCappuccino(): Try[Cappuccino] = for {
  25. ground <- Try(grind("arabica beans"))
  26. water <- Try(heatWater(Water(25)))
  27. espresso <- Try(brew(ground, water))
  28. foam <- Try(frothMilk("milk"))
  29. } yield combine(espresso, foam)

Doing it like this has several advantages: You get a very readable step-by-step instruction of what to do. Moreover, you will likely not get confused while preparing the cappuccino this way, since you are avoiding context switches.

On the downside, preparing your cappuccino in such a step-by-step manner means that your brain and body are on wait during large parts of the whole process. While waiting for the ground coffee, you are effectively blocked. Only when that’s finished, you’re able to start heating some water, and so on.

This is clearly a waste of valuable resources. It’s very likely that you would want to initiate multiple steps and have them execute concurrently. Once you see that the water and the ground coffee is ready, you’d start brewing the espresso, in the meantime already starting the process of frothing the milk.

It’s really no different when writing a piece of software. A web server only has so many threads for processing requests and creating appropriate responses. You don’t want to block these valuable threads by waiting for the results of a database query or a call to another HTTP service. Instead, you want an asynchronous programming model and non-blocking IO, so that, while the processing of one request is waiting for the response from a database, the web server thread handling that request can serve the needs of some other request instead of idling along.

“I heard you like callbacks, so I put a callback in your callback!”

Of course, you already knew all that - what with Node.js being all the rage among the cool kids for a while now. The approach used by Node.js and some others is to communicate via callbacks, exclusively. Unfortunately, this can very easily lead to a convoluted mess of callbacks within callbacks within callbacks, making your code hard to read and debug.

Scala’s Future allows callbacks, too, as you will see very shortly, but it provides much better alternatives, so it’s likely you won’t need them a lot.

“I know Futures, and they are completely useless!”

You might also be familiar with other Future implementations, most notably the one provided by Java. There is not really much you can do with a Java future other than checking if it’s completed or simply blocking until it is completed. In short, they are nearly useless and definitely not a joy to work with.

If you think that Scala’s futures are anything like that, get ready for a surprise. Here we go!

Semantics of Future

Scala’s Future[T], residing in the scala.concurrent package, is a container type, representing a computation that is supposed to eventually result in a value of type T. Alas, the computation might go wrong or time out, so when the future is completed, it may not have been successful after all, in which case it contains an exception instead.

Future is a write-once container – after a future has been completed, it is effectively immutable. Also, the Future type only provides an interface for reading the value to be computed. The task of writing the computed value is achieved via a Promise. Hence, there is a clear separation of concerns in the API design. In this post, we are focussing on the former, postponing the use of the Promise type to the next article in this series.

Working with Futures

There are several ways you can work with Scala futures, which we are going to examine by rewriting our cappuccino example to make use of the Future type. First, we need to rewrite all of the functions that can be executed concurrently so that they immediately return a Future instead of computing their result in a blocking way:

  1. import scala.concurrent.future
  2. import scala.concurrent.Future
  3. import scala.concurrent.ExecutionContext.Implicits.global
  4. import scala.concurrent.duration._
  5. import scala.util.Random
  6. def grind(beans: CoffeeBeans): Future[GroundCoffee] = Future {
  7. println("start grinding...")
  8. Thread.sleep(Random.nextInt(2000))
  9. if (beans == "baked beans") throw GrindingException("are you joking?")
  10. println("finished grinding...")
  11. s"ground coffee of $beans"
  12. }
  13. def heatWater(water: Water): Future[Water] = Future {
  14. println("heating the water now")
  15. Thread.sleep(Random.nextInt(2000))
  16. println("hot, it's hot!")
  17. water.copy(temperature = 85)
  18. }
  19. def frothMilk(milk: Milk): Future[FrothedMilk] = Future {
  20. println("milk frothing system engaged!")
  21. Thread.sleep(Random.nextInt(2000))
  22. println("shutting down milk frothing system")
  23. s"frothed $milk"
  24. }
  25. def brew(coffee: GroundCoffee, heatedWater: Water): Future[Espresso] = Future {
  26. println("happy brewing :)")
  27. Thread.sleep(Random.nextInt(2000))
  28. println("it's brewed!")
  29. "espresso"
  30. }

There are several things that require an explanation here.

First off, there is the apply method on the Future companion object, that requires two arguments:

  1. object Future {
  2. def apply[T](body: => T)(implicit execctx: ExecutionContext): Future[T]
  3. }

The computation to be computed asynchronously is passed in as the body by-name parameter. The second argument, in its own argument list, is an implicit one, which means we don’t have to specify one if a matching implicit value is defined somewhere in scope. We make sure this is the case by importing the global execution context.

An ExecutionContext is something that can execute our future, and you can think of it as something like a thread pool. Since the ExecutionContext is available implicitly, we only have a single one-element argument list remaining. Single-argument lists can be enclosed with curly braces instead of parentheses. People often make use of this when calling the future method, making it look a little bit like we are using a feature of the language and not calling an ordinary method. The ExecutionContext is an implicit parameter for virtually all of the Future API.

Furthermore, of course, in this simple example, we don’t actually compute anything, which is why we are putting in some random sleep, simply for demonstration purposes. We also print to the console before and after our “computation” to make the non-deterministic and concurrent nature of our code clearer when trying it out.

The computation of the value to be returned by a Future will start at some non-deterministic time after that Future instance has been created, by some thread assigned to it by the ExecutionContext.

Callbacks

Sometimes, when things are simple, using a callback can be perfectly fine. Callbacks for futures are partial functions. You can pass a callback to the onSuccess method. It will only be called if the Future completes successfully, and if so, it receives the computed value as its input:

  1. grind("arabica beans").onSuccess { case ground =>
  2. println("okay, got my ground coffee")
  3. }

Similarly, you could register a failure callback with the onFailure method. Your callback will receive a Throwable, but it will only be called if the Future did not complete successfully.

Usually, it’s better to combine these two and register a completion callback that will handle both cases. The input parameter for that callback is a Try:

  1. import scala.util.{Success, Failure}
  2. grind("baked beans").onComplete {
  3. case Success(ground) => println(s"got my $ground")
  4. case Failure(ex) => println("This grinder needs a replacement, seriously!")
  5. }

Since we are passing in baked beans, an exception occurs in the grind method, leading to the Future completing with a Failure.

Composing futures

Using callbacks can be quite painful if you have to start nesting callbacks. Thankfully, you don’t have to do that! The real power of the Scala futures is that they are composable.

If you have followed this series, you will have noticed that all the container types we discussed made it possible for you to map them, flat map them, or use them in for comprehensions and that I mentioned that Future is a container type, too. Hence, the fact that Scala’s Future type allows you to do all that will not come as a surprise at all.

The real question is: What does it really mean to perform these operations on something that hasn’t even finished computing yet?

Mapping the future

Haven’t you always wanted to be a traveller in time who sets out to map the future? As a Scala developer you can do exactly that! Suppose that once your water has heated you want to check if its temperature is okay. You can do so by mapping your Future[Water] to a Future[Boolean]:

  1. val temperatureOkay: Future[Boolean] = heatWater(Water(25)).map { water =>
  2. println("we're in the future!")
  3. (80 to 85).contains(water.temperature)
  4. }

The Future[Boolean] assigned to temperatureOkay will eventually contain the successfully computed boolean value. Go change the implementation of heatWater so that it throws an exception (maybe because your water heater explodes or something) and watch how we're in the future will never be printed to the console.

When you are writing the function you pass to map, you’re in the future, or rather in a possible future. That mapping function gets executed as soon as your Future[Water] instance has completed successfully. However, the timeline in which that happens might not be the one you live in. If your instance of Future[Water] fails, what’s taking place in the function you passed to map will never happen. Instead, the result of calling map will be a Future[Boolean] containing a Failure.

Keeping the future flat

If the computation of one Future depends on the result of another, you’ll likely want to resort to flatMap to avoid a deeply nested structure of futures.

For example, let’s assume that the process of actually measuring the temperature takes a while, so you want to determine whether the temperature is okay asynchronously, too. You have a function that takes an instance of Water and returns a Future[Boolean]:

  1. def temperatureOkay(water: Water): Future[Boolean] = Future {
  2. (80 to 85).contains(water.temperature)
  3. }

Use flatMap instead of map in order to get a Future[Boolean] instead of a Future[Future[Boolean]]:

  1. val nestedFuture: Future[Future[Boolean]] = heatWater(Water(25)).map {
  2. water => temperatureOkay(water)
  3. }
  4. val flatFuture: Future[Boolean] = heatWater(Water(25)).flatMap {
  5. water => temperatureOkay(water)
  6. }

Again, the mapping function is only executed after (and if) the Future[Water] instance has been completed successfully, hopefully with an acceptable temperature.

For comprehensions

Instead of calling flatMap, you’ll usually want to write a for comprehension, which is essentially the same, but reads a lot clearer. Our example above could be rewritten like this:

  1. val acceptable: Future[Boolean] = for {
  2. heatedWater <- heatWater(Water(25))
  3. okay <- temperatureOkay(heatedWater)
  4. } yield okay

If you have multiple computations that can be computed in parallel, you need to take care that you already create the corresponding Future instances outside of the for comprehension.

  1. def prepareCappuccinoSequentially(): Future[Cappuccino] = {
  2. for {
  3. ground <- grind("arabica beans")
  4. water <- heatWater(Water(20))
  5. foam <- frothMilk("milk")
  6. espresso <- brew(ground, water)
  7. } yield combine(espresso, foam)
  8. }

This reads nicely, but since a for comprehension is just another representation for nested flatMap calls, this means that the Future[Water] created in heatWater is only really instantiated after the Future[GroundCoffee] has completed successfully. You can check this by watching the sequential console output coming from the functions we implemented above.

Hence, make sure to instantiate all your independent futures before the for comprehension:

  1. def prepareCappuccino(): Future[Cappuccino] = {
  2. val groundCoffee = grind("arabica beans")
  3. val heatedWater = heatWater(Water(20))
  4. val frothedMilk = frothMilk("milk")
  5. for {
  6. ground <- groundCoffee
  7. water <- heatedWater
  8. foam <- frothedMilk
  9. espresso <- brew(ground, water)
  10. } yield combine(espresso, foam)
  11. }

Now, the three futures we create before the for comprehension start being completed immediately and execute concurrently. If you watch the console output, you will see that it’s non-deterministic. The only thing that’s certain is that the "happy brewing" output will come last. Since the method in which it is called requires the values coming from two other futures, it is only created inside our for comprehension, i.e. after those futures have completed successfully.

Failure projections

You will have noticed that Future[T] is success-biased, allowing you to use map, flatMap, filter etc. under the assumption that it will complete successfully. Sometimes, you may want to be able to work in this nice functional way for the timeline in which things go wrong. By calling the failed method on an instance of Future[T], you get a failure projection of it, which is a Future[Throwable]. Now you can map that Future[Throwable], for example, and your mapping function will only be executed if the original Future[T] has completed with a failure.

Outlook

You have seen the Future, and it looks bright! The fact that it’s just another container type that can be composed and used in a functional way makes working with it very pleasant.

Making blocking code concurrent can be pretty easy by wrapping it in a call to future. However, it’s better to be non-blocking in the first place. To achieve this, one has to make a Promise to complete a Future. This and using the futures in practice will be the topic of the next part of this series.

Posted by Daniel Westheide