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.

C9ab1175a6981a2f67ce8d08aa17c15a?s=128

Michael Pilquist

April 02, 2019
Tweet

Transcript

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

  2. None
  3. None
  4. FS2 in 2019 • Comprehensible • Compositional • Declarative •

    Expressive • Efficient • Foundational 4
  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)
  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) }
  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) }
  8. Control Flow Safari 8 val program: IO[Unit] = source .balance(maxElements)

    .take(numWorkers) .map(_.through(doWork)) .parJoinUnbounded .compile .drain
  9. Stream Compilation 9 Stream[F, O]  G[R]

  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
  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
  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] }
  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
  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
  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]
  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
  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
  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 }
  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)
  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
  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])] }
  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])] }
  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] } }
  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] } }
  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])) } }
  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])] = ??? }
  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]
  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
  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
  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
  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) } }
  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