Future Type Operation with Scala

Reading Time: 6 minutes

Introduction

Scala 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.

Type Future Operations

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.

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.

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.

Zip

In this section, we will demonstrate how you can use Future.zip to combine the results of two future operations into a single tuple. As per the Scala API documentation, the Future.zip will create a new future whose return type will be a tuple holding the return types of the two futures.

1. Define a method which returns a Future Option

Let’s start with our familiar donutStock() method which returns a Future Option of type Int.


def donutStock(donut: String): Future[Option[Int]] = Future {
  println("checking donut stock")
  if(donut == "vanilla donut") Some(10) else None
}

2. Define a method which returns a Future Double for donut price

For the purpose of this example, let’s create another method donutPrice(). It will return a Future of type double to represent the price of a donut.


println(s"\nStep 2: Define a method which returns a Future Double for donut price")
def donutPrice(): Future[Double] = Future.successful(3.25)

3. Zip the values of the first future with the second future

To combine the results of the two future operations, donutStock() and donutPrice(), into a single tuple, you can make use of the zip method as shown below. As a result, note that the return type of donutStockAndPriceOperation is Future[(Option[Int], Double)]. Option[Int] is the return type from donutStock(), Double is the return type from donutPrice(), and both types are enclosed inside a tuple.


println(s"\nStep 3: Zip the values of the first future with the second future")
val donutStockAndPriceOperation = donutStock("vanilla donut") zip donutPrice()
donutStockAndPriceOperation.onComplete {
  case Success(results) => println(s"Results $results")
  case Failure(e)       => println(s"Error processing future operations, error = ${e.getMessage}")
}

You should see the following output when you run your Scala application in IntelliJ:


Step 3: Zip the values of the first future with the second future
checking donut stock
Results (Some(10),3.25)

ZipWith

Similar to future zip() method, Scala also provides a handy future zipWith() method. In addition to combining the results of two futures, the zipWith() method allows you to pass-through a function which can be applied to the results.


1. Define a method which returns a Future Option

Let’s start with our donutStock() method which returns a Future Option of type Int.


println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Option[Int]] = Future {
  println("checking donut stock")
  if(donut == "vanilla donut") Some(10) else None
}

2. Define a method which returns a Future Double for donut price

Next, let’s create another method named donutPrice() which returns a Future of type Double representing the price of a particular donut.


println(s"\nStep 2: Define a method which returns a Future Double for donut price")
def donutPrice(): Future[Double] = Future.successful(3.25)

3. Define a value function to convert Tuple (Option[Int], Double) to Tuple (Int, Double)

when we zip donutStock() Future with donutPrice() future, the return type of the result was a tuple of Option[Int] and Double. Option[Int] type represents the quantity from method donutStock() and Double type represents the price from donutPrice(). The qtyAndPriceF function below is a dummy example to map the Int value from an Option and we will pass this function to future zipWith().


println(s"\nStep 3: Define a value function to convert Tuple (Option[Int], Double) to Tuple (Int, Double)")
val qtyAndPriceF: (Option[Int], Double) => (Int, Double) = (someQty, price) => (someQty.getOrElse(0), price)

4. Call Future.zipWith and pass-through function qtyAndPriceF

By passing-through the function qtyAndPriceF from Step 3 to the zipWith() method, the type (Option[Int], Double) will be transformed into (Int, Double).


println(s"\nStep 4: Call Future.zipWith and pass-through function qtyAndPriceF")
val donutAndPriceOperation = donutStock("vanilla donut").zipWith(donutPrice())(qtyAndPriceF)
donutAndPriceOperation.onComplete {
  case Success(result) => println(s"Result $result")
  case Failure(e)      => println(s"Error processing future operations, error = ${e.getMessage}")
}

You should see the following output when you run your Scala application in IntelliJ:


Step 4: Call Future.zipWith and pass-through function qtyAndPriceF
checking donut stock
Result (10,3.25)

Error Handling

Future recover

In this section, we will show how to use the future recover() function to help you work around exceptions that may arise from a future operation. Typically, though, you would want to recover from known exceptions that your future may throw as opposed to trapping any random exception.

1. Define a method which returns a Future

As usual, let’s start by creating our donutStock() method which returns a Future of type Int. Within the body of our donutStock() method, we will throw an IllegalStateException for any donut which does not match the String vanilla donut.


println("Step 1: Define a method which returns a Future")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Int] = Future {
  if(donut == "vanilla donut") 10
  else throw new IllegalStateException("Out of stock")
}

2. Execute donutStock() future operation

In this step, we will call the donutStock() method and pass-through the String vanilla donut as input. We would expect this future to complete just fine since we only throw an exception if the input String is not vanilla donut.


println("\nStep 2: Execute donutStock() future operation")
donutStock("vanilla donut")
  .onComplete {
    case Success(donutStock)  => println(s"Results $donutStock")
    case Failure(e)           => println(s"Error processing future operations, error = ${e.getMessage}")
}

You should see the following output when you run your Scala application in IntelliJ:


Step 2: Execute donutStock() future operation
Results 10

3. Call Future.recover to recover from a known exception

In this step, however, we will pass-through the input String of unknown donut as parameter to the donutStock() method. As a result, we know based on the implementation of the donutStock() method that this input String will throw an exception. To this end, we can use the recover() function to help us continue the flow of our program. In our example below, we simply return an Int value of 0 donut stock.

Note also that we were explicitly recovering for only IllegalStateException. For a more general case, you could catch NonFatal exception using: case NonFatal(e) => 0


println("\nStep 3: Call Future.recover to recover from a known exception")
donutStock("unknown donut")
  .recover { case e: IllegalStateException if e.getMessage == "Out of stock" => 0 }
  .onComplete {
    case Success(donutStock)  => println(s"Results $donutStock")
    case Failure(e)           => println(s"Error processing future operations, error = ${e.getMessage}")
}

You should see the following output when you run your Scala application in IntelliJ:


Step 3: Call Future.recover to recover from a known exception
Results 0

Future recoverWith

In the previous section, we introduced future recover() method. Similarly, Scala provides a future recoverWith() method but it requires a return type of Future. You can visually notice the difference between recover and recoverWith by comparing their method signatures as per the official Scala API documentation.

recover:

def recover[U >: T](pf: PartialFunction[Throwable, U])

recoverWith:

def recoverWith[U >: T](pf: PartialFunction[Throwable, Future[U]])

1. Define a method which returns a Scala Future

Let’s get started by creating our familiar donutStock() method which returns a Future of type Int. Note that we will throw an IllegalStateException() for any inputs that are not vanilla donut.


println("Step 1: Define a method which returns a Future")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Int] = Future {
  if(donut == "vanilla donut") 10
  else throw new IllegalStateException("Out of stock")
}

2. Execute donutStock() Scala Future operation

In this step, we will pass-through the input parameter of vanilla donut and we should expect the future onComplete() callback to run successfully.


println("\nStep 2: Execute donutStock() future operation")
donutStock("vanilla donut")
  .onComplete {
    case Success(donutStock)  => println(s"Results $donutStock")
    case Failure(e)           => println(s"Error processing future operations, error = ${e.getMessage}")
}

You should see the following output when you run your Scala application in IntelliJ:


Step 2: Execute donutStock() future operation
Results 10

3. Call Future.recoverWith to recover from a known exception

In the code snippet below, we will pass-through an input of unknown donut to the donutStock() method and as such should expect an exception to be thrown. By using the recoverWith() method, we can continue the execution flow of our program. As always, try to avoid throwing exceptions around in the first place! But there may be times when say dealing with I/O that it makes sense to recover from some known exceptions.


println("\nStep 3: Call Future.recoverWith to recover from a known exception")
donutStock("unknown donut")
  .recoverWith { case e: IllegalStateException if e.getMessage == "Out of stock" => Future.successful(0) }
  .onComplete {
    case Success(donutStock)  => println(s"Results $donutStock")
    case Failure(e)           => println(s"Error processing future operations, error = ${e.getMessage}")
}

You should see the following output when you run your Scala application in IntelliJ:


Step 3: Call Future.recoverWith to recover from a known exception
Results 0

Conclusion

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

We saw how to start an asynchronous computation using Future and how to transform the result of future with their type operations.

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

knoldus

Written by 

Meenakshi Goyal is a Software Consultant and started her career in an environment and organization where her skills are challenged each day, resulting in ample learning and growth opportunities. Proficient in Scala, Akka, Akka HTTP , JAVA. Passionate about implementing and launching new projects. Ability to translate business requirements into technical solutions. Her hobbies are traveling and dancing.