Slide 1

Slide 1 text

Asynchronous streams in direct style with and without macros Philipp Haller KTH Royal Institute of Technology Stockholm, Sweden

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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 !" where 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

Slide 4

Slide 4 text

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?

Slide 5

Slide 5 text

Enter scala-async • Purpose:
 Simplify asynchronous programming primarily based on futures. • Created by Jason Zaugg (Lightbend) and yours truly, first released in 2013

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Publisher • A Flow.Publisher produces events of type T which are received by subscribers if they have expressed interest. public static interface Flow.Publisher { void subscribe(Flow.Subscriber subscriber); }

Slide 10

Slide 10 text

Subscriber • A Flow.Subscriber 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 { void onSubscribe(Flow.Subscription subscription); void onNext(T item); void onError(Throwable throwable); void onComplete(); }

Slide 11

Slide 11 text

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(); }

Slide 12

Slide 12 text

Reactive Extensions • Libraries building on basic interfaces for publishers, subscribers etc. • Key: higher-order functions for composing publishers • Implementations for many languages available

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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! :-(

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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] }

Slide 18

Slide 18 text

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!

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Macro-based implementation • Example program: def fwd(s: Flow.Publisher[Int]) = rasync { var x: Option[Int] = None x = await(s) x.get }

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Continuations in Project Loom • A continuation turns a task into a suspendable computation • Suspend and resume at yield points • Transfer values into and out of using higher-level abstractions val cont = new Continuation(SCOPE, new Runnable { def run(): Unit = { } })

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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 }

Slide 29

Slide 29 text

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 } }

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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!