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
320

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

    View Slide

  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

    View Slide

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

    View Slide

  4. A solution using for-
    comprehensions
    def solution() = {
    val date = """(\d+)/(\d+)""".r
    for {
    dayString response case date(month, day) =>
    for (name yield s"It's $name!"
    case _ =>
    Future.successful("Not a date, mate!")
    }
    } yield response
    }
    Can we improve
    readability?

    View Slide

  5. Enter scala-async
    • Purpose:

    Simplify asynchronous programming primarily based on futures.

    • Created by Jason Zaugg (Lightbend) and yours truly, first released in 2013

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  9. 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 super T> subscriber);
    }

    View Slide

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

    View Slide

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

    View Slide

  12. Reactive Extensions
    • Libraries building on basic interfaces for publishers, subscribers etc.

    • Key: higher-order functions for composing publishers

    • Implementations for many languages available

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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!

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  26. 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 = {

    }
    })

    View Slide

  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

    View Slide

  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
    }

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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!

    View Slide