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

Asynchronous streams in direct style with and without macros

Philipp Haller
June 13, 2019
420

Asynchronous streams in direct style with and without macros

Philipp Haller

June 13, 2019
Tweet

Transcript

  1. Asynchronous streams in direct style with and without macros Philipp

    Haller KTH Royal Institute of Technology Stockholm, Sweden
  2. About myself • Associate Professor at KTH Royal Institute of

    Technology in Stockholm, Sweden • Previous positions at Typesafe Inc., Stanford University, and EPFL • PhD 2010 EPFL • Research interests in programming language design and implementation, type systems, concurrency, and distributed programming
  3. A simple task • Given an asynchronous method that returns

    the day of the year as a string wrapped in a future:
 def dayOfYear(): Future[String] • However, sometimes, for unknown reasons, dayOfYear completes the returned future with "", or null, or the string "nope". • Task: create a future which is completed either • with "It's <month>!" where <month> is the textual representation matching the month returned by dayOfYear, or • with "Not a date, mate!" when that's really the best we can do. "04/11" for April 11, or "06/13" for June 13
  4. A solution using for- comprehensions def solution() = { val

    date = """(\d+)/(\d+)""".r for { dayString <- dayOfYear() response <- dayString match { case date(month, day) => for (name <- nameOfMonth(month.toInt)) yield s"It's $name!" case _ => Future.successful("Not a date, mate!") } } yield response } Can we improve readability?
  5. Enter scala-async • Purpose:
 Simplify asynchronous programming primarily based on

    futures. • Created by Jason Zaugg (Lightbend) and yours truly, first released in 2013
  6. A solution using scala-async def solution() = async { val

    date = """(\d+)/(\d+)""".r await(dayOfYear()) match { case date(month, day) => s"It's ${await(nameOfMonth(month.toInt))}!" case _ => "Not a date, mate!" } } No need to name all intermediate results No need for explicit Future.successful() Fewer closures allocated: async {} = 1 closure
  7. Let’s make our previous task slightly more complex! • Instead

    of converting a single string "04/11" (or "06/13" etc.), "", null, or "nope", we should consume a stream of multiple such strings • Output should be a publisher which produces a stream of results
  8. Intermezzo: Reactive Streams • a.k.a. java.util.concurrent.Flow (since JDK 9) •

    Interfaces: j.u.c.Flow.{Publisher, Subscriber, Subscription, Processor} • Some key prior work: • Observer design pattern (Gamma et al. 1994)1 • Reactive Extensions (Erik Meijer 2012)2 • Key innovation of Reactive Streams: backpressure control 2Erik Meijer, Your mouse is a database, Commun. ACM 55(5) (2012) 66–73. 1Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns:
 Elements of Reusable Object Oriented Software, Addison-Wesley Professional, 1994 Composition of observable streams using higher-order functions
  9. Publisher • A Flow.Publisher<T> produces events of type T which

    are received by subscribers if they have expressed interest. public static interface Flow.Publisher<T> { void subscribe(Flow.Subscriber<? super T> subscriber); }
  10. Subscriber • A Flow.Subscriber<T> s consumes events of type T

    from publishers to which s has subscribed and from which s has requested to receive (a certain number of) events. public static interface Flow.Subscriber<T> { void onSubscribe(Flow.Subscription subscription); void onNext(T item); void onError(Throwable throwable); void onComplete(); }
  11. Subscription • A Flow.Subscription enables subscribers to control the flow

    of events received from publishers. public static interface Flow.Subscription { void request(long n); void cancel(); }
  12. Reactive Extensions • Libraries building on basic interfaces for publishers,

    subscribers etc. • Key: higher-order functions for composing publishers • Implementations for many languages available
  13. TextChanges(input) .Select(word => Completions(word)) .Switch() .Subscribe(ObserveChanges(output)); Example from (Erik Meijer

    2012) Image source: Erik Meijer, Your mouse is a database, Commun. ACM 55(5) (2012) 66–73.
 https://queue.acm.org/detail.cfm?id=2169076
  14. Challenges • Often need diagrams to understand semantics (cf. “marble

    diagrams” used to document Reactive Extensions) • Creation of combinators challenging • Stateful combinators in particular • Inversion of control
  15. Back to our task • Instead of converting a single

    string "04/11" (or "06/13" etc.), "", null, or "nope", we should consume a stream of multiple such strings • Output should be a publisher which produces a stream of results • Problem: • await only takes futures as arguments, but we need to await stream events! • async creates a future, but we need to create a stream publisher which yields multiple events instead of producing just one result We can’t use async/await! :-(
  16. scala-async + Flow = scala-async-flow Extension of async/await model which:

    • Provides a variant of async { … } which creates a stream publisher:
 rasync { … } • Generalizes await from futures to stream events: next, done, error • Provides additional methods for yielding events:
 yieldNext, yieldError, yieldDone
  17. Generalizing await • Previously: await the completion of futures
 def

    await[T](future: Future[T]): T • Now: await events produced by different asynchronous objects (Future[T], Flow.Publisher[T], etc.) trait Async[T] { def onComplete[S](handler: Try[T] => S) given (executor: ExecutionContext) def getCompleted: Try[T] }
  18. Solution using scala-async-flow val stream = rasync[String] { var dateOpt

    = await(dateStream) while (dateOpt.nonEmpty) { dateOpt.get match { case date(month, day) => yieldNext(s"It's ${await(nameOfMonth(month.toInt))}!") case _ => yieldNext("Not a date, mate!") } dateOpt = await(dateStream) } yieldDone() } Type of stream: Flow.Publisher[String] Here, we are awaiting a future!
  19. Implementing scala-async-flow Some alternatives: 1. Extension of scala-async macro for

    Scala 2.12 and 2.13 2. Direct compiler support 3. Build on top of continuations or fibers
  20. Library Component def rasync[T](body: given Flow[T] => T): Flow.Publisher[T] =

    { delegate for Flow[T] = new Flow[T] ... } def await[T, S](a: Async[T]) given (flow: Flow[S], executor: ExecutionContext): T def yieldNext[T](event: T) given (flow: Flow[T]) = { flow.yieldNext(event) } Contextual function type
  21. Library Component def rasync[T](body: given Flow[T] => T): Flow.Publisher[T] =

    { delegate for Flow[T] = new Flow[T] ... } def await[T, S](a: Async[T]) given (flow: Flow[S], executor: ExecutionContext): T def yieldNext[T](event: T) given (flow: Flow[T]) = { flow.yieldNext(event) } Contextual function type
  22. Library Component def rasync[T](body: given Flow[T] => T): Flow.Publisher[T] =

    { delegate for Flow[T] = new Flow[T] ... } def await[T, S](a: Async[T]) given (flow: Flow[S], executor: ExecutionContext): T def yieldNext[T](event: T) given (flow: Flow[T]) = { flow.yieldNext(event) } Contextual function type
  23. class StateMachine(flow: Flow[Int]) { var state = 0 val result

    = Promise[Int]() var x: Option[Int] = _ def apply(tr: Try[Int]): Unit = state match { case 0 => x = None val sub = flow.pubToSub(s) val completed = sub.getCompleted if (completed == null) { state = 2 sub.onComplete(evt => apply(evt)) } else if (completed.isFailure) { result.complete(completed) } else { x = completed.get state = 1 apply(null) } case 1 => result.complete(x.get) case 2 => if (tr.isFailure) { result.complete(tr) } else { x = tr.get state = 1 apply(null) } }} Generated state machine
  24. Alternative: what if we could extend the JVM? • OpenJDK

    Project Loom • Goal:
 JVM features for supporting lightweight, high-throughput concurrency constructs • Key features: • Delimited continuations • Fibers (“user-mode threads”) • Project under active development • Sponsored by OpenJDK HotSpot Group
  25. Continuations in Project Loom • A continuation turns a task

    into a suspendable computation • Suspend and resume <body> at yield points • Transfer values into and out of <body> using higher-level abstractions val cont = new Continuation(SCOPE, new Runnable { def run(): Unit = { <body> } })
  26. var continue = false val cont = new Continuation(SCOPE, new

    Runnable { def run(): Unit = { println("hello from continuation") while (!continue) { println("suspending") Continuation.`yield`(SCOPE) println("resuming") } println("all the way to the end") } }) cont.run() println("isDone: " + cont.isDone()) continue = true cont.run() println("isDone: " + cont.isDone()) hello from continuation suspending isDone: false resuming all the way to the end isDone: true Output: Example
  27. def rasync[T](body: given Flow[T] => T): Flow.Publisher[T] = { delegate

    flow for Flow[T] = new Flow[T] ... val cont = new Continuation(SCOPE, new Runnable { def run(): Unit = { try { val v = body flow.emitNext(v) flow.emitComplete() } catch { case NonFatal(error) => flow.emitError(error) } } }) ... flow }
  28. def await[T, S](a: Async[T]) given (flow: Flow[S], executor: ExecutionContext): T

    = { val res = a.getCompleted if (res eq null) { a.onComplete(x => flow.resume(x)) flow.suspend() a.getCompleted.get } else { res.get } }
  29. Theoretical foundations Paper:
 P. Haller and H. Miller: A reduction

    semantics for direct-style asynchronous observables.
 Journal of Logical and Algebraic Methods in Programming 105 (2019) 75–111
 https://doi.org/10.1016/j.jlamp.2019.03.002 • Formalization of programming model • Type soundness proof • Proof of protocol conformance • Describes macro-based implementation
  30. Open Issues • “Blue and red functions” problem • Calling

    a suspendible method (given Flow[T]) requires the caller to be suspendible, too • Need two variants for each higher-order function: regular and suspendible • Loom continuations don’t suffer from that problem; can suspend as long as there is an active continuation • Relaxing need for Flow[T] capabilities may be feasible
  31. Resources • Prototype implementation:
 https://github.com/phaller/scala-async-flow
 Branch “cont” makes use of

    Project Loom continuations and Scala 3 • Papers and more:
 http://www.csc.kth.se/~phaller/ • Twitter: @philippkhaller Thank you!