Published 2014-09-22.
Last modified 2019-06-27.
Time to read: 6 minutes.
This lecture discusses how callbacks work for Scala Futures, including completing Futures successfully and with failure. The transcript has been updated for Scala 2.12 – happily, the advice given previously was prescient so the recommendations given in this lecture have not changed.
The sample code for this lecture can be found in
courseNotes/.
We discussed ExecutionContexts in the MultiThreading lecture.
Futures require an ExecutionContext, and they are backed by thread pools.
Scala provides a default implementation, available by importing concurrent.ExecutionContext.Implicits.global.
We need five Promise[String] instances for this lecture's code examples, backed by the default ExecutionContext.
We'll create the first Promise with a value, and leave the other four pending.
scala> import concurrent._ import concurrent._
scala> import concurrent.ExecutionContext.Implicits.global import concurrent.ExecutionContext.Implicits.global
scala> import scala.util.{Try, Success, Failure} import scala.util.{Try, Success, Failure}
scala> val promises: Vector[Promise[String]] = Promise.successful("Hi there") +: (1 to 4).map { _ => Promise[String]() }.toVector promises: Vector[scala.concurrent.Promise[String]] = Vector(scala.concurrent.impl.Promise$KeptPromise@1d3ac898, scala.concurrent.impl.Promise$DefaultPromise@1b73be9f, scala.concurrent.impl.Promise$DefaultPromise@628c4ac0, scala.concurrent.impl.Promise$DefaultPromise@7b84fcf8, scala.concurrent.impl.Promise$DefaultPromise@30b19518)
Here are two utility methods that we will use to report on the status of the promises we just created:
def promiseStatus(i: Int): Unit =
if (promises(i).isCompleted) {
promises(i).future.value.get match {
case Success(value) => println(s"promises($i).future.value='$value'.")
case Failure(exception) => println(s"promises($i).future exception message: '${exception.getMessage}'.")
}
} else println(s"promises($i) is pending.")
def promiseStatuses(msg: String): Unit = {
println(s"\n== $msg ==")
0 until promises.length foreach { promiseStatus }
println()
}
The promiseStatuses method uses the shorthand discussed in the
Higher-Order Functions lecture.
Now let's see what the state of the promises are:
scala> promiseStatuses("Five Promises Started") scala> == Five Promises Started == promises(0).future.value='Hi there'. promises(1) is pending. promises(2) is pending. promises(3) is pending. promises(4) is pending.
Callback Methods
We can get the value of the promise after the value is written via the Promise's future property.
For a Promise[T], the type of the future property is Future[T].
The Future class provides four two callback methods.
-
onSuccess[U](pf: PartialFunction[T, U])(implicit executor: ExecutionContext): UnitonSuccess -
Deprecated in Scala v2.12.
onSuccessis called if theFuturecompleted successfully. ReturnsUnitso it cannot be composed.Removed in Scala v2.13
-
onFailure[U](pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): UnitonFailure -
Deprecated in Scala v2.12.
onFailureis called if theFuturecompleted with aThrowablevalue. ReturnsUnitso it cannot be composed.Removed in Scala v2.13
-
onComplete
onComplete[U](f: (Try[T]) => U)(implicit executor: ExecutionContext): Unit -
onCompleteis called regardless of whether theFuturecompleted successfully or not. ReturnsUnitso it cannot be composed. I recommend this callback not be used; useandTheninstead. -
andThen
andThen[U](pf: PartialFunction[Try[T], U])(implicit executor: ExecutionContext): Future[T] andThenchains a sequence of calls together, which are executed in the order listed.andThenworks just the same asonComplete, but multipleandThenclauses can be composed, and they are executed in the order listed. I recommend this callback be used instead ofonComplete.andThenis a composable callback; however,andThenis not a combinator because it does not transform theFuture.
You can define multiple callbacks for a Future including one, some or all of the above methods as desired.
None of these callbacks are able to alter the Future they are associated with.
The Future Combinators
lecture explores how you can use combinators to create modified versions of a Future.
These callbacks are either passed a Try[Future[T]] or the value of a Try.
onComplete and andThen receive a Function1 or PartialFunction1,
respectively, which have a Try as input.
The Try passed to either of the callbacks might be an instance of Success or Failure.
Callbacks Trigger Immediately for Promises Created With Values
We can use the onComplete callback to do something with the value of a Future once it is set.
Since promises(0)'s value was established when it was created,
the onComplete and andThen callbacks fire as soon as the the expression is evaluated.
scala> promises(0).future onComplete { | case Success(value) => | println(s"promises(0) completed successfully with value='$value'.") | | case Failure(throwable) => | println(s"promises(0) onComplete exception; message: '${throwable.getMessage}'.") | } promises(0) completed successfully with value='Hi there'.
Do you recognize this expression as being a partial function? If not, you should review the Partial Functions lecture earlier in this course.
Callbacks Trigger After Promises Complete
A Future's callbacks will be executed if it is created with a value or when its result becomes available.
Let's set up two callbacks for promises(1): andThen and onComplete.
Both of these callbacks will trigger when promises(1) completes.
scala> promises(1).future onComplete { | case Success(value) => | println(s"promises(1) onComplete success: value='$value'.") | | case Failure(throwable) => | println(s"promises(1) onComplete exception; message: '${throwable.getMessage}'.") | }
scala> promises(1).future andThen { | case Success(value) => | println(s"promises(1) andThen success: value='$value'.") | | case Failure(throwable) => | println(s"promises(1) andThen exception; message: '${throwable.getMessage}'.") | }
Because I am using the Scala REPL and I type relatively slowly compared to a CPU I can check to see if
promises(1) has completed, however you would not normally write this in an actual program because the promise
often would not be completed by the time the test is made.
scala> promises(1).isCompleted res0: Boolean = false
Now we will trigger the defined promises(1) callbacks by completing promises(1).
Notice that the andThen and onComplete callbacks both triggered,
but not in the declared order, and the onComplete callback's output appeared after the REPL prompt.
scala> promises(1).success("The tao that is knowable is not the true tao") promises(1) andThen success: value='The tao that is knowable is not the true tao'. promises(1) onComplete success: value='The tao that is knowable is not the true tao'. res1: scala.concurrent.Promise[String] = scala.concurrent.impl.Promise$DefaultPromise@1b73be9f promises(1) onComplete success; value='The tao that is knowable is not the true tao'.
Here is another way to complete a Promise, by passing the Success
subclass of Try to Promise.complete.
As you can see, promises can only be completed once because they are write-once objects.
scala> promises(1).complete(Success("The tao that is knowable is not the true tao")) java.lang.IllegalStateException: Promise already completed. at scala.concurrent.Promise$class.complete(Promise.scala:55) at scala.concurrent.impl.Promise$DefaultPromise.complete(Promise.scala:153) ...
Let's see what promiseStatuses reports now:
scala> promiseStatuses("After Promises(1) Completed") == After Promises(1) Completed == promises(0).future.value='Hi there'. promises(1).future.value='The tao that is knowable is not the true tao'. promises(2) is pending. promises(3) is pending. promises(4) is pending.
Partial Functions Can Have Many Cases
I showed onComplete and andThen with partial functions that have one or two cases,
but you could write more specific partial functions.
Here is an example of an onComplete callback with two cases:
val optionIntPromise1 = Promise[Option[Int]]()
optionIntPromise1.future OnComplete {
case Success(Some(value)) => println(s"The value $value only is matched if the future returns an Option")
case Success(None) => println(s"None could only be matched if the future returns an Option")
}
optionIntPromise1.success(Some(3))
Await.ready(optionIntPromise1.future, 30 minutes)
The Await.ready call causes the thread that is executing this code to cease doing work until the future passed to it completes.
The second argument causes a timeout to occur after 30 minutes passes,
and if the thread is still blocked then Await.ready returns Unit,
and the thread resumes execution.
Here is an example of an onComplete callback with 3 cases:
val optionFilePromise1 = Promise[Option[java.io.File]]()
optionFilePromise1.future onComplete {
case Failure(fnfe: java.io.FileNotFoundException) => println(s"Hey, you are missing a file! ${fnfe.getMessage}")
case Failure(ioe: java.io.IOException) => println(s"Could be burgler? ${ioe.getMessage}")
case Failure(throwable) => println(s"optionFilePromise1 throwable message: '${throwable.getMessage}'.")
}
optionFilePromise1.failure(new java.io.IOException("Oh noes!"))
Here is an example of an andThen callback with 5 cases:
val optionFilePromise2 = Promise[Option[java.io.File]]()
optionFilePromise2.future andThen {
case Success(Some(value)) => println(s"The value $value only is matched if the future returns an Option")
case Success(None) => println(s"None could only be matched if the future returns an Option")
case Failure(fnfe: java.io.FileNotFoundException) => println(s"Hey, you are missing a file! ${fnfe.getMessage}")
case Failure(ioe: java.io.IOException) => println(s"Could be burgler? ${ioe.getMessage}")
case Failure(throwable) => println(s"optionFilePromise2 exception message: '${throwable.getMessage}'.")
}
optionFilePromise2.failure(new java.io.IOException("Oh noes!"))
The number of cases in the partial function is only limited by the types and values returned by the Future.
Chaining andThen Callbacks
onComplete returns Unit, so it cannot be chained with other methods, unfortunately.
Happily, andThen returns the original Future, so we can chain multiple andThen clauses together.
Notice that the partial function passed to the first andThen contains cases for Success and Failure,
while the partial function passed to the second andThen has one catch-all case.
scala> promises(2).future andThen { | case Success(value) => println(s"promises(2) andThen success: promise3 value='$value'.") | case Failure(throwable) => println(s"promises(2) andThen exception message: '${throwable.getMessage}'.") | } andThen { | case _ => println("promises(2).andThen.andThen - Will that be all, master?") | }
scala> promises(2).complete(Success("Great achievement looks incomplete, yet it works perfectly")) promises(2) andThen success value='Great achievement looks incomplete, yet it works perfectly'. promises(2).andThen.andThen - Will that be all, master?
You can think of andThen as being like onComplete,
but andThen returns the original Future,
instead of returning Unit like onComplete does.
I never use onComplete, I always use andThen instead.
We'll see an important reason at the end of this lecture.
Let's see what promiseStatuses reports now:
scala> promiseStatuses("After Promises(2) Completed") == After Promises(2) Completed == promises(0).future.value='Hi there'. promises(1).future.value='The tao that is knowable is not the true tao'. promises(2).future.value='Great achievement looks incomplete, yet it works perfectly'. promises(3) is pending. promises(4) is pending.
Completing With Failure
As we discussed in the Traits / Mixins lecture in the
Introduction to Scala course,
NoStackTrace
can increase efficiency if it is used to define an object or a class,
but you lose stack traces which might be useful for debugging errors later on,
leading to questions like "what created this Throwable?"
If you are using Futures and Promises and therefore your program switches execution contexts at lot,
the cost of populating the stack trace might not be noticeable.
John Rose of Oracle wrote an
interesting article
that discusses this topic.
scala> import scala.util.control.NoStackTrace import scala.util.control.NoStackTrace
scala> class ExceptTrace(msg: String) extends Exception(msg) with NoStackTrace defined class ExceptTrace
scala> promises(3).future.onComplete { | case Failure(exception) => | println(s"promises(3) completed with exception message: '${exception.getMessage}'.") | }
scala> promises(3).failure(new ExceptTrace("Boom!")) promises(3) completed with exception message: 'Kaboom'. res3: scala.concurrent.Promise[String] = scala.concurrent.impl.Promise$DefaultPromise@7b84fcf8
The above is efficient; only one class is defined, an instance will be created each time one is required.
Let's examine the value of the completed Future that is associated with each Promise.
scala> promiseStatuses("After Promises(3) Completed") == After Promises(3) Completed == promises(0).future.value='Hi there'. promises(1).future.value='The tao that is knowable is not the true tao'. promises(2).future.value='Great achievement looks incomplete, yet it works perfectly'. promises(3).future exception message: 'Kaboom'. promises(4) is pending.
If some of your exceptions are invariant in all their properties,
you could define a singleton object instead of a class instance for even greater efficiency.
scala> object TheExceptTrace extends Exception("Boom!") with NoStackTrace defined module TheExceptTrace
scala> promise5.complete(Failure(TheExceptTrace)) promise4: scala.concurrent.Promise[Nothing] = scala.concurrent.impl.Promise$KeptPromise@5f2adaa8
Let's examine the value of the completed Future that is associated with each Promise.
scala> promiseStatuses("Five Promises Concluded") == Five Promises Concluded == promises(0).future.value='Hi there'. promise(1).future.value='The tao that is knowable is not the true tao'. promise(2).future.value='Great achievement looks incomplete, yet it works perfectly'. promise(3).future exception message: 'Kaboom'. promise(4).future exception message: 'Kablam'.
Creating a Future Directly
You don't have to create a Promise in order to create a Future –
you can create a Future directly, using its companion object's apply method.
Let's define a tail-recursive factorial method that will take a long time to run because it is CPU-bound,
and execute it within a Future.
scala> def factorial(number: BigInt): BigInt = { | import annotation.tailrec | @tailrec def fact(total: BigInt, number: BigInt): BigInt = { | if (number == BigInt(1)) | total | else | fact(total* number, number - 1) | } | fact(1, number) | } factorial: (number: BigInt)BigInt
scala> Future(factorial(12345)) res4: scala.concurrent.Future[BigInt] = scala.concurrent.impl.Promise$DefaultPromise@115989a9
You will find factorial defined in the multi package object, in
courseNotes/.
The factorial method is automatically lifted to a Function1[BigInt, BigInt] when passed to Future.apply.
Now let's run the computation within a Future and provide an onComplete callback.
scala> val f1 = Future(multi.factorial(12345))
scala> f1 onComplete { | case Success(value) => println(s"factorial success #1: factorial(12345)=$value") | case Failure(exception) => println(s"factorial onComplete completed with exception message: '${exception.getMessage}'.") | } After a delay (depending on how fast you type), huge output!
Finally, let's set up a same computation using the andThen callback instead of onComplete.
scala> val f2 = Future(multi.factorial(12345)) andThen { | case Success(value) => println(s"factorial success #2: factorial(12345)=$value") | case Failure(exception) => println(s"factorial andThen completed with exception message: '${exception.getMessage}'.") | } After a delay, huge output!
Notice that the REPL prompt reappeared, then a bit later the output of the andThen factorial appeared.
Also notice that the assignment to f2 was performed in the same expression that set up the andThen callback.
This was not possible with onComplete, because onComplete returns Unit
Exercise – Test Your Understanding
The preceding onComplete always generates output when used from the REPL,
but when the demo program runs the onComplete callback sometimes does not generate output.
Take a look at the demo program (multi/futures/FutureCallback.scala) and try to understand why.
The program ends with these two statements.
Future.sequence(List(f1, f2) ::: promises.map(_.future).toList) andThen { case _ => System.exit(0) }
synchronized { wait() }
We know that the last statement pauses the main thread. Let's examine the penultimate statement for a moment. First, look at this expression:
List(f1, f2) ::: promises.map(_.future).toList
This expression creates a List of the Futures from each promise in
promises and concatenates the List[Future[_]] with a List of the
Futures f1 and f2,
yielding a List[Future[_]] containing every Future created in the entire demo program,
including the Futures contained in the Promises.
We'll learn how Future.sequence works in the
Working With Collections of Futures lecture,
but for now suffice it to say that it forms a new Future from the list of Futures.
When the Future of the List[Future[_]] completes, the andThen callback triggers.
Why would the onComplete callback sometimes not generate output?
Solution and Takeway
The andThen callback returns a new Future with the same value as the Future it was invoked from.
The result of andThen was assigned to f2, and f2's completion was used as one of the criteria for ending the program.
Conversely, because onComplete does not return a value, we were forced to use the completion of the original future (f1) as one of the conditions for ending the program.
There is no guarantee that the callback will be called or completed before its thread is terminated.
This means that sometimes f1's callback is able to complete, and sometimes the program ends before output is generated.
andThen returns a Future after the callback has completed.
The other callback (onComplete) cannot be used in a
convenient manner without blocking such that an assertion can be made that these callbacks have completed at a later time.
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.