1. Overview

The Future and Promise are two high-level asynchronous constructs in concurrent programming. In this tutorial, we’re going to learn more about them and the purpose of each in Scala programming language.

2. Future

The Future is a read-only placeholder for the result of ongoing computation. It acts as a proxy for an actual value that doesn’t yet exist. Think of IO-Bound or CPU-Bound operations, which take a notable time to complete.

2.1. Creating Futures

To create asynchronous computation, we can put our computation inside the apply function of the Future:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val result = Future {
  println("Long running computation started.")
  val result = {
    Thread.sleep(5000)
    5
  }
  println("Our computation, finally finished.")
  result
}

To run the future, we need an ExecutionContext, which allows us to separate our business logic (the code) from the execution environment. Because the ExecutionContext is an implicit parameter, we can import or create an ExectutionContext and mark it as implicit in our scope. Here for simplicity, we use the global execution context:

implicit val ec = scala.concurrent.ExecutionContext.Implicits.global
val r1 = Future{ /* computation */ }(ec) //Passing ec explicitly
val r2 = Future{ /* computation */ }     //Passing ec implicitly

2.2. Composing Futures

What is the benefit of using the Future? The Future has the map and flatMap operations, so we can chain our futures using idiomatic for-comprehensions. This gives us good composability in concurrent programming. 

In the User class below, the createUser() function depends on the result of other asynchronous functions like notExist() and avatar(). Let’s see how to compose these computations together:

import java.math.BigInteger
import java.net.URL
import java.security.MessageDigest

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

type Name = String
type Email = String
type Password = String
type Avatar = URL

case class User(name: Name, email: Email, password: Password, avatar: Avatar)

def notExist(email: Email): Future[Boolean] = Future {
  Thread.sleep(100)
  true
}
def md5hash(str: String): String =
  new BigInteger(1,
    MessageDigest
      .getInstance("MD5")
      .digest(str.getBytes)
  ).toString(16)
def avatar(email: Email): Future[Avatar] = Future {
  Thread.sleep(200)
  new Avatar("http://avatar.example.com/user/23k520f23f4.png")
}
def createUser(name: Name, email: Email, password: Password): Future[User] =
  for {
    _ <- notExist(email)
    avatar <- avatar(email)
    hashedPassword = md5hash(password)
  } yield User(name, email, hashedPassword, avatar)

Here the createUser() takes name, email, and password and returns a Future that contains the corresponding User (Future[User]).

To create a new user, we need to perform some steps that contain IO operations. The first one is to check if there’s a corresponding user in the database. The second is getting his/her avatar from a public avatar service.

We’re using Thread.sleep() to simulate the latency of database operations.

Since avatar() and notExist() are Futures and every Future has a flatMap, we can compose them in for-comprehension style.

2.3. Accessing Values

Until now, we’ve learned how to create Futures and chain them together, so how can we access the value result of a Future? Before going to the details of accessing the value of a Future, let’s learn more about the states of values in the Future construct.

Future has two stages:

  1. Not Completed: The computation is not completed yet.
  2. Completed: The computation is completed, leaving the result in one of two states. If the computation results in a value, it’s considered a success, and if it results in an exception, it’s considered a failure.

Until the result of the ongoing computation is ready, the state of the Future is not completed, and after that, the Future is in either the success or failure state.

Here, we introduce two methods for getting the result of a Future:

  1. Using the onComplete callback
  2. Blocking the Future by using result() or ready()

The Future has the onComplete() method. It gets the f: Try[T] => U as a callback, which let us decide what to do in each state of the completion stage:

val userFuture: Future[User] =
  createUser("John", "[email protected]", "secret")

userFuture.onComplete {
  case Success(user) =>
    println(s"User created: $user")
  case Failure(exception) =>
    println(s"Creating user failed due to the exception: $exception")
}

By providing a callback to the onComplete() method, once the Future is completed, the callback method gets called.

Another way to access the result of the asynchronous computation is to block it, but we don’t recommend this. Await.result() and Await.ready() functions block the thread until the computation completes. Here is the signature of each one:

def result[T](awaitable: Awaitable[T], atMost: Duration): T
def ready[T](awaitable: Awaitable[T], atMost: Duration): awaitable.type

The first function parameter is an awaitable to be awaited. As every Future is awaitable, we can pass it here. The next parameter is the duration that we will wait for the computation to be completed.

We can get the value of the Future by running the result() method of Await:

val user: User = Await.result(userFuture, Duration.Inf)

Be careful when using the result() method because it can throw an exception before it completes.

Another way is to wait until computation be completed without extracting the value of the Future, by applying the ready() function on the Future:

val completedFuture: Future[User] = Await.ready(userFuture, Duration.Inf)

‌After doing so, whenever the value() method is called on the Future, the result shouldn’t be None and must be either Some(Success(t)) or Some(Failure(error)):

completedFuture.value.get match {
  case Success(user) =>
    println(s"User created: $user")
  case Failure(exception) =>
    println(s"Creating user failed due to the exception: $exception")
}

3. Promise

The Promise is a writable, single-assignment container that completes a Future. The Promise is similar to the Future. However, the Future is about the read-side of an asynchronous operation, while the Promise is about the write-side.

In other words, the Promise is a write handle to a value that will be available at some point in the future. It allows us to put the value of a completed asynchronous operation into the Future, and change the state of the Future from not completed to completed by invoking the success method. Once the Promise is completed, we cannot call the success() method again.

Now, let’s look at the steps needed to create a Future from a Promise:

  1. Create a Promise for a type that we want to return in the future.
  2. Run the block of the computation code in an ExecutionContext by calling the execute method on the ExecutionContext. When the execution succeeds, we call the success() method of Promise to set the value of the completed computation. If the computation fails, we call the failure() method to change the state of Promise to failure.
  3. Return the Future, which will contain the value of our Promise.

Let’s write a function that gets block: => T  as a block of computation and then returns the Future[T] containing the future value of Promise:

def runByPromise[T](block: => T)(implicit ec: ExecutionContext): Future[T] = {
  val p = Promise[T]
  ec.execute { () =>
    try { 
      p.success(block)
    } catch {
      case NonFatal(e) => p.failure(e)
    }
  }
  p.future
}

The Promise is a good construct to bridge the gap between legacy Java callback-based APIs and Futures.

4. Conclusion

In this article, we’ve shown two constructs for writing non-blocking, asynchronous code in Scala. For handling the read-side of asynchronous operations, we introduced the Future construct, and for handling the write-side, we introduced the Promise.

As usual, the full source code can be found over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments