1. Overview

Some operations like a database query or a call to another HTTP service can take a while to complete. Running them on the main thread would block further program execution and decrease performance.

In this tutorial, we’ll focus on Future, which is a Scala approach to running operations in the background and a solution to this problem.

2. Future

Future represents a result of an asynchronous computation that may or may not be available yet.

When we create a new Future, Scala spawns a new thread and executes its code. Once the execution is finished, the result of the computation (value or exception) will be assigned to the Future.

3. Create Future

3.1. ExecutionContext

Before we create any Future, we need to provide an implicit ExecutionContext. This specifies how and on which thread pool the Future code will execute. We can create it from Executor or ExecutorService:

val forkJoinPool: ExecutorService = new ForkJoinPool(4)
implicit val forkJoinExecutionContext: ExecutionContext = 
  ExecutionContext.fromExecutorService(forkJoinPool)

val singleThread: Executor = Executors.newSingleThreadExecutor()
implicit val singleThreadExecutionContext: ExecutionContext = 
  ExecutionContext.fromExecutor(singleThread)

There is also a global built-in ExecutionContext, which uses ForkJoinPool with its parallelism level set to the number of available processors:

implicit val globalExecutionContext: ExecutionContext = ExecutionContext.global

In the following sections, we’ll use ExecutionContext.global, making it available using a single import:

import scala.concurrent.ExecutionContext.Implicits.global

3.2. Schedule Future

Now that we have an ExecutionContext, it’s time to create a Future that will run a long-running operation in the background. We’ll simulate this with Thread.sleep:

def generateMagicNumber(): Int = {
  Thread.sleep(3000L)
  23
}
val generatedMagicNumberF: Future[Int] = Future {
  generateMagicNumber()
}

When we call Future.apply with the call to generateMagicNumber inside, the Future runtime executes it on another thread*.* It might look like we were passing the result of invoking the method to the Future, however, Future.apply takes it as a by-name parameter. It moves evaluation into a thread provided by implicit ExecutionContext.

3.3. Computed Future

When we have an already computed value, there’s no need to start an asynchronous computation in order to get a Future. We can create a successfully completed Future:

def multiply(multiplier: Int): Future[Int] =
  if (multiplier == 0) {
    Future.successful(0)
  } else {
    Future(multiplier * generateMagicNumber())
  }

Or one completed with a failure:

def divide(divider: Int): Future[Int] =
  if (divider == 0) {
    Future.failed(new IllegalArgumentException("Don't divide by zero"))
  } else {
    Future(generateMagicNumber() / divider)
  }

To simplify this conditional behavior, we can use the Future.fromTry() function:

def tryDivide(divider: Int): Future[Int] = Future.fromTry(Try {
  generateMagicNumber() / divider
})

When the expression passed to the Try {} block throws an exception, the Future.fromTry() is equivalent to the Future.failed() function. Otherwise, it’ll be somewhat similar to the Future.successful() function.

4. Await for Future

We’ve already created a Future, so we need a way to wait for its result:

val maxWaitTime: FiniteDuration = Duration(5, TimeUnit.SECONDS)
val magicNumber: Int = Await.result(generatedMagicNumberF, maxWaitTime)

Await.result blocks the main thread and waits a defined duration for the result of the given Future. If it’s not ready after that time or complete with a failure, Await.result throws an exception*.*

In this example, we’re waiting a maximum of 5 seconds for the generatedMagicNumberF result.

If we wanted to wait forever, we should use Duration.Inf.

Because Await.result blocks the main thread, we should only use it when we really need to wait. If we want to transform the Future result or combine it with others, then we can do that in non-blocking ways, which we’ll look at later on.

5. Callbacks

5.1. onComplete

Instead of waiting for the Future result and blocking the main thread, we can register a callback using the onComplete method:

def printResult[A](result: Try[A]): Unit = result match {
  case Failure(exception) => println("Failed with: " + exception.getMessage)
  case Success(number)    => println("Succeed with: " + number)
}
magicNumberF.onComplete(printResult)

In this case, the Future executes the printResult method when the magicNumberF completes, either successfully or with a failure.

5.2. foreach

If we want to call a callback only when the Future completes successfully, we should use the foreach method:

def printSucceedResult[A](result: A): Unit = println("Succeed with: " + result)
magicNumberF.foreach(printSucceedResult)

Unlike the onComplete method, this won’t execute the given callback function when magicNumberF completes with failure.

It has the same meaning as the foreach method on Try or Option.

6. Error Handling

6.1. failed

If we have a use-case that depends on knowing if a given Future failed with the appropriate exception, we want to treat failure as success. For this purpose, we have the failed method:

val failedF: Future[Int] = Future.failed(new IllegalArgumentException("Boom!"))
val failureF: Future[Throwable] = failedF.failed

It tries to transform a failed Future into a successfully completed one with Throwable as a result. If Future completes successfully, then the resulting Future will fail with NoSuchElementException.

6.2. fallbackTo

Let’s imagine that we have a DatabaseRepository that is able to read a magic number from the database and FileBackup that reads a magic number from the latest backup file (created once per day):

trait DatabaseRepository {
  def readMagicNumber(): Future[Int]
  def updateMagicNumber(number: Int): Future[Boolean]
}
trait FileBackup {
  def readMagicNumberFromLatestBackup(): Future[Int]
}

Because this number is crucial for our business, we want to read it from the latest backup in case of any problem with the database. In this situation, we should use the fallbackTo method:

trait MagicNumberService {
  val repository: DatabaseRepository
  val backup: FileBackup

  val magicNumberF: Future[Int] =
    repository.readMagicNumber()
      .fallbackTo(backup.readMagicNumberFromLatestBackup())
}

It takes an alternative Future in case of a failure of the current one and evaluates them simultaneously. If both fail, the resulting Future will fail with the Throwable taken from the current one*.*

6.3. recover

If we want to handle a particular exception by providing an alternative value, we should use the recover method:

val recoveredF: Future[Int] = Future(3 / 0).recover {
  case _: ArithmeticException => 0
}

It takes a partial function that turns any matching exception into a successful result. Otherwise, it will keep the original exception.

6.4. recoverWith

If we want to handle a particular exception with another Future, we should use recoverWith instead of recover:

val recoveredWithF: Future[Int] = Future(3 / 0).recoverWith {
  case _: ArithmeticException => magicNumberF
}

7. Transform Future

7.1. map

When we have a Future instance, we can use the map method to transform its successful result without blocking the main thread:

def increment(number: Int): Int = number + 1
val nextMagicNumberF: Future[Int] = magicNumberF.map(increment)

It creates a new Future[Int] by applying the increment method to the successful result of magicNumberF. Otherwise, the new one will contain the same exception as magicNumberF.

Evaluation of the increment method happens on another thread taken from the implicit ExecutionContext.

7.2. flatMap

If we want to transform a Future using a function that returns Future, then we should use the flatMap method:

val updatedMagicNumberF: Future[Boolean] =
  nextMagicNumberF.flatMap(repository.updateMagicNumber)

It behaves in the same way as the map method but keeps the resulting Future flat, returning Future[Boolean] instead of Future[Future[Boolean]].

Having the flatMap and map methods gives us the ability to write code that’s easier to understand.

7.3. transform

As opposed to the map() function, we can map both successful and failed cases with the transform(f: Try[T] => Try[S]) function:

val value = Future.successful(42)
val transformed = value.transform {
  case Success(value) => Success(s"Successfully computed the $value")
  case Failure(cause) => Failure(new IllegalStateException(cause))
}

As shown above, the transform() method accepts a function that takes the Future result as the input and returns a Try instance as the output. In this case, we’re converting the given Future[Int] to a Future[String].

This method has an overloaded version that takes two functions as the input — one for a successful case, and the other for failure scenarios:

val overloaded = value.transform(
  value => s"Successfully computed $value", 
  cause => new IllegalStateException(cause)
)

The first function maps the successful result to something else, and the second one maps the thrown exception.

7.4. transformWith

Similar to the transform() function, the transformWith(f: Try[T] => Future[S]) accepts a function as the input. This function, however, converts the given Try instance directly to a Future instance:

value.transformWith {
  case Success(value) => Future.successful(s"Successfully computed the $value")
  case Failure(cause) => Future.failed(new IllegalStateException(cause))
}

As shown above, the return type of the input function is a Future instead of a Try.

7.5. andThen

The andThen() function applies a side-effecting function to the given Future and returns the same Future:

Future.successful(42).andThen {
  case Success(v) => println(s"The answer is $v")
}

As shown above, the andThen() consumes the successful result here. Since the andThen() function returns the same Future after applying the given function, we can chain multiple andThen() invocations together:

val f = Future.successful(42).andThen {
  case Success(v) => println(s"The answer is $v")
} andThen {
  case Success(_) => // send HTTP request to signal success
  case Failure(_) =>  // send HTTP request to signal failure
}
  
f.onComplete { v =>
  println(s"The original future has returned: ${v}")
}

As shown above, the variable f still contains the original Future after applying two side-effecting functions.

8. Combining Futures

8.1. zip

For combing results of two independent Futures into a pair, we should use the zip method:

val pairOfMagicNumbersF: Future[(Int, Int)] =
  repository.readMagicNumber()
    .zip(backup.readMagicNumberFromLatestBackup())

It tries to combine the successful results of both Futures into a pair. If any of them fail, the resulting Future will also fail with the same reason as the leftmost of them.

8.2. zipWith

If we want to combine the results of two independent Futures into something other than a pair, we should use the zipWith method:

def areEqual(x: Int, y: Int): Boolean = x == y
val areMagicNumbersEqualF: Future[Boolean] =
  repository.readMagicNumber()
    .zipWith(backup.readMagicNumberFromLatestBackup())(areEqual)

It uses the given function for combining successful results of both Futures. 

In our example, we use the areEqual method for zipping, which checks if both numbers (taken from database and backup) are equal.

8.3. traverse

Let’s imagine that we have a list of magic numbers and we want to publish each of them using Publisher:

val magicNumbers: List[Int] = List(1, 2, 3, 4)
trait Publisher {
  def publishMagicNumber(number: Int): Future[Boolean]
}

In this situation, we can use the Future.traverse method, which performs a parallel map of multiple elements:

val published: Future[List[Boolean]] =
  Future.traverse(magicNumbers)(publisher.publishMagicNumber)

It calls the publishMagicNumber method for each of the given magic numbers and combines them into a single Future. Each of those evaluations happens on a different thread taken from the ExecutionContext.

If any of them fail, the resulting Future will also fail.

9. Conclusion

In this article, we explored Scala’s Future API*.*

We saw how to start an asynchronous computation using Future and how to wait for its result. Then, we learned a few useful methods that transform and combine Future results without blocking the main thread.

Finally, we showed how to handle successful results and errors, as well as how to combine results.

As always, the full source code of the article is available over on GitHub.


» 下一篇: Scala 中的可变性