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

Asynchronous streams in direct style with and without macros

3b84657fdb075382e3781310ca8a9a70?s=47 Philipp Haller
June 13, 2019
220

Asynchronous streams in direct style with and without macros

3b84657fdb075382e3781310ca8a9a70?s=128

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. Macro-based implementation • Example program: def fwd(s: Flow.Publisher[Int]) = rasync

    { var x: Option[Int] = None x = await(s) x.get }
  24. 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
  25. 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
  26. 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> } })
  27. 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
  28. 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 }
  29. 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 } }
  30. 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
  31. 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
  32. 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!