Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Futures are Calling and I Must Choose

The Futures are Calling and I Must Choose

Presented at ScalaDays Chicago 2017

Comparison of Scala Future, Twitter Future, Guava ListenableFuture, and Java CompletableFuture, with a peek at Tasks too!

Chris Phelps

April 21, 2017
Tweet

More Decks by Chris Phelps

Other Decks in Programming

Transcript

  1. Copyright 2017 Tendril, Inc. All rights reserved. The Futures are

    Calling and I Must Choose SCALADAYS CHICAGO, APRIL 19, 2017
  2. Copyright 2017 Tendril, Inc. All rights reserved. Futures are values.

    OVERVIEW The Future does not represent a computation which produces the value.
  3. Copyright 2017 Tendril, Inc. All rights reserved. Future History OVERVIEW

    • Java Future modeled async operations • Minimal ways to observe and react • Twitter and Akka • Scala SIP-14, Jan 2012 • Scala standard Future in 2.10 6
  4. Copyright 2017 Tendril, Inc. All rights reserved. Agenda OVERVIEW •

    Scala Future • Twitter Future • Guava ListenableFuture • Java CompletableFuture • Tasks • Comparison and Recommendation 7 https://www.flickr.com/photos/3oheme/5141328136/in/photolist-8QjDJy
  5. Copyright 2017 Tendril, Inc. All rights reserved. What makes up

    a Future? SCALA FUTURES • 3 states • Unresolved • Resolved with Success and a value • Resolved with Failure and a Throwable • Immediately resolved instances • Promises • Rich set of combinators in class and companion object
  6. Copyright 2017 Tendril, Inc. All rights reserved. Obtaining a Future

    SCALA FUTURES • Call some library function • Future.apply() • Promise
  7. Copyright 2017 Tendril, Inc. All rights reserved. Creating a Future

    with apply() SCALA FUTURES 11 import scala.concurrent.ExecutionContext.Implicits.global val myFuture: Future[Int] = Future { calculateSlowly() } Creating a Future with Promise val myPromise = Promise[Int]() val myNewFuture = myPromise.future val res = calculateSlowly() myPromise.success(res)
  8. Copyright 2017 Tendril, Inc. All rights reserved. Promises SCALA FUTURES

    12 Promise Producer Consumer 1. return Future 2. register callback 3. complete() 4. callback executed
  9. Copyright 2017 Tendril, Inc. All rights reserved. Callbacks SCALA FUTURES

    13 import scala.concurrent.ExecutionContext.Implicits.global val myFuture: Future[Int] = calculate() myFuture onComplete { case Success(res) => doSomethingWith(res) case Failure(t) => println(“An error occurred”) } myFuture onSuccess { case res => doSomethingWith(res) } myFuture onFailure { case t => println(“An error occurred”) }
  10. Copyright 2017 Tendril, Inc. All rights reserved. Future combinators SCALA

    FUTURES • map [S](f: (T) ⇒ S)(...): Future[S] • flatMap [S](f: (T) ⇒ Future[S])(...): Future[S] 14
  11. Copyright 2017 Tendril, Inc. All rights reserved. Map and FlatMap

    SCALA FUTURES 15 def calculate(): Future[Int] = ??? val myFuture = calculate() def scale(in: Int): Int = ??? val scaled = myFuture.map { x => scale(x) } def recalculate(in: Int): Future[Int] = ??? val recalculated = mapped.flatMap { x => recalculate(x) } val chained = for { a <- calculate() b <- recalculate(a) } yield scale(b)
  12. Copyright 2017 Tendril, Inc. All rights reserved. Future combinators SCALA

    FUTURES • map [S](f: (T) ⇒ S)(...): Future[S] • flatMap [S](f: (T) ⇒ Future[S])(...): Future[S] • foldLeft [T, R](futures: Iterable[Future[T]]) (zero: R)(op: (R, T) ⇒ R)(...): Future[R] • firstCompletedOf[T](futures:TraversableOnce[Future[T]]) (...): Future[T] • sequence[A, M[X] <: TraversableOnce[X]] (in: M[Future[A]])(...): Future[M[A]] 16
  13. Copyright 2017 Tendril, Inc. All rights reserved. Computation runs in

    an ExecutionContext SCALA FUTURES • Transform body is submitted to EC as a Callable • Global execution context – ForkJoin pool • Build from any Executor or ExecutionService • Why might you chose one over another? • Control over pool size and characteristics • ForkJoinPool is not ideal for long blocking operations • Certain work in a particular pool 17
  14. Copyright 2017 Tendril, Inc. All rights reserved. Twitter Futures have

    some differences TWITTER FUTURES • Combinators • Execution model • Cancellation
  15. Copyright 2017 Tendril, Inc. All rights reserved. Twitter supports many

    standard combinators TWITTER FUTURES • filter (p: (A) ⇒ Boolean): Future[A] • flatMap [B](f: (A) ⇒ Future[B]): Future[B] • flatten [B](implicit ev: <:<[A, Future[B]]): Future[B] • foreach (k: (A) ⇒ Unit): Future[A] • map [B](f: (A) ⇒ B): Future[B] • withFilter (p: (A) ⇒ Boolean): Future[A]
  16. Copyright 2017 Tendril, Inc. All rights reserved. Twitter adds many

    combinators TWITTER FUTURES • by (when: Time)(implicit timer: Timer): Future[A] • within (timeout: Duration)(implicit timer: Timer): Future[A] • parallel [A](n: Int)(f: ⇒ Future[A]): Seq[Future[A]] • select [A](fs: Seq[Future[A]]): Future[(Try[A], Seq[Future[A]])] • when [A](p: Boolean)(f: ⇒ Future[A]): Future[Unit] • whileDo [A](p: ⇒ Boolean)(f: ⇒ Future[A]): Future[Unit] Theme: ”Observing” or “Choreographing” other futures
  17. Copyright 2017 Tendril, Inc. All rights reserved. TWITTER FUTURES scala.util.Future

    com.twitter.util.Future • isCompleted: Boolean • value: Option[Try[T]] • recover [U >: T](pf: PartialFunction[Throwable, U]) : Future[U] • recoverWith [U >: T](pf: PartialFunction[Throwable, Future[U]]) : Future[U] • zip [U](that: Future[U]): Future[(T, U)] • zipWith [U, R](that: Future[U])(f: (T, U) ⇒ R): Future[R] • isDone (implicit ev: <:<[Future.this.type, Future[Unit]]): Boolean • poll: Option[Try[A]] • handle [B >: A](rescueException: PartialFunction[Throwable, B]): Future[B] • rescue [B >: A](rescueException: PartialFunction[Throwable, Future[B]]): Future[B] • join [B] (other: Future[B]): Future[(A, B)] • joinWith [B, C] (other: Future[B])(fn: (A, B) ⇒ C): Future[C] 22 Twitter supports many of the same combinators but with different names
  18. Copyright 2017 Tendril, Inc. All rights reserved. TWITTER FUTURES scala.util.Future

    com.twitter.util.Future • recover [U >: T](pf: PartialFunction[Throwable, U]) (implicit executor: ExecutionContext): Future[U] • recoverWith [U >: T](pf: PartialFunction[Throwable, Future[U]]) (implicit executor: ExecutionContext): Future[U] • zip [U](that: Future[U]): Future[(T, U)] • zipWith [U, R](that: Future[U])(f: (T, U) ⇒ R) (implicit executor: ExecutionContext): Future[R] • handle [B >: A](rescueException: PartialFunction[Throwable, B]): Future[B] • rescue [B >: A](rescueException: PartialFunction[Throwable, Future[B]]): Future[B] • join [B] (other: Future[B]): Future[(A, B)] • joinWith [B, C] (other: Future[B])(fn: (A, B) ⇒ C): Future[C] 23 Except.... No ExecutionContext?
  19. Copyright 2017 Tendril, Inc. All rights reserved. The Twitter execution

    model is different from Scala TWITTER FUTURES • Transform body is executed in the prior thread • Pipeline executed by same thread • Work can be forked to a new pool • Will these necessarily be in order? Depth first or something else? • If already resolved, new callback executes immediately in caller thread • Future.apply() runs body immediately in caller thread • New Future creation needs explicit FuturePool 24
  20. Copyright 2017 Tendril, Inc. All rights reserved. Creating new Twitter

    futures TWITTER FUTURES val pool = FuturePool.unboundedPool val notf = Future { // executes immediately in caller thread calculate() } val f = pool { // executes in the pool calculate() } 25
  21. Copyright 2017 Tendril, Inc. All rights reserved. Twitter Future supports

    cancellation TWITTER FUTURES • Promise raise() – marks promise as interrupted • Future is NOT resolved • Promise isInterrupted() • Running calculation is not stopped • Calculation can check isInterruped() and interrupt itself • raiseWithin() – NEW future which fails after the timeout 26
  22. Copyright 2017 Tendril, Inc. All rights reserved. Cancellation TWITTER FUTURES

    val pool = FuturePool.unboundedPool val promise = new Promise[Int]() pool { val r1 = expensiveCalculation() promise.isInterrupted match { case Some(t) => promise.setException(t) case None => promise.setValue(nextCalculation(r1)) } } promise.raise(new IllegalStateException(“I give up”)) 27
  23. Copyright 2017 Tendril, Inc. All rights reserved. Bijection TWITTER FUTURES

    import scala.concurrent.{Future => ScalaFuture} import com.twitter.util.{Future => TwitterFuture} import com.twitter.bijection.Conversion.asMethod import com.twitter.bijection.twitter_util.UtilBijections._ def doSomething: ScalaFuture[T] = { val response: TwitterFuture[T] = ??? response.as[ScalaFuture[T]] } 28
  24. Copyright 2017 Tendril, Inc. All rights reserved. Guava ListenableFuture GUAVA

    LISTENABLEFUTURE • Google Guava core library • Brought functional collections to Java • ListenableFuture provides Futures with callbacks • From ca 2011 • Before Scala Future • Before CompletableFuture
  25. Copyright 2017 Tendril, Inc. All rights reserved. Guava ListenableFuture GUAVA

    LISTENABLEFUTURE • ListenableFuture - Java Future with added callbacks • addListener() • Futures - Functional-style transformations • transform() • allAsList(), successfulAsList() • catching(), catchingAsync() • immediateFuture(), immediateFailedFuture() • ListeningExecutorService – decorator to create ListenableFuture instances • SettableFuture – analogous to Promise
  26. Copyright 2017 Tendril, Inc. All rights reserved. Transforms and callbacks

    GUAVA LISTENABLEFUTURE val es = MoreExecutors.listeningDecorator( Executors.newFixedThreadPool(3)) def doThing(): ListenableFuture[Int] = { es.submit(new Callable[Int] { override def call() = expensiveCalculation() }) } val g: ListenableFuture[Int] = Futures.transform(f, new GuavaFunction[Int, Int]() { override def apply(x: Int) = scaleResult(x) }) Futures.addCallback(h, new FutureCallback[Int] { override def onSuccess(result: Int): Unit = ??? override def onFailure(t: Throwable): Unit = ??? }) 32
  27. Copyright 2017 Tendril, Inc. All rights reserved. Transforms and callbacks

    (with 2.12) GUAVA LISTENABLEFUTURE val es = MoreExecutors.listeningDecorator( Executors.newFixedThreadPool(3)) def doThing(): ListenableFuture[Int] = { es.submit(() => expensiveCalculation()) } val g: ListenableFuture[Int] = Futures.transform(f, ((x: Int) => scaleResult(x)): GuavaFunction[Int,Int]) Futures.addCallback(h, new FutureCallback[Int] { override def onSuccess(result: Int): Unit = ??? override def onFailure(t: Throwable): Unit = ??? }) 33
  28. Copyright 2017 Tendril, Inc. All rights reserved. ListenableFuture is unpleasant

    from Scala GUAVA LISTENABLEFUTURE • Not chainable • Requires type hint • Relies on GuavaFunction and AsyncFunction • transform() vs transformAsync() (in Guava 20.0) • ExecutionContext required (in Guava 21.0) 34
  29. Copyright 2017 Tendril, Inc. All rights reserved. Gilt Foundation Classes

    GUAVA LISTENABLEFUTURE import com.gilt.gfc.guava.future.GuavaFutures implicit val es = Executors.newFixedThreadPool(3) val lf: ListenableFuture[String] = GuavaFutures.future { figureOutTheThing() } import com.gilt.gfc.guava.future.FutureConverters._ val alf: ListenableFuture[Int] = Future { findOtherThing() }.asListenableFuture import com.gilt.gfc.guava.future.GuavaFutures._ val res: ListenableFuture[Int] = lf.map(s => s.length) 35
  30. Copyright 2017 Tendril, Inc. All rights reserved. Java CompletableFuture JAVA

    COMPLETABLEFUTURE • Java8 • Future with callbacks • Acts as both Promise and Future • Creation • As Promise • In an executor via supplyAsync(Supplier) • In an executor via runAsync(Runnable)
  31. Copyright 2017 Tendril, Inc. All rights reserved. CompletableFuture example JAVA

    COMPLETABLEFUTURE val cf = new CompletableFuture[Int]() val x1: CompletableFuture[Int] = cf.thenApplyAsync( (i: Int) => scaleTransform(i)) // final stage – deal with the result x1.handle((r, t) => if (r != null) doHappyPath(r) else if (t != null) doExceptionPath(t)) // some time later cf.complete(41) 38
  32. Copyright 2017 Tendril, Inc. All rights reserved. Execution Model JAVA

    COMPLETABLEFUTURE • Different methods to do direct or async operations • apply(), applyAsync() • handle() handleAsync() • Regular version uses the same thread • Async versions use default executor or specified executor • Default executor is ForkJoinPool.commonPool() • Pool sized based on number of cores • New thread if commonPool is not configured for parallelism 39
  33. Copyright 2017 Tendril, Inc. All rights reserved. How do names

    and combinators compare? JAVA COMPLETABLEFUTURE • CompletableFuture operations provide variants: • Runnable / Supplier • Function / Consumer • No recoverWith, just recover (exceptionally) • Cannot replace a failure with an unresolved fallback • Combinators execute next step without intermediate Future • Scala zip() resolves with a tuple, map() to execute next step • foo.thenCombine(bar, (a,b) => doSomethingWith(a,b)) • Overall handler method taking nullable success and failure • handle(BiFunction<? super T,Throwable,? extends U> fn) 40
  34. Copyright 2017 Tendril, Inc. All rights reserved. Tasks can be

    thought of as lazy Futures • Scalaz, Monix, FS2 • Explicitly run: attemptRun(), runAsync(), unsafeRunAsync() • No unwanted computations • No leaked computations • Memoization • Run every time by default • Memoize the result on demand • Trampolined • Cancelable
  35. Copyright 2017 Tendril, Inc. All rights reserved. Monix Task example

    JAVA COMPLETABLEFUTURE import monix.execution.Scheduler.Implicits.global import monix.execution.CancelableFuture import monix.eval.Task import scala.util.{Success, Failure} val task = Task { 1 + 1 } val cancelable = task.runAsync { result => result match { case Success(value) => ??? case Failure(ex) => ??? }} val future: CancelableFuture[Int] = task.runAsync 43
  36. Copyright 2017 Tendril, Inc. All rights reserved. Scala Twitter Guava

    Java Promise-like creation Promise Promise SettableFuture CompletableFuture Async creation Future.apply() myPool.apply() ListeningDecorator execSvc.submit() supplyAsync() Execution model Run in ExecutionContext Completing thread runs Completing thread or executor (method overload) Completing thread or executor (different method) Supports for- comprehension Yes Yes No (yes with gfc-guava) No (possible with conversions) Cancellation No Yes (cooperative) No Yes (no interrupt despite argument) Java/Scala interoperation Implement e.g. PartialFunction from Java Variants provided for interop Java fair, Scala bad Better with gfc-guava Java good, type inference issues Future comparison CONCLUSIONS 45
  37. Copyright 2017 Tendril, Inc. All rights reserved. What do we

    recommend? CONCLUSIONS • Use whatever your libraries use • If you aren’t introducing Twitter, stick with Scala unless you need cancellation or monitoring combinators • If you need to mix Scala and Twitter, use Bijection • If you need to interop with Java, prefer CompletableFuture • At this point, avoid Guava ListenableFuture 46
  38. Copyright 2017 Tendril, Inc. All rights reserved. Other abstractions to

    consider CONCLUSIONS • ReactiveX • Observable streams • Multiple languages supported • Streams • Scalaz, Fs2, Akka • Actors • Akka • Coroutines 47
  39. Copyright 2017 Tendril, Inc. All rights reserved. Chris Phelps @CJPhelps

    https://github.com/chrisphelps/futures-are-calling 48 Photo used under a Creative Commons license (Zach Dischner)