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

A Tale of Two Monix Streams

Alexandru Nedelcu
May 17, 2018
420

A Tale of Two Monix Streams

Talk given at Scala Days 2018, in Berlin: https://eu.scaladays.org/lect-6938-a-tale-of-two-monix-streams.html

Monix started as a project exposing an idiomatic, opinionated and back-pressured ReactiveX implementation for Scala, but has grown beyond those boundaries to fully incorporate the lessons of functional programming.

I'm presenting a contrast between the Observable data type, which works with an underlying push-based and very efficient protocol and the new Iterant data type, a generic, purely functional, pull-based streaming alternative coming in Monix 3.0.

Besides outlining the direction of where Monix is going, the presentation is a lesson in functional programming design for FP enthusiasts.

Alexandru Nedelcu

May 17, 2018
Tweet

Transcript

  1. A TALE OF TWO
    MONIX STREAMS
    Alexandru Nedelcu

    @alexelcu | alexn.org | oriel.io

    View Slide

  2. MONIX
    WHAT IS MONIX?
    ▸ Scala / Scala.js library
    ▸ For composing asynchronous programs
    ▸ Exposes Observable, Task, Coeval,

    Iterant and many concurrency primitives
    ▸ Typelevel (see typelevel.org)
    ▸ 3.0.0-RC1
    ▸ See: monix.io
    2

    View Slide

  3. 3.0
    ▸ Deep integration with Typelevel Cats
    ▸ Iterant data type for lawful pull-based 

    streaming
    ▸ major improvements to 

    Observable, Task, Coeval 

    and CancelableFuture
    3
    MONIX

    View Slide

  4. MONIFU
    ▸ Started on January 2, 2014, at 2:30 a.m.
    ▸ Developed for monitoring and controlling power
    plants
    ▸ Inspired by RxJava / ReactiveX (link)
    ▸ Renamed to Monix on Dec 30, 2015

    (issue #91)
    ▸ Monix comes from: Monads + Rx
    4
    MONIX

    View Slide

  5. MONIFU
    ▸ Started on January 2, 2014, at 2:30 a.m.
    ▸ Developed for monitoring and controlling power
    plants
    ▸ Inspired by RxJava / ReactiveX (link)
    ▸ Renamed to Monix on Dec 30, 2015

    (issue #91)
    ▸ Monix comes from: Monads + Rx
    5
    MONIX

    View Slide

  6. MONIFU
    ▸ Started on January 2, 2014, at 2:30 a.m.
    ▸ Developed for monitoring and controlling power
    plants
    ▸ Inspired by RxJava / ReactiveX (link)
    ▸ Renamed to Monix on Dec 30, 2015

    (issue #91)
    ▸ Monix comes from: Monads + Rx
    6
    MONIX

    View Slide

  7. MONIFU
    ▸ Started on January 2, 2014
    ▸ Developed for monitoring and controlling power
    plants
    ▸ Inspired by RxJava / ReactiveX (link)
    ▸ Renamed to Monix on Dec 30, 2015

    (issue #91)
    ▸ Monix comes from: Monads + Rx
    7
    MONIX

    View Slide

  8. I’M A DEVELOPER, I HAVE NO LIFE
    8
    MONIX

    View Slide

  9. 9
    MONIX

    + =

    View Slide

  10. STREAMS

    View Slide

  11. OBSERVABLE[+A]
    PUSH - BASED STREAMING

    View Slide

  12. MONIX
    RX .NET - ORIGINS
    ▸ Reactive Extensions (also known as ReactiveX)
    ▸ The Observable pattern
    ▸ Built at Microsoft by
    ▸ Jeffery Van Gogh
    ▸ Wes Dyer
    ▸ Erik Meijer
    ▸ Bart De Smet
    12

    View Slide

  13. MONIX
    RX .NET - ORIGINS
    13
    trait Iterator[+A] {
    def hasNext: Boolean
    def next(): A
    }
    trait Iterable[+A] {
    def iterator: Iterator[A]
    }

    View Slide

  14. MONIX
    RX .NET - ORIGINS
    14
    trait Observable[+A] {
    def subscribe(o: Observer[A]): Cancelable
    }
    trait Observer[-A] {
    def onNext(elem: A): Unit
    def onComplete(): Unit
    def onError(e: Throwable): Unit
    }

    View Slide

  15. MONIX
    RX .NET - ORIGINS
    15

    View Slide

  16. OBSERVABLE IS THE DUAL OF
    ITERABLE
    Erik Meijer
    MONIX 16

    View Slide

  17. 17
    MONIX
    ▸Pure push - based
    ▸No protections against slow consumers
    ▸Unbounded buffers / throttling
    ▸Trivia: flatMap is aliased to mergeMap
    because concatMap is unsafe
    RX .NET - PROBLEMS

    View Slide

  18. 18
    MONIX
    ▸Pure push - based
    ▸No protections against slow consumers
    ▸Unbounded buffers / throttling
    ▸Trivia: flatMap is aliased to mergeMap
    because concatMap is unsafe
    RX .NET - PROBLEMS

    View Slide

  19. 19
    MONIX
    ▸Pure push - based
    ▸No protections against slow consumers
    ▸Unbounded buffers / throttling
    ▸Trivia: flatMap is aliased to mergeMap
    because concatMap is unsafe
    RX .NET - PROBLEMS

    View Slide

  20. MONIX
    REACTIVE-STREAMS.ORG
    20
    trait Subscription {
    def request(n: Long): Unit
    def cancel(): Unit
    }
    trait Subscriber[A] {
    def onSubscribe(s: Subscription): Unit
    def onNext(elem: A): Unit
    def onComplete(): Unit
    def onError(e: Throwable): Unit
    }

    View Slide

  21. TOWARD THE FUTURE[A]

    View Slide

  22. MONIX
    IDEA 1: BACK-PRESURE WITH FUTURE
    22
    import scala.concurrent.Future
    trait Observer[-A] {
    def onNext(elem: A): Future[Unit]
    def onComplete(): Unit
    def onError(e: Throwable): Unit
    }

    View Slide

  23. MONIX
    IDEA 2: CONSUMER DRIVEN CANCELATION
    23
    import monix.execution.Ack
    import scala.concurrent.Future
    trait Observer[-A] {
    def onNext(elem: A): Future[Ack]
    def onComplete(): Unit
    def onError(e: Throwable): Unit
    }

    View Slide

  24. MONIX
    IDEA 2: CONSUMER DRIVEN CANCELATION
    24
    sealed trait Ack extends Future[Ack] {
    "// ""...
    }
    object Ack {
    /** Signals demand for more. "*/
    case object Continue extends Ack
    /** Signals demand for early termination. "*/
    case object Stop extends Ack
    }

    View Slide

  25. MONIX
    SIDE EFFECTS, W00T!
    25
    class SumObserver(take: Int) extends Observer[Int] {
    private var count = 0
    private var sum = 0
    def onNext(elem: Int): Ack = {
    count += 1
    sum += elem
    if (count < take) Continue else {
    onComplete()
    Stop
    }
    }
    def onComplete() =
    println(s"Sum: $sum")
    def onError(e: Throwable) =
    e.printStackTrace()
    }

    View Slide

  26. MONIX
    OBSERVABLE IS HIGH-LEVEL
    26
    val sum: Observable[Long] =
    Observable.range(0, 1000)
    .take(100)
    .map(_ * 2)
    .foldF
    "// Actual execution
    sum.subscribe(result "=> {
    println(s"Sum: $result")
    Stop
    })

    View Slide

  27. MONIX
    val sum: Task[Long] =
    Observable.range(0, 1000)
    .take(100)
    .map(_ * 2)
    .foldL
    "// Actual execution
    val f: CancelableFuture[Long] =
    sum.runAsync
    OBSERVABLE IS HIGH-LEVEL
    27

    View Slide

  28. MONIX
    observable.flatMap { i "=> Observable.range(0, i) }
    observable.mapTask { key "=> Task(f(key)) }
    observable.mapEval { key "=> IO(f(key)) }
    OBSERVABLE IS A MONADIC TYPE
    28

    View Slide

  29. MONIX
    SUSPENDING SIDE EFFECTS
    29
    def readFile(path: String): Observable[String] =
    Observable.suspend {
    "// The side effect
    val lines = Source.fromFile(path).getLines
    Observable.fromIterator(lines)
    }

    View Slide

  30. MONIX
    SUSPENDING SIDE EFFECTS
    ▸Does not need IO / Task for
    evaluation or suspending effects
    ▸Observable is IO-ish
    30

    View Slide

  31. MONIX
    observable.mergeMap { key "=> eventsSeq(key) }
    observable.switchMap { key "=> eventsSeq(key) }
    REACTIVE W00T!
    31

    View Slide

  32. MONIX
    observable.throttleFirst(1.second)
    observable.sample(1.second)
    observable.debounce(1.second)
    observable.echoOnce(1.second)
    REACTIVE W00T!
    32

    View Slide

  33. MONIX
    observable.sampleRepeated(1.second)
    observable.debounceRepeated(1.second)
    observable.echoRepeated(1.second)
    REACTIVE W00T!
    33

    View Slide

  34. MONIX
    observable
    .distinctUntilChanged

    .sample(1.second)

    .echoRepeated(5.seconds)
    REACTIVE W00T!
    34

    View Slide

  35. MONIX
    observable.publishSelector { hot "=>
    val a = hot.filter(_ % 2 "== 0).map(_ * 2)
    val b = hot.filter(_ % 2 "== 1).map(_ * 3)
    Observable.merge(a, b)
    }
    REACTIVE W00T!
    35

    View Slide

  36. MONIX
    OBSERVABLE OPTIMISATIONS
    36

    View Slide

  37. MONIX
    OBSERVABLE OPTIMISATIONS
    ▸ Models complex state machine for eliminating
    asynchronous boundaries
    ▸ Deals with Concurrency by means of one Atomic
    ▸ Cache-line padding for avoiding false sharing
    ▸ Uses getAndSet platform intrinsics
    ▸ monix-execution ftw
    37
    FOR FLAT-MAP

    View Slide

  38. MONIX
    OBSERVABLE OPTIMISATIONS
    ▸ Models complex state machine for eliminating
    asynchronous boundaries
    ▸ Deals with Concurrency by means of one Atomic
    ▸ Cache-line padding for avoiding false sharing
    ▸ Uses getAndSet platform intrinsics
    ▸ monix-execution ftw
    38
    FOR FLAT-MAP

    View Slide

  39. MONIX
    OBSERVABLE OPTIMISATIONS
    ▸ Models complex state machine for eliminating
    asynchronous boundaries
    ▸ Deals with Concurrency by means of one Atomic
    ▸ Cache-line padding for avoiding false sharing
    ▸ Uses getAndSet platform intrinsics
    ▸ monix-execution ftw
    39
    FOR FLAT-MAP

    View Slide

  40. MONIX
    OBSERVABLE OPTIMISATIONS
    ▸ Using JCTools.org for non-blocking queues
    ▸ MPSC scenarios
    ▸ Consumer does not contend with producers
    40
    FOR MERGE-MAP / BUFFERING

    View Slide

  41. MONIX
    CONSEQUENCES
    ▸Best in class performance

    (synchronous ops have ~zero overhead,
    can optimise synchronous pipelines)
    ▸Referential Transparency

    (subscribe <-> unsafePerformIO)
    ▸ Pure API, Dirty Internals
    41

    View Slide

  42. MONIX
    CONSEQUENCES
    ▸Best in class performance

    (synchronous ops have ~zero overhead,
    can optimise synchronous pipelines)
    ▸Referential Transparency

    (subscribe <-> unsafePerformIO)
    ▸Pure API, Dirty Internals
    42

    View Slide

  43. ITERANT[F,A]
    PULL-BASED STREAMING

    View Slide

  44. ARCHITECTURE IS FROZEN
    MUSIC
    Johann Wolfgang Von Goethe
    ITERANT 44

    View Slide

  45. DATA STRUCTURES ARE
    FROZEN ALGORITHMS
    Jon Bentley
    ITERANT 45

    View Slide

  46. 46
    ITERANT
    1. Freeze Algorithms into Data-Structures

    (Immutable)
    2. Think State Machines
    3. Be Lazy
    FP DESIGN - KEY INSIGHTS

    View Slide

  47. 47
    ITERANT
    1. Freeze Algorithms into Data-Structures
    2. Think State Machines

    (most of the time)
    3. Be Lazy
    FP DESIGN - KEY INSIGHTS

    View Slide

  48. 48
    ITERANT
    1. Freeze Algorithms into Data-Structures
    2. Think State Machines
    3. Be Lazy 

    (Strict Values => Functions ;-))
    FP DESIGN - KEY INSIGHTS

    View Slide

  49. Finite State Machine Cat

    View Slide

  50. ITERANT
    LINKED LISTS
    50
    sealed trait List[+A]
    case class Cons[+A](
    head: A,
    tail: List[A])
    extends List[A]
    case object Nil
    extends List[Nothing]

    View Slide

  51. ITERANT
    LAZY EVALUATION
    51
    sealed trait Iterant[A]
    case class Next[A](
    item: A,
    rest: () "=> Iterant[A])
    extends Iterant[A]
    case class Halt[A](
    e: Option[Throwable])
    extends Iterant[A]

    View Slide

  52. ITERANT
    RESOURCE MANAGEMENT
    52
    sealed trait Iterant[A]
    case class Next[A](
    item: A,
    rest: () "=> Iterant[A],
    stop: () "=> Unit)
    extends Iterant[A]
    case class Halt[A](
    e: Option[Throwable])
    extends Iterant[A]

    View Slide

  53. ITERANT
    DEFERRING
    53
    sealed trait Iterant[A]
    "// ""...
    case class Suspend[A](
    rest: () "=> Iterant[A],
    stop: () "=> Unit)
    extends Iterant[A]

    View Slide

  54. ITERANT
    FILTER
    54
    def filter[A](fa: Iterant[A])(p: A "=> Boolean): Iterant[A] =
    fa match {
    case halt @ Halt(_) "=> halt
    "// ""...
    }

    View Slide

  55. ITERANT
    FILTER
    55
    def filter[A](fa: Iterant[A])(p: A "=> Boolean): Iterant[A] =
    fa match {
    "// ""...
    case Suspend(rest, stop) "=>
    Suspend(() "=> filter(rest())(p), stop)
    "// ""...
    }

    View Slide

  56. ITERANT
    FILTER
    56
    def filter[A](fa: Iterant[A])(p: A "=> Boolean): Iterant[A] =
    fa match {
    "// ""...
    case Next(a, rest, stop) "=>
    if (p(a)) Next(a, () "=> filter(rest())(p), stop)
    else Suspend(() "=> filter(rest())(p), stop)
    }

    View Slide

  57. TRAMPOLINES

    View Slide

  58. ITERANT
    CAN WE DO THIS ?
    58
    case class Next[A](
    item: A,
    rest: Future[Iterant[F, A]],
    stop: Future[Unit])
    extends Iterant[A]

    View Slide

  59. ITERANT
    type Task[+A] = () "=> Future[A]
    case class Next[A](
    item: A,
    rest: Task[Iterant[F, A]],
    stop: Task[Unit])
    extends Iterant[A]
    CAN WE DO THIS ?
    59

    View Slide

  60. ITERANT
    import monix.eval.Task
    case class Next[A](
    item: A,
    rest: Task[Iterant[F, A]],
    stop: Task[Unit])
    extends Iterant[A]
    CAN WE DO THIS ?
    60

    View Slide

  61. ITERANT
    import monix.eval.Coeval
    case class Next[A](
    item: A,
    rest: Coeval[Iterant[F, A]],
    stop: Coeval[Unit])
    extends Iterant[A]
    CAN WE DO THIS ?
    61

    View Slide

  62. ITERANT
    import cats.effect.IO
    case class Next[A](
    item: A,
    rest: IO[Iterant[F, A]],
    stop: IO[Unit])
    extends Iterant[A]
    CAN WE DO THIS ?
    62

    View Slide

  63. ITERANT
    sealed trait Iterant[F[_], A]
    case class Next[F[_], A](
    item: A,
    rest: F[Iterant[F, A]],
    stop: F[Unit])
    extends Iterant[F, A]
    PARAMETRIC POLYMORPHISM
    63

    View Slide

  64. ITERANT
    PARAMETRIC POLYMORPHISM
    64
    import cats.syntax.all._
    import cats.effect.Sync

    def filter[F[_], A](p: A "=> Boolean)(fa: Iterant[F, A])
    (implicit F: Sync[F]): Iterant[F, A] = {
    fa match {
    "// ""...
    case Suspend(rest, stop) "=>
    Suspend(rest.map(filter(p)), stop)
    "//""...
    }
    }

    View Slide

  65. ITERANT
    BRING YOUR OWN BOOZE
    65
    import monix.eval.Task
    val sum: Task[Int] =
    Iterant[Task].range(0, 1000)
    .filter(_ % 2 "== 0)
    .map(_ * 2)
    .foldL

    View Slide

  66. ITERANT
    BRING YOUR OWN BOOZE
    66
    import cats.effect.IO
    val sum: IO[Int] =
    Iterant[IO].range(0, 1000)
    .filter(_ % 2 "== 0)
    .map(_ * 2)
    .foldL

    View Slide

  67. ITERANT
    BRING YOUR OWN BOOZE
    67
    import monix.eval.Coeval
    val sum: Coeval[Int] =
    Iterant[Coeval].range(0, 1000)
    .filter(_ % 2 "== 0)
    .map(_ * 2)
    .foldL

    View Slide

  68. ITERANT
    PERFORMANCE PROBLEMS
    ▸Linked Lists are everywhere in FP
    ▸Linked Lists are terrible
    ▸Async or Lazy Boundaries are terrible
    ▸Find Ways to work with Arrays and
    ▸… to avoid lazy/async boundaries
    68

    View Slide

  69. ITERANT
    PERFORMANCE SOLUTIONS
    ▸Linked Lists are everywhere in FP
    ▸Linked Lists are terrible
    ▸Async or Lazy Boundaries are terrible
    ▸Find Ways to work with Arrays and
    ▸… to avoid lazy/async boundaries
    69

    View Slide

  70. ITERANT
    WHAT CAN ITERATE OVER ARRAYS?
    70

    View Slide

  71. ITERANT
    WHAT CAN ITERATE OVER ARRAYS?
    71
    trait Iterator[+A] {
    def hasNext: Boolean
    def next(): A
    }
    trait Iterable[+A] {
    def iterator: Iterator[A]
    }

    View Slide

  72. ITERANT
    WHAT CAN ITERATE OVER ARRAYS?
    72
    case class NextBatch[F[_], A](
    batch: Iterable[A],
    rest: F[Iterant[F, A]],
    stop: F[Unit])
    extends Iterant[F, A]
    case class NextCursor[F[_], A](
    cursor: Iterator[A],
    rest: F[Iterant[F, A]],
    stop: F[Unit])
    extends Iterant[F, A]

    View Slide

  73. View Slide

  74. OBSERVABLE[+A]
    vs
    ITERANT[F,A]

    View Slide

  75. OBSERVABLE VS ITERANT
    Case Study: scanEval
    75
    https://github.com/monix/monix/pull/412

    View Slide

  76. OBSERVABLE VS ITERANT
    Case Study: scanEval
    76
    import cats.effect.Sync
    sealed abstract class Iterant[F[_], A] {
    "// ""...
    def scanEval[S](seed: F[S])(op: (S, A) "=> F[S])

    (implicit F: Sync[F]): Iterant[F, S] = ???
    }

    View Slide

  77. OBSERVABLE VS ITERANT
    Case Study: scanEval
    77
    def loop(state: S)(source: Iterant[F, A]): Iterant[F, S] =
    try source match {
    case Next(head, tail, stop) "=>
    protectedF(state, head, tail, stop)
    case ref @ NextCursor(cursor, rest, stop) "=>
    evalNextCursor(state, ref, cursor, rest, stop)
    case NextBatch(gen, rest, stop) "=>
    val cursor = gen.cursor()
    val ref = NextCursor(cursor, rest, stop)
    evalNextCursor(state, ref, cursor, rest, stop)
    case Suspend(rest, stop) "=>
    Suspend[F,S](rest.map(loop(state)), stop)
    case Last(item) "=>
    val fa = ff(state, item)
    Suspend(fa.map(s "=> lastS[F,S](s)), F.unit)
    case halt @ Halt(_) "=>
    halt.asInstanceOf[Iterant[F, S]]
    } catch {
    case NonFatal(ex) "=> signalError(source, ex)
    }

    View Slide

  78. OBSERVABLE VS ITERANT
    Case Study: scanEval
    78
    import cats.effect.Effect
    abstract class Observable[+A] {
    "//…
    def scanEval[F[_], S](seed: F[S])(op: (S, A) "=> F[S])
    (implicit F: Effect[F]): Observable[S] = ???
    }

    View Slide

  79. OBSERVABLE VS ITERANT
    Case Study: scanEval
    79
    abstract class Observable[+A] {
    "//""...
    def scanEval[F[_], S](seed: F[S])(op: (S, A) "=> F[S])
    (implicit F: Effect[F]): Observable[S] =
    scanTask(Task.fromEffect(seed))((a,e) "=> Task.fromEffect(op(a,e)))
    def scanTask[S](seed: Task[S])(op: (S, A) "=> Task[S]): Observable[S] =
    ???
    }

    View Slide

  80. OBSERVABLE VS ITERANT
    Case Study: scanEval
    ▸Observable’s scanTask is flatMap
    ▸With the aforementioned
    implementation
    80

    View Slide

  81. OBSERVABLE VS ITERANT
    Benchmark: scanEval
    81

    View Slide

  82. OBSERVABLE VS ITERANT
    Case Study: scanEval
    ▸Observable has performance
    ▸Iterant has reason
    82

    View Slide

  83. OBSERVABLE VS ITERANT
    Benchmark: map(f).foldL
    83

    View Slide

  84. OBSERVABLE VS ITERANT
    OBSERVABLE FOLD-RIGHT
    ▸ Cannot express foldRight for Observable
    ▸ But can work with substitutes, e.g. foldWhileLeftL …
    84
    abstract class Observable[+A] {
    "// ""...
    def foldWhileLeftL[S](seed: "=> S)
    (op: (S, A) "=> Either[S, S]): Task[S]
    }

    View Slide

  85. OBSERVABLE VS ITERANT
    ITERANT FOLD-RIGHT
    85
    sealed abstract class Iterable[F[_], A] {
    "// ""...
    def foldRightL[B](b: F[B])
    (f: (A, F[B], F[Unit]) "=> F[B])
    (implicit F: Sync[F]): F[B]
    }

    View Slide

  86. OBSERVABLE VS ITERANT
    ITERANT FOLD-RIGHT
    86
    def exists[F[_], A](ref: Iterant[F, A], p: A "=> Boolean)
    (implicit F: Sync[F]): F[Boolean] =
    ref.foldRightL(F.pure(false)) { (e, next, stop) "=>
    if (p(e)) stop followedBy F.pure(true)
    else next
    }

    View Slide

  87. OBSERVABLE VS ITERANT
    ITERANT FOLD-RIGHT
    87
    def forall[F[_], A](ref: Iterant[F, A], p: A "=> Boolean)
    (implicit F: Sync[F]): F[Boolean] =
    ref.foldRightL(F.pure(true)) { (e, next, stop) "=>
    if (!p(e)) stop followedBy F.pure(false)
    else next
    }

    View Slide

  88. OBSERVABLE VS ITERANT
    ITERANT FOLD-RIGHT
    88
    def concat[F[_], A](lh: Iterant[F, A], rh: Iterant[F, A])
    (implicit F: Sync[F]): Iterant[F, A] =
    Iterant.suspend[F, A] {
    lh.foldRightL(F.pure(rh)) { (a, rest, stop) "=>
    F.pure(Iterant.nextS(a, rest, stop))
    }
    }

    View Slide

  89. OBSERVABLE VS ITERANT
    ITERANT FOLD-RIGHT
    89

    View Slide

  90. OBSERVABLE VS ITERANT
    Conclusions
    ▸Observable is best for
    ▸Reactive operations
    ▸Shared data sources
    ▸Throttling
    ▸Buffering
    ▸Performance
    ▸Iterant is best for
    ▸Easier implementation reasoning
    ▸Converting Task / IO calls into streams
    90

    View Slide

  91. OBSERVABLE VS ITERANT
    Conclusions
    ▸Both
    ▸Implement reactive-streams.org
    ▸Can express asynchronous streams
    ▸Do back-pressuring & safe resource
    handling
    91

    View Slide

  92. One more thing …
    vs

    View Slide

  93. QUESTIONS?
    monix.io
    @monix
    @alexelcu
    @alexandru
    alexn.org
    @monix

    View Slide