Promises in Scala

Versions: Scala 2.12.1

The Futures appear as the first element to learn of Scala's asynchronous world. They're quite simple and probably exist in the languages you have been working on before. But they're not the single asynchronous types in Scala because they are accompanied by Promises covered in this post.

Data Engineering Design Patterns

Looking for a book that defines and solves most common data engineering problems? I'm currently writing one on that topic and the first chapters are already available in πŸ‘‰ Early Release on the O'Reilly platform

I also help solve your data engineering problems πŸ‘‰ contact@waitingforcode.com πŸ“©

At first glance both Futures and Promises look similar, so I will start by explaining the differences between them in the first section. In the next one, I will describe the characteristic points about the Promises. Finally, in the last part, I will explore several use cases of Promises.

Future vs Promise

We can analyze the difference between Future and Promise types from different points of view. The first one is about the dependency between both types. Future represents an asynchronous computation which will complete at some point in the future and we can build it simply by putting the asynchronous code inside its apply method. But we can also create a Future from an instance of Promise through its future method.

Another difference between these types is the value definition. Future is a read-only type and the value is passed as a parameter to its apply method. On the other side, the value returned by the Promise can be set from outside with one of the available public setters like complete or success.

Also, we can think about Future and Promise in terms of code intent. When a Future is defined as a function's parameter, we can consider it as a value computed at some point in the future. When a Promise is used as an argument, we can consider that the function's body could contain the logic generating the value. And we can apply that logic to the return types. When a Future is returned, it says that the value may not be currently available whereas a returned Promise may delegate the data generation to the function consuming the returned type.

Promise characteristics

Therefore, a Promise is writable type that can be used t complete a future. It can return a success or a failure:

it("should complete a Future with a success") {
  val numberPromise = Promise[Int]()

  val numberFuture = numberPromise.future
  numberPromise.complete({
    Thread.sleep(500L)
    Success(2)
  })

  val number = Await.result(numberFuture, 700 millis)

  number shouldEqual 2
}
it("should complete a Future with a failure") {
  val numberPromise = Promise[Int]()
  numberPromise.failure(new RuntimeException("Unexpected failure"))

  val error = intercept[RuntimeException] {
    Await.result(numberPromise.future, 700 millis)
  }

  error.getMessage shouldEqual "Unexpected failure"
  error shouldBe a [RuntimeException]
}

However, it complete the Future only once:

it("should not be completed twice") {
  val numberPromise = Promise[Int]()
  numberPromise.complete(Success(2))
  val error = intercept[IllegalStateException] {
    numberPromise.complete(Success(3))
  }

  error.getMessage shouldEqual "Promise already completed."
}

it("should fail when multiple tasks call complete method") {
  val numberGenerator = Promise[Int]()
  val latch = new CountDownLatch(2)
  var alreadyCompletedException: Option[IllegalStateException] = None

  Future {
    Thread.sleep(ThreadLocalRandom.current().nextLong(2000L))
    try {
      numberGenerator.complete(Success(1))
    } catch {
      case ise: IllegalStateException => alreadyCompletedException = Some(ise)
    }
    latch.countDown()
  }
  Future {
    Thread.sleep(ThreadLocalRandom.current().nextLong(2000L))
    try {
      numberGenerator.complete(Success(2))
    } catch {
      case ise: IllegalStateException => alreadyCompletedException = Some(ise)
    }
    latch.countDown()
  }

  val number = Await.result(numberGenerator.future, 2000 millis)
  (number == 1 || number == 2) shouldBe true
  latch.await(2, TimeUnit.SECONDS)
  alreadyCompletedException shouldBe defined
  alreadyCompletedException.get.getMessage shouldEqual "Promise already completed."
}

The last test failed because of the complete(...) method called twice to set the value. Fortunately, Promise has safer methods to mark it as completed. We can distinguish 3 such methods, trySuccess(value: T), tryFailure(cause: Throwable) and tryComplete(result: Try[T]). The difference is only the type passed as a parameter and the purpose explained by the name. These safe methods can be used to write the code where multiple threads try to complete one common Promise and the results of the fastest one is used:

it("should be completed by one of the first 2 tasks") {
  val numberGenerator = Promise[Int]()
  val latch = new CountDownLatch(2)
  var completed1 = false
  var completed2 = false

  Future {
    Thread.sleep(ThreadLocalRandom.current().nextLong(2000L))
    numberGenerator.trySuccess(1)
    completed1 = true
    latch.countDown()
  }
  Future {
    Thread.sleep(ThreadLocalRandom.current().nextLong(2000L))
    numberGenerator.trySuccess(2)
    completed2 = true
    latch.countDown()
  }

  val number = Await.result(numberGenerator.future, 2000 millis)
  (number == 1 || number == 2) shouldBe true
  latch.await(2, TimeUnit.SECONDS)
  completed1 shouldBe true
  completed2 shouldBe true
  val numberAfterSecondFutureCompletion = Await.result(numberGenerator.future, 2000 millis)
  numberAfterSecondFutureCompletion shouldEqual number
}

Use cases

At first glance, finding the good use cases of Promises is hard because we think being able to write everything with the Futures. But it's not the case. One of good illustrations for Promise usefulness is the last example from the previous section. We have there 2 concurrent asynchronous processes working on one shared value. Needless to say that the Future's firstCompletedOf((futures: TraversableOnce[Future[T]])) method uses that pattern under-the-hood:

def firstCompletedOf[T](futures: TraversableOnce[Future[T]])(implicit executor: ExecutionContext): Future[T] = {
  val p = Promise[T]()
  val completeFirst: Try[T] => Unit = p tryComplete _
  futures foreach { _ onComplete completeFirst }
  p.future
}

It's not only hard but also less readable than the Promise. But it's only the first use case. I was talking about the next point in the first section. The Promise is also a good way to declare the intent of the code. When a Promise is used as the parameter, we should implement the computation part directly in the function. Otherwise, we would use a Future whose computation logic is implemented at the moment of initialization:

val fileFromCloud = Promise[String]

getAsync(fileFromCloud)

def getAsync(fileRetrievalPromise: Promise[String]): Future[String] = {
    fileRetrievalPromise.success(....
    fileRetrievalPromise.future
}

However, I agree that it's less evident to see than the previous use case. After all, you can reformulate the code into:

def getAsync: Future[String] = {
    Future {
        // logic retrieval here
    }
}

But the code intent use case is more evident for the returned type. The following method immediately suggests its caller to provide the computation logic:

def getAsync: Promise[String] = {
    // something else...
    Promise[String]
}

Whereas to return a Future in such context, the caller must inject the logic as a parameter:

def getAsync(retrievalLogic: () => String): Future[String] = {
    // something else...
    Future {
        // something here
        retrievalLogic()
    }
}

I think you'll agree with me that exposing a method returning a Promise lets the callers more flexibility. Our use case was simple and maybe it's not easy to see that flexibility point. But let's imagine that one caller wants to pass a function with a String parameter ((String) => String). It would be impossible in the current implementation and adapting it for every variance would complexify the code very much.

The Promise API also provides an alternative way to compose the asynchronous calls:

it("should be combined with other Promises") {
  val promise1 = Promise[Int]()
  val promise2 = Promise[Int]()
  val promise3 = Promise[Int]()

  Future {
    Thread.sleep(2000L)
    println("Completing the promise1")
    promise1.success(2)
  }
  Future {
    Thread.sleep(1000L)
    println("Completing the promise1")
    promise2.success(6)
  }

  promise1.future.foreach(nr1 => {
    promise2.future.foreach(nr2 => {
      promise3.success(nr1 + nr2 + 2)
    })
  })
  val number = Await.result(promise3.future, 2500 millis)

  number shouldEqual 10
}

The definition with Premises is however very close to the definition with Futures:

describe("Future") {
  it("should be composable harder than a Promise") {
    val future1 = Future {
      Thread.sleep(2000L)
      1
    }

    val future2 = future1.transformWith {
      case Success(nr) => Future(nr + 10)
    }.transformWith {
      case Success(nr) => Future(nr + 20)
    }

    val number = Await.result(future2, 2500 millis)

    number shouldEqual 31
  }
}

Some people compare Scala Promises to Java's CompletableFutures and I think it's a good summary to describe this another asynchronous type. Like this Java class, Promise is a declaration of a future computation that may be completed at any place in the program. Unlike Future, we don't need to define the completion code when the instance is initialized. Instead, we can defer the completion moment. It opens some new use cases, especially when we design a public API where, by passing a Promise as a parameter, we give a lot of flexibility to the client.