Slide 1

Slide 1 text

A sky full of streams Embracing compositionality of functional streams Jakub Kozłowski

Slide 2

Slide 2 text

Streams around you

Slide 3

Slide 3 text

Listening to events (UI, Queue, ...) Scanning paginated results (external APIs) Traversing a list multiple times class Traversing( findProject: ProjectId => IO[Project], findIssues: Project => IO[List[IssueId]], findIssue: IssueId => IO[Issue], allProjectIds: IO[List[ProjectId]], me: UserId ) { def allMyIssuesInProjectsWithExclusions( excludedProject: Project => Boolean, excludedIssue: Issue => Boolean ): IO[List[Issue] } = ???

Slide 4

Slide 4 text

trait Projects { def getPage(afterProject: Option[ProjectId]): IO[List[Project]] } trait Issues { def getPage( projectId: ProjectId, afterIssue: Option[IssueId] ): IO[List[Issue]] } class Github(projects: Projects, issues: Issues) { def findFirstWithMatchingIssue( predicate: Issue => Boolean ): IO[Option[Project]] } Listening to events (UI, Queue, ...) Scanning paginated results (external APIs) Traversing a list multiple times = ???

Slide 5

Slide 5 text

trait Listener { def create[A: Decoder]( handler: A => Unit ): Unit } Listening to events (UI, Queue, ...) Scanning paginated results (external APIs) Traversing a list multiple times = ???

Slide 6

Slide 6 text

Many, many others - TCP connections - HTTP calls - Scheduled work - Files, binary data - Websockets

Slide 7

Slide 7 text

Common core?

Slide 8

Slide 8 text

Common core? Potentially making control flow decisions

Slide 9

Slide 9 text

Common core? Potential parallelism at each step Potentially making control flow decisions

Slide 10

Slide 10 text

Common core? Potential effects at each step Potential parallelism at each step Potentially making control flow decisions

Slide 11

Slide 11 text

Common core? Handling multiple values Potential effects at each step Potential parallelism at each step Potentially making control flow decisions

Slide 12

Slide 12 text

Common core? Handling multiple values Potential effects at each step Potential parallelism at each step Potentially making control flow decisions Resource safety

Slide 13

Slide 13 text

Common core? Handling multiple values Potential effects at each step Potential parallelism at each step Potentially making control flow decisions Resource safety

Slide 14

Slide 14 text

Common core? Handling multiple values Potential effects at each step Potential parallelism at each step Potentially making control flow decisions Resource safety

Slide 15

Slide 15 text

fs2.Stream[F[_], O]

Slide 16

Slide 16 text

fs2.Stream[F[_], O] An immutable, lazy value

Slide 17

Slide 17 text

fs2.Stream[F[_], O] An immutable, lazy value Emits from 0 to ∞ values || Fails with a Throwable

Slide 18

Slide 18 text

fs2.Stream[F[_], O] An immutable, lazy value Emits from 0 to ∞ values || Fails with a Throwable Can have effects of F

Slide 19

Slide 19 text

fs2.Stream[F[_], O] An immutable, lazy value Emits from 0 to ∞ values || Fails with a Throwable Can have effects of F F[A] = usually IO[A]

Slide 20

Slide 20 text

fs2.Stream[F[_], O] An immutable, lazy value Emits from 0 to ∞ values || Fails with a Throwable Can have effects of F F[A] = usually IO[A] Or, less formally...

Slide 21

Slide 21 text

fs2.Stream[F[_], O] A description of: A sequence of effects potentially producing values

Slide 22

Slide 22 text

fs2.Stream[F[_], O] A description of: A series of effects producing values

Slide 23

Slide 23 text

fs2.Stream[F[_], O] A description of: A series of effectsproducing values

Slide 24

Slide 24 text

fs2.Stream[F[_], O] A description of: A series of effects producing values

Slide 25

Slide 25 text

fs2.Stream[F[_], O] A description of: A series of effects producing values

Slide 26

Slide 26 text

Pure streams ✨ fs2.Stream[fs2.Pure, O] Can be evaluated to a list without effects

Slide 27

Slide 27 text

Pure streams ✨ fs2.Stream[fs2.Pure, O] Thanks to a clever hack:

Slide 28

Slide 28 text

Pure streams ✨ fs2.Stream[fs2.Pure, O] package object fs2 { type Pure[A] <: Nothing } Thanks to a clever hack:

Slide 29

Slide 29 text

Pure streams ✨ fs2.Stream[fs2.Pure, O] package object fs2 { type Pure[A] <: Nothing } Thanks to a clever hack: And covariance:

Slide 30

Slide 30 text

Pure streams ✨ fs2.Stream[fs2.Pure, O] package object fs2 { type Pure[A] <: Nothing } Thanks to a clever hack: And covariance: final class Stream[+F[_], +O]

Slide 31

Slide 31 text

Pure streams ✨ fs2.Stream[fs2.Pure, O] package object fs2 { type Pure[A] <: Nothing } Thanks to a clever hack: And covariance: final class Stream[+F[_], +O] val pure: Stream[Pure, Int] = Stream(1, 2, 3) val io : Stream[F, Int] = p

Slide 32

Slide 32 text

Pure streams ✨ fs2.Stream[fs2.Pure, O] package object fs2 { type Pure[A] <: Nothing } Thanks to a clever hack: And covariance: final class Stream[+F[_], +O] val pure: Stream[Pure, Int] = Stream(1, 2, 3) val io : Stream[F, Int] = p For any F[_]

Slide 33

Slide 33 text

Building fs2 streams

Slide 34

Slide 34 text

Lift a single value Building fs2 streams

Slide 35

Slide 35 text

Lift a single value Stream.emit (_: A ) Building fs2 streams

Slide 36

Slide 36 text

Lift a single value Lift a sequence Stream.emit (_: A ) Building fs2 streams

Slide 37

Slide 37 text

Lift a single value Lift a sequence Stream.emit (_: A ) Stream.emits(_: Seq[A]) Building fs2 streams

Slide 38

Slide 38 text

Lift a single value Lift a single effect Lift a sequence Stream.emit (_: A ) Stream.emits(_: Seq[A]) Building fs2 streams

Slide 39

Slide 39 text

Stream.eval (_: F[A]) Lift a single value Lift a single effect Lift a sequence Stream.emit (_: A ) Stream.emits(_: Seq[A]) Building fs2 streams

Slide 40

Slide 40 text

Infinite streams ∞ Stream.constant(42) Stream.awakeEvery[IO](1.second) Stream.iterate(1.0)(_ * 2)

Slide 41

Slide 41 text

Infinite streams ∞ Out of time/memory? Try these:

Slide 42

Slide 42 text

.drain Infinite streams ∞ Out of time/memory? Try these:

Slide 43

Slide 43 text

.drain .head Infinite streams ∞ Out of time/memory? Try these:

Slide 44

Slide 44 text

.drain .head .take(10) Infinite streams ∞ Out of time/memory? Try these:

Slide 45

Slide 45 text

.drain .head .take(10) .interruptAfter(10.minutes) Infinite streams ∞ Out of time/memory? Try these:

Slide 46

Slide 46 text

.drain .head .take(10) .interruptAfter(10.minutes) .interruptWhen(Stream.random[IO].map(_ % 10 == 0)) Infinite streams ∞ Out of time/memory? Try these:

Slide 47

Slide 47 text

.drain .head .take(10) .interruptAfter(10.minutes) .interruptWhen(Stream.random[IO].map(_ % 10 == 0)) Infinite streams ∞ Out of time/memory? Try these: When this produces true

Slide 48

Slide 48 text

Consuming streams val example: Stream[IO, Int] example.compile. Effectful streams need to be "compiled" to the effect

Slide 49

Slide 49 text

Consuming streams val example: Stream[IO, Int] example.compile. Effectful streams need to be "compiled" to the effect

Slide 50

Slide 50 text

Transforming streams numbers .map(_ % 10 + 1) .flatMap { until => Stream.range(0, until) } .evalMap(showOut) val numbers: Stream[IO, Int] = Stream.random[IO] def showOut(i: Int) = IO(println(i))

Slide 51

Slide 51 text

Transforming streams numbers .map(_ % 10 + 1) .flatMap { until => Stream.range(0, until) } .evalMap(showOut) Transform each element Replace element with sub-stream and flatten Transform each element efectfully def showOut(i: Int) = IO(println(i))

Slide 52

Slide 52 text

Transforming streams numbers .map(_ % 10 + 1) .flatMap { until => Stream.range(0, until) } .evalMap(showOut) def showOut(i: Int) = IO(println(i)) //[-467868903, 452477122, 1143039958, ...] //[-2, 3, 9, ...] //[0, 1, 2, // 0, 1, 2, 3, 4, 5, 6, 7, 8, ...]

Slide 53

Slide 53 text

What actually happens numbers.debug("random") .map(_ % 10 + 1).debug("map") .flatMap { until => Stream.range(0, until) }.debug("flatMap") .evalMap(showOut)

Slide 54

Slide 54 text

What actually happens numbers.debug("random") .map(_ % 10 + 1).debug("map") .flatMap { until => Stream.range(0, until) }.debug("flatMap") .evalMap(showOut) random: -467868903 map: -2 random: 452477122 map: 3 flatMap: 0 0 flatMap: 1 1 flatMap: 2 2 random: 1143039958 map: 9 flatMap: 0 0 flatMap: 1 1 flatMap: 2 2 flatMap: 3 3 flatMap: 4 4 flatMap: 5 5 flatMap: 6 6 flatMap: 7 7 flatMap: 8 8

Slide 55

Slide 55 text

What actually happens numbers.debug("random") .map(_ % 10 + 1).debug("map") .flatMap { until => Stream.range(0, until) }.debug("flatMap") .evalMap(showOut) random: -467868903 map: -2 random: 452477122 map: 3 flatMap: 0 0 flatMap: 1 1 flatMap: 2 2 random: 1143039958 map: 9 flatMap: 0 0 flatMap: 1 1 flatMap: 2 2 flatMap: 3 3 flatMap: 4 4 flatMap: 5 5 flatMap: 6 6 flatMap: 7 7 flatMap: 8 8

Slide 56

Slide 56 text

What actually happens random: -467868903 map: -2 random: 452477122 map: 3 flatMap: 0 0 flatMap: 1 1 flatMap: 2 2 random: 1143039958 map: 9 flatMap: 0 0 flatMap: 1 1 flatMap: 2 2 flatMap: 3 3 flatMap: 4 4 flatMap: 5 5 flatMap: 6 6 flatMap: 7 7 flatMap: 8 8 numbers.debug("random") .map(_ % 10 + 1).debug("map") .flatMap { until => Stream.range(0, until) }.debug("flatMap") .evalMap(showOut)

Slide 57

Slide 57 text

Control flow Stream.bracket { IO { new BufferedReader(new FileReader(new File("./build.sbt"))) } }(f => IO(f.close())) val file: Stream[IO, BufferedReader] =

Slide 58

Slide 58 text

Stream.bracket { IO { new BufferedReader(new FileReader(new File("./build.sbt"))) } }(f => IO(f.close())) val file: Stream[IO, BufferedReader] = ... val process = file.flatMap { reader => Stream .eval(IO(Option(reader.readLine()))) .repeat .unNoneTerminate }.map(_.length) Stream.sleep_[IO](200.millis) ++ Stream.random[IO].flatMap(Stream.range(0, _)).take(5) ++ Stream(1, 3, 5) ++ Control flow

Slide 59

Slide 59 text

Stream(1, 3, 5) ++ file.flatMap { reader => Stream .eval(IO(Option(reader.readLine()))) .repeat .unNoneTerminate }.map(_.length) Stream.sleep_[IO](200.millis) ++ Stream.random[IO].flatMap(Stream.range(0, _)).take(5) ++ 3 known elements 5 ~random elements No elements, 200ms wait Lots of elements (1 per file line) Control flow

Slide 60

Slide 60 text

def slowDownEveryNTicks( resets: Stream[IO, Unit], n: Int ): Stream[IO, FiniteDuration] - emit n values every 1 millisecond - emit n values every 2 milliseconds - emit n values every 4 milliseconds - ... Reset delay every time something is emitted by `resets`

Slide 61

Slide 61 text

def slowDownEveryNTicks( resets: Stream[IO, Unit], n: Int ): Stream[IO, FiniteDuration] = { val showSlowingDown = Stream.eval_(IO(println("---------- Slowing down! ----------"))) val showResetting = IO(println("---------- Resetting delays! ----------")) val delaysExponential: Stream[IO, FiniteDuration] = Stream .iterate(1.millisecond)(_ * 2) .flatMap { Stream.awakeDelay[IO](_).take(n.toLong) ++ showSlowingDown } Stream.eval(MVar.empty[IO, Unit]).flatMap { restart => val delaysUntilReset = delaysExponential.interruptWhen(restart.take.attempt) delaysUntilReset.repeat concurrently resets.evalMap(_ => restart.put(()) *> showResetting) } }

Slide 62

Slide 62 text

def slowDownEveryNTicks( resets: Stream[IO, Unit], n: Int ): Stream[IO, FiniteDuration] = { val showSlowingDown = Stream.eval_(IO(println("---------- Slowing down! ----------"))) val showResetting = IO(println("---------- Resetting delays! ----------")) val delaysExponential: Stream[IO, FiniteDuration] = Stream .iterate(1.millisecond)(_ * 2) .flatMap { Stream.awakeDelay[IO](_).take(n.toLong) ++ showSlowingDown } Stream.eval(MVar.empty[IO, Unit]).flatMap { restart => val delaysUntilReset = delaysExponential.interruptWhen(restart.take.attempt) delaysUntilReset.repeat concurrently resets.evalMap(_ => restart.put(()) *> showResetting) } }

Slide 63

Slide 63 text

def slowDownEveryNTicks( resets: Stream[IO, Unit], n: Int ): Stream[IO, FiniteDuration] = { val showSlowingDown = Stream.eval_(IO(println("---------- Slowing down! ----------"))) val showResetting = IO(println("---------- Resetting delays! ----------")) val delaysExponential: Stream[IO, FiniteDuration] = Stream .iterate(1.millisecond)(_ * 2) .flatMap { Stream.awakeDelay[IO](_).take(n.toLong) ++ showSlowingDown } Stream.eval(MVar.empty[IO, Unit]).flatMap { restart => val delaysUntilReset = delaysExponential.interruptWhen(restart.take.attempt) delaysUntilReset.repeat concurrently ... resets.evalMap(_ => restart.put(()) *> showResetting) } }

Slide 64

Slide 64 text

def slowDownEveryNTicks( resets: Stream[IO, Unit], n: Int ): Stream[IO, FiniteDuration] = { val showSlowingDown = Stream.eval_(IO(println("---------- Slowing down! ----------"))) val showResetting = IO(println("---------- Resetting delays! ----------")) val delaysExponential: Stream[IO, FiniteDuration] = Stream .iterate(1.millisecond)(_ * 2) .flatMap { Stream.awakeDelay[IO](_).take(n.toLong) ++ showSlowingDown } Stream.eval(MVar.empty[IO, Unit]).flatMap { restart => val delaysUntilReset = delaysExponential.interruptWhen(restart.take.attempt) delaysUntilReset.repeat concurrently resets.evalMap(_ => restart.put(()) *> showResetting) } }

Slide 65

Slide 65 text

def slowDownEveryNTicks( resets: Stream[IO, Unit], n: Int ): Stream[IO, FiniteDuration] = { val showSlowingDown = Stream.eval_(IO(println("---------- Slowing down! ----------"))) val showResetting = IO(println("---------- Resetting delays! ----------")) val delaysExponential: Stream[IO, FiniteDuration] = Stream .iterate(1.millisecond)(_ * 2) .flatMap { Stream.awakeDelay[IO](_).take(n.toLong) ++ showSlowingDown } Stream.eval(MVar.empty[IO, Unit]).flatMap { restart => val delaysUntilReset = delaysExponential.interruptWhen(restart.take.attempt) delaysUntilReset.repeat concurrently resets.evalMap(_ => restart.put(()) *> showResetting) } }

Slide 66

Slide 66 text

val slowDown = slowDownEveryNTicks( resets = Stream.awakeEvery[IO](3.seconds).void, n = 5 ) clientMessages.zipLeft(slowDown) Demo time!

Slide 67

Slide 67 text

Inversion of flow control def allMyIssuesInProjectsWithExclusions( excludedProject: Project => Boolean, excludedIssue: Issue => Boolean ): IO[List[Issue]]

Slide 68

Slide 68 text

Inversion of flow control def allMyIssuesInProjectsWithExclusions( excludedProject: Project => Boolean, excludedIssue: Issue => Boolean ): IO[List[Issue]] def firstIssue( inProject: Project => Boolean, ): IO[Option[Issue]]

Slide 69

Slide 69 text

Inversion of flow control def allMyIssuesInProjectsWithExclusions( excludedProject: Project => Boolean, excludedIssue: Issue => Boolean ): IO[List[Issue]] def firstIssue( inProject: Project => Boolean, ): IO[Option[Issue]]

Slide 70

Slide 70 text

Inversion of flow control def allMyIssuesInProjectsWithExclusions( excludedProject: Project => Boolean, excludedIssue: Issue => Boolean ): IO[List[Issue]] val projects: Stream[IO, Project] val issues: Pipe[IO, Project, Issue] def firstIssue( inProject: Project => Boolean, ): IO[Option[Issue]]

Slide 71

Slide 71 text

Inversion of flow control def allMyIssuesInProjectsWithExclusions( excludedProject: Project => Boolean, excludedIssue: Issue => Boolean ): IO[List[Issue]] val projects: Stream[IO, Project] val issues: Pipe[IO, Project, Issue] = projects .filter(!excludedProject(_)) .through(issues) .filter(!excludedIssue(_)) .filter(_.creator === me) .compile .toList def firstIssue( inProject: Project => Boolean, ): IO[Option[Issue]]

Slide 72

Slide 72 text

Inversion of flow control def allMyIssuesInProjectsWithExclusions( excludedProject: Project => Boolean, excludedIssue: Issue => Boolean ): IO[List[Issue]] val projects: Stream[IO, Project] val issues: Pipe[IO, Project, Issue] = projects .find(inProject) .through(issues) .head.compile.last = projects .filter(!excludedProject(_)) .through(issues) .filter(!excludedIssue(_)) .filter(_.creator === me) .compile .toList def firstIssue( inProject: Project => Boolean, ): IO[Option[Issue]]

Slide 73

Slide 73 text

Inversion of flow control def allMyIssuesInProjectsWithExclusions( excludedProject: Project => Boolean, excludedIssue: Issue => Boolean ): IO[List[Issue] val projects: Stream[IO, Project] val issues: Pipe[IO, Project, Issue] = projects .find(inProject) .through(issues) .head.compile.last = projects .filter(!excludedProject(_)) .through(issues) .filter(!excludedIssue(_)) .filter(_.creator === me) .compile .toList def firstIssue( inProject: Project => Boolean, ): IO[Option[Issue] type Pipe[F[_], -I, +O] = Stream[F, I] => Stream[F, O]

Slide 74

Slide 74 text

Reusable transformations val filterText: Pipe[IO, Message, TextMessage] = _.evalMap { case msg: TextMessage => msg.some.pure[IO] case msg => logger.error(s"Not a text message: $msg").as(none) }.unNone def decode[A: Decoder]: Pipe[IO, String, A] = _.map(io.circe.parser.decode[A](_)).evalMap { case Right(v) => v.some.pure[IO] case Left(e) => logger.error(e)("Decoding error").as(none) }.unNone consumerMessages .through(filterText) .map(_.getText) .through(decode[UserEvent])

Slide 75

Slide 75 text

Compositionality and streams

Slide 76

Slide 76 text

Compositionality principle The meaning of an expression is the meaning of its parts and the way they are combined together.

Slide 77

Slide 77 text

Compositionality and referential transparency val program: IO[Unit] = createFork.bracket(commit(a))(closeFork(_)) >> sendEvent(commitSuccessful(data))

Slide 78

Slide 78 text

Compositionality and referential transparency val program: IO[Unit] = { val prog1 = createFork.bracket(commit(data))(closeFork(_)) val prog2 = sendEvent(commitSuccessful(data)) prog1 >> prog2 } val program: IO[Unit] = createFork.bracket(commit(a))(closeFork(_)) >> sendEvent(commitSuccessful(data))

Slide 79

Slide 79 text

Referential transparency is not always enough val program: IO[Unit] = { val prog1 = createFork.bracket(commit(data))(closeFork(_)) val prog2 = sendEvent(commitSuccessful(data)) prog1 >> prog2 }

Slide 80

Slide 80 text

Referential transparency is not always enough def program(use: Repository => IO[A]): IO[A] = { val prog1 = createFork.bracket(use)(closeFork(_)) val prog2 = sendEvent(commitSuccessful(data)) prog1 <* prog2 }

Slide 81

Slide 81 text

Referential transparency is not always enough def program(use: Repository => IO[A]): IO[A] = { val prog1 = createFork.bracket(use)(closeFork(_)) val prog2 = sendEvent(commitSuccessful(data)) prog1 <* prog2 }

Slide 82

Slide 82 text

Referential transparency is not always enough def program(use: Repository => IO[A]): IO[A] = { val prog1 = createFork.bracket(use)(closeFork(_)) val prog2 = sendEvent(commitSuccessful(data)) prog1 <* prog2 } program(quiteLongStream(_).compile.toList):

Slide 83

Slide 83 text

Referential transparency is not always enough def program(use: Repository => IO[A]): IO[A] = { val prog1 = createFork.bracket(use)(closeFork(_)) val prog2 = sendEvent(commitSuccessful(data)) prog1 <* prog2 } program(quiteLongStream(_).compile.toList):

Slide 84

Slide 84 text

Streams are self-contained val program: Stream[IO, Repository] = Stream.bracket(createFork)(closeFork(_)) ++ Stream.eval_(sendEvent(commitSuccessful(data))) program.flatMap(quiteLongStream)

Slide 85

Slide 85 text

def tupleWith42[A](fa: IO[A]): IO[(A, Int)] = fa.map(a => (a, 42)) How can we test this function?

Slide 86

Slide 86 text

val mostUsefulIssues = issueSource.filter(_.upvotes > 100) How can we test this stream?

Slide 87

Slide 87 text

def mostUsefulIssues[F[_]](issueSource: Stream[F, Issue]) = issueSource.filter(_.upvotes > 100) How can we test this stream?

Slide 88

Slide 88 text

def mostUsefulIssues[F[_]](issueSource: Stream[F, Issue]) = issueSource.filter(_.upvotes > 100) How can we test this stream? mostUsefulIssues( Stream(Issue("#1", "/u/root", 90), Issue("#2", "/u/root", 110)) ).toList === List(Issue("#2", "/u/root", 110))

Slide 89

Slide 89 text

fs2 is truly compositional

Slide 90

Slide 90 text

fs2 is truly compositional - No matter where the data comes from, it's always the same abstraction

Slide 91

Slide 91 text

fs2 is truly compositional - No matter where the data comes from, it's always the same abstraction - All combinators work on all the streams of compatible types

Slide 92

Slide 92 text

fs2 is truly compositional - No matter where the data comes from, it's always the same abstraction - All combinators work on all the streams of compatible types - Stream scales from pure streams to complex processes with lots of resources and concurrency

Slide 93

Slide 93 text

fs2 is truly compositional - No matter where the data comes from, it's always the same abstraction - All combinators work on all the streams of compatible types - Stream scales from pure streams to complex processes with lots of resources and concurrency - List + IO on superpowers with far less responsibility for you

Slide 94

Slide 94 text

val converter: Stream[IO, Unit] = Stream.resource(Blocker[IO]).flatMap { blocker => def fahrenheitToCelsius(f: Double): Double = (f - 32.0) * (5.0/9.0) io.file.readAll[IO](Paths.get("testdata/fahrenheit.txt"), blocker, 4096) .through(text.utf8Decode) .through(text.lines) .filter(s => !s.trim.isEmpty && !s.startsWith("//")) .map(line => fahrenheitToCelsius(line.toDouble).toString) .intersperse("\n") .through(text.utf8Encode) .through(io.file.writeAll(Paths.get("testdata/celsius.txt"), blocker)) }

Slide 95

Slide 95 text

def server( blocker: Blocker ): Stream[IO, Resource[IO, fs2.io.tcp.Socket[IO]]] = Stream.resource(fs2.io.tcp.SocketGroup[IO](blocker)).flatMap { group => group.server[IO]( new InetSocketAddress("0.0.0.0", 8080) ) } val clientMessages = Stream .resource(Blocker[IO]) .flatMap(server) .map { Stream .resource(_) .flatMap(_.reads(1024)) .through(fs2.text.utf8Decode) .through(fs2.text.lines) .map("Message: " + _) } .parJoin(maxOpen = 10)

Slide 96

Slide 96 text

No content

Slide 97

Slide 97 text

"co.fs2" %% "fs2-core" % "2.1.0" https://fs2.io Give it a try!

Slide 98

Slide 98 text

Acknowledgements Thanks to Fabio Labella for help with this talk! Thanks to Michael Pilquist, Paul Chiusano, Pavel Chlupacek, Fabio and all the contributors of fs2 and cats-effect/cats Massively inspired by: - Declarative control flow with fs2 streams by Fabio Labella: https://www.youtube.com/watch?v=x3GLwl1FxcA - Compositional Programming by Runar Bjarnason: https://www.youtube.com/watch?v=ElLxn_l7P2I

Slide 99

Slide 99 text

Thank you blog.kubukoz.com @kubukoz Slides: https://speakerdeck.com/kubukoz Code: https://git.io/fjpEv

Slide 100

Slide 100 text

Thank you blog.kubukoz.com @kubukoz Slides: https://speakerdeck.com/kubukoz Code: https://git.io/fjpEv Find me on YouTube! (https://www.youtube.com/channel/UCBSRCuGz9laxVv0rAnn2O9Q)