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

FS3: Evolving a Streaming Platform

FS3: Evolving a Streaming Platform

The Functional Streams for Scala (FS2) library provides core infrastructure for many critical components of the open source Scala ecosystem, including libraries like http4s and doobie. In this talk, we’ll look at a possible future direction for FS2 (code named FS3), focused on replacing the internal pseudo-Free interpreter with a more direct encoding. We’ll also look at how various API decisions constrain the solution space. Attendees will leave with a better understanding of the FS2 API and hopefully, a few will be inspired to help work on FS3.

Michael Pilquist

April 02, 2019
Tweet

More Decks by Michael Pilquist

Other Decks in Technology

Transcript

  1. FS3: Evolving a Streaming Platform
    Michael Pilquist // @mpilquist

    View Slide

  2. View Slide

  3. View Slide

  4. FS2 in 2019
    • Comprehensible
    • Compositional
    • Declarative
    • Expressive
    • Efficient
    • Foundational
    4

    View Slide

  5. Control Flow Safari
    5
    // On Stream[F, O]:
    def metered[F2[x] >: F[x]: Timer](
    rate: FiniteDuration
    ): Stream[F2, O] =
    Stream.fixedRate[F2](rate).zipRight(this)

    View Slide

  6. Control Flow Safari
    6
    Stream.eval(Queue.bounded[IO, Event](max)).flatMap { q 
    val producer: Stream[IO, Unit] = source.through(q.enqueue)
    val consumer: Stream[IO, Event] = q.dequeue
    consumer.concurrently(producer)
    }

    View Slide

  7. Control Flow Safari
    7
    def prefetchN[F2[x] >: F[x]: Concurrent](
    n: Int
    ): Stream[F2, O] =
    Stream.eval(Queue.bounded[F2, Option[Chunk[O]]](n))
    .flatMap { queue 
    val producer = chunks.noneTerminate
    .covary[F2].through(queue.enqueue)
    queue.dequeue.unNoneTerminate
    .flatMap(Stream.chunk(_))
    .concurrently(producer)
    }

    View Slide

  8. Control Flow Safari
    8
    val program: IO[Unit] =
    source
    .balance(maxElements)
    .take(numWorkers)
    .map(_.through(doWork))
    .parJoinUnbounded
    .compile
    .drain

    View Slide

  9. Stream Compilation
    9
    Stream[F, O]  G[R]

    View Slide

  10. Pure Stream Compilation
    10
    Input Output Compiler Operation
    Stream[Pure, O] List[O]
    Vector[O]
    Chunk[O]
    C[O]
    toList
    toVector
    toChunk
    to(C)
    Stream[Pure, String] String string
    Stream[Pure, O] Option[O] last

    foldSemigroup
    Stream[Pure, O] O foldMonoid

    View Slide

  11. Effectful Stream Compilation
    11
    Input Output Compiler Operation
    Stream[F, O] F[List[O]]
    F[Vector[O]]
    F[Chunk[O]]
    F[C[O]]
    toList
    toVector
    toChunk
    to(C)
    Stream[F, String] F[String] string
    Stream[F, O] F[Option[O]] last

    foldSemigroup
    Stream[F, O] F[O] lastOrError
    foldMonoid

    View Slide

  12. Stream Compilation
    12
    trait Compiler[F[_], G[_]] {
    def apply[O, S, R](
    s: Stream[F, O],
    init: ()  S,
    accumulate: (S, Chunk[O])  S,
    finalize: S  R
    ): G[R]
    }

    View Slide

  13. Stream & Pull in 1.0
    13
    Algebra[F[_], +O, R]
    Output
    Step
    Eval
    Acquire
    OpenScope
    CloseScope
    GetScope
    Stream[+F[_], +O]
    Pull[+F[_], +O, +R]
    FreeC[Algebra[F, O, ?], R]]
    FreeC[Algebra[F, O, ?], Unit]]
    FreeC[F[_], +R]
    Pure
    Fail
    Interrupted
    Eval
    Bind

    View Slide

  14. FS3: Goals & Constraints
    • Goals
    • Take advantage of more powerful effect types (and type classes)
    • Improve readability of internals by replacing FreeC + Algebra with a more direct encoding
    • Non-negotiables
    • Polymorphic in effect type
    • Single API for both pure & effectful streams
    • Recursive Pull API
    • Stream based resource management
    • Control flow
    14

    View Slide

  15. FS3: Goals & Constraints
    • Non-negotiables
    • Polymorphic in effect type
    • Single API for both pure & effectful streams
    • Recursive Pull API
    • Stream based resource management
    • Control flow
    15
    Stream[IO, A]
    Stream[Pure, A]
    Stream[Id, A]
    Stream[Fallible, A]
    Stream[ConnectionIO, A]
    Stream[Free[Domain, ?], A]
    Stream[Task, A]
    Stream[UIO, A]

    View Slide

  16. FS3: Goals & Constraints
    • Non-negotiables
    • Polymorphic in effect type
    • Single API for both pure & effectful streams
    • Recursive Pull API
    • Stream based resource management
    • Control flow
    16
    val src: Stream[Pure, Int] = ???
    val result: String = src
    .map(_.toString)
    .intersperse("\n")
    .compile
    .string
    val src2: Stream[IO, Int] = ???
    val result2: IO[String] = src
    .map(_.toString)
    .intersperse("\n")
    .compile
    .string

    View Slide

  17. FS3: Goals & Constraints
    • Non-negotiables
    • Polymorphic in effect type
    • Single API for both pure & effectful streams
    • Recursive Pull API
    • Stream based resource management
    • Control flow
    17
    def concatLines[F[_], G[_]](s: Stream[F, Int])(
    implicit c: Stream.Compiler[F, G]
    ): G[String] =
    s.map(_.toString).intersperse("\n").compile.string

    View Slide

  18. FS3: Goals & Constraints - Recursive Pull API
    18
    def mapFilter[A, B](
    s: Stream[F, A])(f: A  Option[B]
    ): Stream[F, B] = {
    def pull(s: Stream[F, A]): Pull[F, B, Unit] =
    s.pull.uncons.flatMap {
    case None  Pull.done
    case Some((chunk, rest)) 
    Pull.output(chunk.mapFilter(f))  pull(rest)
    }
    pull(s).stream
    }

    View Slide

  19. FS3: Goals & Constraints - Stream Based Resource Management
    19
    val acquire: F[FileHandle] = ???
    def release(fh: FileHandle): F[FileHandle] = ???
    val s: Stream[F, FileHandle] =
    Stream.bracket(acquire)(release)
    s.flatMap { fh 
    // Safe to use fh here!
    }
    val fileResource: Resource[F, FileHandle] = ???
    val s2: Stream[F, FileHandle] = Stream.resource(fileResource)

    View Slide

  20. FS3: Goals & Constraints - Control Flow
    20
    s.interruptAfter(10.seconds)
     s2
    • Non-negotiables
    • Polymorphic in effect type
    • Single API for both pure & effectful streams
    • Recursive Pull API
    • Stream based resource management
    • Control flow

    View Slide

  21. Pull Redesigned
    21
    trait Pull[F[_], O, R] {
    def compile[S](
    init: S,
    accumulate: (S, Chunk[O])  S
    )(implicit F: Sync[F]): F[(S, Option[R])]
    }

    View Slide

  22. Pull Redesigned
    22
    trait Pull[+F[_], +O, +R] {
    def compile[F2[x] >: F[x]: Sync, R2 >: R, S](
    initial: S,
    accumulate: (S, Chunk[O])  S
    ): F2[(S, Option[R2])]
    }

    View Slide

  23. Pull Redesigned
    23
    object Pull {
    def pure[F[_], R](r: R): Pull[F, INothing, R] =
    new Pull[F, INothing, R] {
    def compile[F2[x] >: F[x], R2 >: R, S](
    initial: S,
    acc: (S, Chunk[INothing])  S
    )(implicit F: Sync[F2]): F2[(S, Option[R2])] =
    (initial, Some(r): Option[R2]).pure[F2]
    }
    }

    View Slide

  24. Pull Redesigned
    24
    object Pull {
    def output[F[_], O](os: Chunk[O]): Pull[F, O, Unit] =
    new Pull[F, O, Unit] {
    def compile[F2[x] >: F[x], R2 >: Unit, S](
    initial: S,
    acc: (S, Chunk[O])  S
    )(implicit F: Sync[F2]): F2[(S, Option[R2])] =
    (acc(initial, os), Some(()): Option[R2]).pure[F2]
    }
    }

    View Slide

  25. Pull Redesigned
    25
    object Pull {
    def eval[F[_], R](fr: F[R]): Pull[F, INothing, R] =
    new Pull[F, INothing, R] {
    def compile[F2[x] >: F[x], R2 >: R, S](
    initial: S,
    acc: (S, Chunk[INothing])  S
    )(implicit F: Sync[F2]): F2[(S, Option[R2])] =
    (fr: F2[R]).map(r  (initial, Some(r): Option[R2]))
    }
    }

    View Slide

  26. Pull Redesigned
    26
    object Pull {
    def acquire[F[_], R](
    resource: F[R])(
    release: R  F[Unit]
    ): Pull[F, INothing, R] =
    new Pull[F, INothing, R] {
    def compile[F2[x] >: F[x], R2 >: R, S](
    initial: S,
    acc: (S, Chunk[INothing])  S
    )(implicit F: Sync[F2]): F2[(S, Option[R2])] =
    ???
    }

    View Slide

  27. Pull Redesigned
    27
    trait Pull[+F[_], +O, +R] {
    protected def step[F2[x] >: F[x]: Sync, O2 >: O, R2 >: R](
    scope: Scope[F2]): F2[StepResult[F2, O2, R2]]
    }
    case class Done[F[_], O, R](
    result: R
    ) extends StepResult[F, O, R]
    case class Output[F[_], O, R](
    scope: Scope[F],
    head: Chunk[O],
    tail: Pull[F, O, R]
    ) extends StepResult[F, O, R]

    View Slide

  28. Stream & Pull in FS3
    28
    Stream[+F[_], +O]
    Pull[+F[_], +O, +R]
    Pull[F, O, Unit] Output
    Result
    Fail
    FlatMap
    FlatMapOutput
    HandleErrorWith
    MapOutput
    Scope
    InterruptScope
    StepWith
    Uncons
    Acquire
    Eval
    GetScope

    View Slide

  29. What’s left?
    29
    ~2.522%
    “Don’t be frustrated. It was always like this with the interruption
    implementation. Always when you think you have it, something pops out
    and sometimes this implies full rework.” - Pavel Chlupacek (paraphrased)


    https://github.com/functional-streams-for-scala/fs2/issues/1142#issuecomment-469990603
    Primarily interruption edge cases

    View Slide

  30. Space Program Effects
    • Dead code elimination
    • Simplified scope closure semantics
    • Pull equivalent of -Ywarn-value-discard

    p.stream can now only be called when the result type of p is Unit

    Pull[F, O, R]  Stream[F, O] 

    Pull[F, O, Unit]  Stream[F, O]
    • New test suite that works on both JVM and Javascript
    • Replaced ScalaCheck with new ScalaTest generators
    • Property tests can return asynchronous IO values
    30

    View Slide

  31. New Test Suite
    31
    "interrupt" - {
    "can interrupt a hung eval" in {
    forAll { (s: Stream[Pure, Int]) 
    Stream
    .eval(Semaphore[IO](0))
    .flatMap { semaphore 
    s.covary[IO].evalMap(_  semaphore.acquire)

    .interruptAfter(50.millis)
    }
    .compile
    .toList
    .asserting(_ shouldBe Nil)
    }
    }

    View Slide

  32. Takeaways
    • FS3 prototype looks promising
    • Future work
    • Fix pending tests
    • Remove Sync constraint from compilation
    • Investigate alternative designs for interruption and scope management
    • Decide fate of prototype
    • We need your help! If interested, please contact us!
    • More generally
    • Celebrate mature software
    • Don’t be afraid to experiment
    32

    View Slide