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

Compositional Streaming with FS2

Compositional Streaming with FS2

In recent years, a number of open source Scala libraries have appeared that support working with data streams. In this talk, we’ll look at Functional Streams for Scala (FS2), the library formerly known as Scalaz Stream, and explore its unique take on stream processing. We’ll look at working with impure data sources in a pure world, data transformations, and patterns for stream based program design.

Presented at Scalae By The Bay 2016 in San Francisco on November 13, 2016.

Michael Pilquist

November 13, 2016
Tweet

More Decks by Michael Pilquist

Other Decks in Technology

Transcript

  1. scalaz-stream => fs2 • Dependencies: scalaz & scodec-bits • Actively

    maintained! • scalaz 7.{1,2,3} • scala 2.{10,11,12} 3 libraryDependencies += "org.scalaz.stream" %% "scalaz-stream" % "0.8.6" libraryDependencies += "co.fs2" %% "fs2-core" % "0.9.2" • No dependencies • Supports Scala.js • Bindings available for Scalaz and Cats • Significantly improved API • Smaller core interpreter • Correct • More parametricity https://github.com/functional-streams-for-scala/fs2 series/0.8 series/0.9
  2. Pure Streams 5 scala> val s = Stream(1, 2, 3)

    s: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(1, 2, 3)))
  3. Pure Streams 6 scala> val s = Stream(1, 2, 3)

    s: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(1, 2, 3))) scala> val xs = (s  s.map(_*10)).toList xs: List[Int] = List(1, 2, 3, 10, 20, 30)
  4. Pure Streams 7 scala> val s = Stream(1, 2, 3)

    s: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(1, 2, 3))) scala> val xs = (s  s.map(_*10)).toList xs: List[Int] = List(1, 2, 3, 10, 20, 30) scala> val ys = s.intersperse(0).toList ys: List[Int] = List(1, 0, 2, 0, 3)
  5. Pure Streams 8 scala> val s = Stream(1, 2, 3)

    s: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(1, 2, 3))) scala> val xs = (s  s.map(_*10)).toList xs: List[Int] = List(1, 2, 3, 10, 20, 30) scala> val ys = s.intersperse(0).toList ys: List[Int] = List(1, 0, 2, 0, 3) toList is only available on pure streams
  6. Pure Streams 9 scala> val fibs: Stream[Nothing, Int] = Stream(0,

    1)  fibs.zipWith(fibs.tail)(_ + _) fibs: fs2.Stream[Nothing,Int] =  scala> fibs.take(10).toList // CAUTION: *NOT* Memoized! res0: List[Int] = List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
  7. Effectful Streams 11 val sample: Task[Sample] = Task.delay { sensor.read()

    } val samples: Stream[Task, Sample] = Stream.eval(sample).repeat
  8. Effectful Streams 12 val sample: Task[Sample] = Task.delay { sensor.read()

    } val samples: Stream[Task, Sample] = Stream.eval(sample).repeat val firstTen: Task[Vector[Sample]] = samples.take(10).runLog
  9. Effectful Streams 13 val sample: Task[Sample] = Task.delay { sensor.read()

    } val samples: Stream[Task, Sample] = Stream.eval(sample).repeat val firstTen: Task[Vector[Sample]] = samples.take(10).runLog val result: Vector[Sample] = firstTen.unsafeRun()
  10. Running Effectful Streams 14 implicit class StreamRunOps[F[_]: Catchable, O]( val

    self: Stream[F,O] ) { def run: F[Unit] def runLog: F[Vector[O]] def runLast: F[Option[O]] def runFold[A](z: A)(f: (A,O)  A): F[A] }
  11. Pipe Examples 16 object text { def utf8Decode[F[_]]: Pipe[F, Byte,

    String] = ??? def lines[F[_]]: Pipe[F, String, String] = ??? } val linesOfFile: Stream[Task, String] = io.file.readAll[Task](myFile, 4096). through(text.utf8Decode). through(text.lines)
  12. Pipe2 Examples 20 val x = Stream(1, 2, 3) val

    y = Stream(4, 5, 6) val z = x interleave y val elems = z.toList // elems: List[String] = List(1, 4, 2, 5, 3, 6)
  13. Pipe2 Examples 21 val x = Stream(1, 2, 3) val

    y = Stream(4, 5, 6) val z = x interleave y // alternatively x.pure.through2(y)(pipe2.interleave) val elems = z.toList // elems: List[String] = List(1, 4, 2, 5, 3, 6)
  14. Sink Examples 25 val outputFile: Sink[Task, Byte] = io.file.writeAll[Task](myFile) val

    toFile: Stream[Task, Unit] = lines.to(outputFile) val prg: Task[Unit] = toFile.run
  15. Sink Examples 26 val outputFile: Sink[Task, Byte] = io.file.writeAll[Task](myFile) val

    toFile: Stream[Task, Unit] = lines.to(outputFile) val prg: Task[Unit] = toFile.run prg.unsafeRun()
  16. Sink Examples: Writing events to Kafka 27 val kafkaTopic: Sink[Task,

    Record] = Producer.sink(Topic("foo"), kafkaSettings)
  17. Sink Examples: Writing events to Kafka 28 val kafkaTopic: Sink[Task,

    Record] = Producer.sink(Topic("foo"), kafkaSettings) val events: Stream[Task, Event] = ???
  18. Sink Examples: Writing events to Kafka 29 val kafkaTopic: Sink[Task,

    Record] = Producer.sink(Topic("foo"), kafkaSettings) val events: Stream[Task, Event] = ??? val program: Stream[Task, Unit] = events.map(eventToRecord).to(kafkaTopic)
  19. Sink Examples: Writing events to Kafka 30 val kafkaTopic: Sink[Task,

    Record] = Producer.sink(Topic("foo"), kafkaSettings) val events: Stream[Task, Event] = ??? val program: Stream[Task, Unit] = events.map(eventToRecord).to(kafkaTopic) program.run.unsafeRun()
  20. Composing Stream Transformations 33 type Pipe[F[_],-I,+O] = Stream[F,I]  Stream[F,O]

    type Pipe2[F[_],-I,-I2,+O] = (Stream[F,I], Stream[F,I2])  Stream[F,O] Do pipes compose as well as functions?
  21. Composing Stream Transformations 34 type Pipe[F[_],-I,+O] = Stream[F,I]  Stream[F,O]

    type Pipe2[F[_],-I,-I2,+O] = (Stream[F,I], Stream[F,I2])  Stream[F,O] type Sink[F[_],-I] = Pipe[F,I,Unit] Do pipes compose as well as functions?
  22. Composing Stream Transformations 35 type Pipe[F[_],-I,+O] = Stream[F,I]  Stream[F,O]

    type Pipe2[F[_],-I,-I2,+O] = (Stream[F,I], Stream[F,I2])  Stream[F,O] type Sink[F[_],-I] = Pipe[F,I,Unit] // Function composition! src.through(text.utfDecode andThen text.lines) Do pipes compose as well as functions?
  23. README 36 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096).
  24. README 37 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096). through(text.utf8Decode andThen text.lines).
  25. README 38 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096). through(text.utf8Decode andThen text.lines). filter(s  !s.trim.isEmpty && !s.startsWith("//")).
  26. README 39 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096). through(text.utf8Decode andThen text.lines). filter(s  !s.trim.isEmpty && !s.startsWith("//")). map(line  fahrenheitToCelsius(line.toDouble).toString).
  27. README 40 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096). through(text.utf8Decode andThen text.lines). filter(s  !s.trim.isEmpty && !s.startsWith("//")). map(line  fahrenheitToCelsius(line.toDouble).toString). intersperse("\n").
  28. README 41 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096). through(text.utf8Decode andThen text.lines). filter(s  !s.trim.isEmpty && !s.startsWith("//")). map(line  fahrenheitToCelsius(line.toDouble).toString). intersperse("\n"). through(text.utf8Encode).
  29. README 42 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096). through(text.utf8Decode andThen text.lines). filter(s  !s.trim.isEmpty && !s.startsWith("//")). map(line  fahrenheitToCelsius(line.toDouble).toString). intersperse("\n"). through(text.utf8Encode). to(io.file.writeAll(Paths.get("celsius.txt"))).
  30. README 43 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) val converter: Task[Unit] = io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096). through(text.utf8Decode andThen text.lines). filter(s  !s.trim.isEmpty && !s.startsWith("//")). map(line  fahrenheitToCelsius(line.toDouble).toString). intersperse("\n"). through(text.utf8Encode). to(io.file.writeAll(Paths.get("celsius.txt"))). run
  31. README 44 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) val converter: Task[Unit] = io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096). through(text.utf8Decode andThen text.lines). filter(s  !s.trim.isEmpty && !s.startsWith("//")). map(line  fahrenheitToCelsius(line.toDouble).toString). intersperse("\n"). through(text.utf8Encode). to(io.file.writeAll(Paths.get("celsius.txt"))). run converter.unsafeRun()
  32. README 45 import fs2.{io, text, Task}, java.nio.file.Paths def fahrenheitToCelsius(f: Double):

    Double = (f - 32.0) * (5.0/9.0) val converter: Task[Unit] = io.file.readAll[Task](Paths.get("fahrenheit.txt"), 4096). through(text.utf8Decode andThen text.lines). filter(s  !s.trim.isEmpty && !s.startsWith("//")). map(line  fahrenheitToCelsius(line.toDouble).toString). intersperse("\n"). through(text.utf8Encode). to(io.file.writeAll(Paths.get("celsius.txt"))). run converter.unsafeRun() • Input file is read & output file is written in a streamy fashion • Constant memory usage • Errors are handled • I/O errors • Exceptions thrown by function passed to map • File handles are acquired when necessary and are guaranteed to be released
  33. Resource Allocation 46 def bracket[F[_],R,A](acquire: F[R])( use: R  Stream[F,A],

    release: R  F[Unit] ): Stream[F,A] • Evaluates acquire to produce a resource • Passes resource to use to generate a stream • Decorates the stream returned from use so that the resource is released when the inner stream terminates • Stream termination occurs when: • Natural end of stream is reached • Puller of stream decides to stop pulling (e.g., take(5)) • Unhandled error occurs
  34. Pulls and Handles 47 Stream[F, A] Pull[F, A, R] open

    close • Closing a pull results in a stream • Managed resources acquired via Pull.acquire • Pull establishes a "scope" in which managed resources are tracked • Pull forms a monad in R • Pull[F, A, R] can: • evaluate effects of type F • output values of type A • pass a resource of type R
  35. Pulls and Handles 48 def open: Pull[F, Nothing, Handle[F,O]] =

    ??? Allows us to pull elements from the stream
  36. Pulls and Handles 48 def open: Pull[F, Nothing, Handle[F,O]] =

    ??? Allows us to pull elements from the stream src.open.flatMap { h  h.receive1 { (o, _)  Pull.output1(o) } }.close head
  37. Pulls and Handles 48 def open: Pull[F, Nothing, Handle[F,O]] =

    ??? Allows us to pull elements from the stream src.open.flatMap { h  h.receive1 { (o, _)  Pull.output1(o) } }.close head src.open.flatMap { h  h.receive1 { (_, h)  h.echo } }.close tail
  38. Pulls and Handles 49 src.open.flatMap { h  h.receive1 {

    (o, _)  Pull.output1(o) } }.close src.open.flatMap { h  h.receive1 { (_, h)  h.echo } }.close src.open.flatMap(f).close src.pull(f)
  39. Recursive Pulls 51 def take[F[_], A](count: Int): Pipe[F, A, A]

    = { def loop(remaining: Int): Handle[F, A]  Pull[F, A, Unit] = h  { } src  src.pull(loop(count)) }
  40. Recursive Pulls 52 def take[F[_], A](count: Int): Pipe[F, A, A]

    = { def loop(remaining: Int): Handle[F, A]  Pull[F, A, Unit] = h  { if (remaining <= 0) { Pull.done } else { } } src  src.pull(loop(count)) }
  41. Recursive Pulls 53 def take[F[_], A](count: Int): Pipe[F, A, A]

    = { def loop(remaining: Int): Handle[F, A]  Pull[F, A, Unit] = h  { if (remaining <= 0) { Pull.done } else { h.receive1 { (a, h)  Pull.output1(a)  loop(remaining - 1)(h) } } } src  src.pull(loop(count)) }
  42. Recursive Pulls 54 def take[F[_], A](count: Int): Pipe[F, A, A]

    = { def loop(remaining: Int): Handle[F, A]  Pull[F, A, Unit] = h  { if (remaining <= 0) { Pull.done } else { h.receive1 { (a, h)  Pull.output1(a)  loop(remaining - 1)(h) } } } src  src.pull(loop(count)) } • Recursive pulls model state machines • State stored as params to recursive call • Multiple states can be represented as multiple co- recursive functions
  43. Interruption X val src: Stream[Task, A] = ??? import fs2.async.mutable.Signal

    val mkCancel: Task[Signal[Task, Boolean]] = async.signalOf[Task, Boolean](false)
  44. Interruption X val src: Stream[Task, A] = ??? import fs2.async.mutable.Signal

    val mkCancel: Task[Signal[Task, Boolean]] = async.signalOf[Task, Boolean](false) val interruptibleSource: Stream[Task, A] = Stream.eval(mkCancel).flatMap { cancel  // Somewhere in here, call cancel.set(true) src.interruptWhen(cancel) }
  45. Asynchronous Buffering 56 def bufferAsync[F[_], A]( bufferSize: Int)(implicit F: Async[F]

    ): Pipe[F, A, A] = source  { val mkQueue: F[Queue[F, Attempt[Option[Chunk[A]]]]] = async.boundedQueue(bufferSize) }
  46. Asynchronous Buffering 57 def bufferAsync[F[_], A]( bufferSize: Int)(implicit F: Async[F]

    ): Pipe[F, A, A] = source  { val mkQueue: F[Queue[F, Attempt[Option[Chunk[A]]]]] = async.boundedQueue(bufferSize) Stream.eval(mkQueue).flatMap { queue  } }
  47. Asynchronous Buffering 58 def bufferAsync[F[_], A]( bufferSize: Int)(implicit F: Async[F]

    ): Pipe[F, A, A] = source  { val mkQueue: F[Queue[F, Attempt[Option[Chunk[A]]]]] = async.boundedQueue(bufferSize) Stream.eval(mkQueue).flatMap { queue  val fill: Stream[F, Nothing] = source.chunks.noneTerminate.attempt.to(queue.enqueue).drain } }
  48. Asynchronous Buffering 59 def bufferAsync[F[_], A]( bufferSize: Int)(implicit F: Async[F]

    ): Pipe[F, A, A] = source  { val mkQueue: F[Queue[F, Attempt[Option[Chunk[A]]]]] = async.boundedQueue(bufferSize) Stream.eval(mkQueue).flatMap { queue  val fill: Stream[F, Nothing] = source.chunks.noneTerminate.attempt.to(queue.enqueue).drain val out: Stream[F, A] = queue.dequeue.through(pipe.rethrow).unNoneTerminate. flatMap(Stream.chunk) } }
  49. Asynchronous Buffering 60 def bufferAsync[F[_], A]( bufferSize: Int)(implicit F: Async[F]

    ): Pipe[F, A, A] = source  { val mkQueue: F[Queue[F, Attempt[Option[Chunk[A]]]]] = async.boundedQueue(bufferSize) Stream.eval(mkQueue).flatMap { queue  val fill: Stream[F, Nothing] = source.chunks.noneTerminate.attempt.to(queue.enqueue).drain val out: Stream[F, A] = queue.dequeue.through(pipe.rethrow).unNoneTerminate. flatMap(Stream.chunk) fill merge out } }
  50. Concurrency X object concurrent { def join[F[_]: Async, O]( maxOpen:

    Int)(outer: Stream[F,Stream[F,O]]): Stream[F,O] = ??? }
  51. Concurrency X object concurrent { def join[F[_]: Async, O]( maxOpen:

    Int)(outer: Stream[F,Stream[F,O]]): Stream[F,O] = ??? } def evalMapConcurrent[F[_]: Async, A, B]( maxOpen: Int)(f: A  F[B]): Pipe[F, A, B] = src  ???
  52. Concurrency X object concurrent { def join[F[_]: Async, O]( maxOpen:

    Int)(outer: Stream[F,Stream[F,O]]): Stream[F,O] = ??? } def evalMapConcurrent[F[_]: Async, A, B]( maxOpen: Int)(f: A  F[B]): Pipe[F, A, B] = { src  concurrent.join(maxOpen)( src.map(a  Stream.eval(f(a)))) }
  53. Type Classes from fs2.util 62 Functor Applicative Monad Traverse Catchable

    • Tracks exceptions thrown during evaluation • Allows extraction of failure
  54. Type Classes from fs2.util 63 Functor Applicative Monad Traverse Catchable

    Suspendable • Supports deferred evaluation • Provides suspend and delay methods
  55. Type Classes from fs2.util 64 Functor Applicative Monad Traverse Catchable

    Suspendable Effect • Provides mechanism which evaluates an F[A] • Callback based unsafeRunAsync
  56. Type Classes from fs2.util 65 Functor Applicative Monad Traverse Catchable

    Suspendable Effect Async • Effect which support asynchronous refs • Async.Ref[F, A] provides a "memory cell" that supports a variant of compare-and-set • All async primitives (e.g., semaphore, signal, queue) built with Refs
  57. So what? • Combinators expressed polymorphically in effect type •

    Easier to reason about what a method does when minimum type class constraint is made • Multitude of Tasks • fs2, scalaz, Monix • Multitude of type constructors • scodec-stream's Cursor effect • Task + custom behaviors (e.g., TraceTask) 66
  58. Use Case: Network Analysis Tool 67 Capture Source Recorder Rules

    Rules Rules UI Elastic Search Monitoring pcap Decoder Stream[Task, TimeStamped[Option[A]]] Pipe[Task, TimeStamped[Option[A]], TimeStamped[Option[A]]]
  59. Use Case: Network Analysis Tool 68 Capture Source Recorder Rules

    Rules Rules UI Elastic Search Monitoring pcap Decoder Supports capturing via: • UDP datagrams sent directly to app • UDP or TCP monitoring via libpcap • Capture from coaxial RF • Playback of pcap file
  60. Use Case: Network Analysis Tool 69 Capture Source Recorder Rules

    Rules Rules UI Elastic Search Monitoring pcap Decoder Supports capturing via: • UDP datagrams sent directly to app • UDP or TCP monitoring via libpcap • Capture from coaxial RF • Playback of pcap file udp.open[Task](addr).flatMap { socket  socket.reads().map { p  TimeStamped.now((port, p.bytes)) } }
  61. Use Case: Network Analysis Tool 70 Capture Source Recorder Rules

    Rules Rules UI Elastic Search Monitoring pcap Decoder Supports capturing via: • UDP datagrams sent directly to app • UDP or TCP monitoring via libpcap • Capture from coaxial RF • Playback of pcap file Stream.bracket(Pcap.open(…))( h  Stream.repeatEval(poll(h)), h  h.release )
  62. Use Case: Network Analysis Tool 71 Capture Source Recorder Rules

    Rules Rules UI Elastic Search Monitoring pcap Decoder Supports capturing via: • UDP datagrams sent directly to app • UDP or TCP monitoring via libpcap • Capture from coaxial RF • Playback of pcap file Stream.bracket(RfDevice.open(…))( h  udp.open[Task](…).flatMap(…), h  h.release )
  63. Use Case: Network Analysis Tool 72 Capture Source Recorder Rules

    Rules Rules UI Elastic Search Monitoring pcap Decoder Supports capturing via: • UDP datagrams sent directly to app • UDP or TCP monitoring via libpcap • Capture from coaxial RF • Playback of pcap file • Stream large files from disk • Filter inputs based on various criteria • Playback: • in realtime • multiplier of realtime • as fast as possible
  64. PCAP File Playback 73 val timestamped: Stream[Task, TimeStamped[Either[UnknownEtherType, B]]] =

    timestampedDecoder.decodeMmap(new FileInputStream(path).getChannel) val throttlingFactor: Option[Double] = ??? val throttled: Stream[Task, TimeStamped[Either[UnknownEtherType, B]]] = throttlingFactor.fold(timestamped) { factor  timestamped through throttled(factor) } val source: Stream[Task, TimeStamped[Option[Either[UnknownEtherType, B]]]] = throttled through interpolateTicks()
  65. PCAP File Playback 74 def throttle[F[_]: Async, A]( throttlingFactor: Double)(implicit

    scheduler: Scheduler ): Pipe[F, TimeStamped[A], TimeStamped[A]] = ???
  66. PCAP File Playback def throttle[F[_]: Async, A]( throttlingFactor: Double)(implicit scheduler:

    Scheduler ): Pipe[F, TimeStamped[A], TimeStamped[A]] = { val tickPeriod = 100.millis val ticks: Stream[F, Unit] = time.awakeEvery[F](tickPeriod).map(_  ()) source  (source through2 ticks)(throttleWithClock(tickPeriod)) }
  67. PCAP File Playback 76 def throttle[F[_]: Async, A]( throttlingFactor: Double)(implicit

    scheduler: Scheduler ): Pipe[F, TimeStamped[A], TimeStamped[A]] = { val tickPeriod = 100.millis val ticks: Stream[F, Unit] = time.awakeEvery[F](tickPeriod).map(_  ()) source  (source through2 ticks)(throttleWithClock(tickPeriod)) } awaitInput awaitTick input / items pending clock tick / nothing pending clock tick / output pending input / nothing pending
  68. 78 def throttleWithClock( tickPeriod: FiniteDuration ): Pipe2[F, TimeStamped[A], Unit, TimeStamped[A]]

    = { def awaitInput(upto: Instant): ??? = def awaitTick(upto: Instant, p: Chunk[TimeStamped[A]]): ??? = }
  69. 79 def throttleWithClock( tickPeriod: FiniteDuration ): Pipe2[F, TimeStamped[A], Unit, TimeStamped[A]]

    = { type PullFromSourceOrTicks = (Handle[F, TimeStamped[A]], Handle[F, Unit])  Pull[F, TimeStamped[A], (Handle[F, TimeStamped[A]], Handle[F, Unit])] def awaitInput(upto: Instant): PullFromSourceOrTicks = (src, ticks)  ??? def awaitTick(upto: Instant, p: Chunk[TimeStamped[A]]): PullFromSourceOrTicks = (src, ticks)  ??? }
  70. 80 def throttleWithClock( tickPeriod: FiniteDuration ): Pipe2[F, TimeStamped[A], Unit, TimeStamped[A]]

    = { type PullFromSourceOrTicks = (Handle[F, TimeStamped[A]], Handle[F, Unit])  Pull[F, TimeStamped[A], (Handle[F, TimeStamped[A]], Handle[F, Unit])] def awaitInput(upto: Instant): PullFromSourceOrTicks = (src, ticks)  src.receive { (chunk, tl)  ??? } def awaitTick(upto: Instant, p: Chunk[TimeStamped[A]]): PullFromSourceOrTicks = (src, ticks)  ??? }
  71. 81 def throttleWithClock( tickPeriod: FiniteDuration ): Pipe2[F, TimeStamped[A], Unit, TimeStamped[A]]

    = { type PullFromSourceOrTicks = (Handle[F, TimeStamped[A]], Handle[F, Unit])  Pull[F, TimeStamped[A], (Handle[F, TimeStamped[A]], Handle[F, Unit])] def awaitInput(upto: Instant): PullFromSourceOrTicks = (src, ticks)  src.receive { (chunk, tl)  ??? } def awaitTick(upto: Instant, p: Chunk[TimeStamped[A]]): PullFromSourceOrTicks = (src, ticks)  ticks.receive1 { (tick, tl)  ??? } }
  72. 82 def throttleWithClock( tickPeriod: FiniteDuration ): Pipe2[F, TimeStamped[A], Unit, TimeStamped[A]]

    = { type PullFromSourceOrTicks = (Handle[F, TimeStamped[A]], Handle[F, Unit])  Pull[F, TimeStamped[A], (Handle[F, TimeStamped[A]], Handle[F, Unit])] def awaitInput(upto: Instant): PullFromSourceOrTicks = (src, ticks)  src.receive { (chunk, tl)  ??? } def awaitTick(upto: Instant, p: Chunk[TimeStamped[A]]): PullFromSourceOrTicks = (src, ticks)  ticks.receive1 { (tick, tl)  ??? } _.pull2(_) { (src, ticks)  src.receive1 { (ta, tl)  Pull.output1(ta)  awaitInput(ta.time)(tl, ticks) }} }
  73. Compositional Streaming with FS2 • 0.9.2 available now for Scala

    2.11 & 2.12 • 1.0.0 in progress — source compatible with 0.9 • Vibrant Ecosystem • Interop: scalaz-stream, akka-stream, cats, scalaz • Infrastructure: doobie, http4s, kafka • Contact me via Gitter.im chat, Github issue tracker, or @mpilquist 83