Streams

What are streams for?

  • Infinite data
  • Complex event handling
  • IO: side-effects, concurrency and resource management
  • Iteration

What is fs2?

  • Building streams
  • Transforming streams
  • Based on cats effect
  • Haskell's IterateeIO

Trivial Example

case class Cat(name: String)

Stream(Cat("Mao"))
 .repeat
 .take(3)
 .compile
 .toList
 
 // List(Cat("Mao"), Cat("Mao"), Cat("Mao"))

How does it work?

sealed trait Pull[R]
  
case class Output(value: Cat) extends Pull[Unit]
case class Uncons(inner: Pull[Unit]) extends Pull[Option[Cat]]
case class Bind(inner: Pull[R], f: R => Pull[R2]) 
  extends Pull[R2]
case class Pure(value: R) extends Pull[R]

A free monad

Trivial Example

case class Cat(name: String)

Stream(Cat("Mao")) // Build the DSL
 .repeat
 .take(3)
 .compile // Interpret the DSL
 .toList
 
 // List(Cat("Mao"), Cat("Mao"), Cat("Mao"))

Effects

Stream.eval(IO.println("Mao"))
 .repeat
 .take(3)
 .compile
 .drain
 .unsafeRunSync()
 
 // Prints
 // Mao
 // Mao
 // Mao

Effects

Stream.eval(IO.println("Mao"))
 .repeat
 .take(3)
 .evalMap(_ => IO.println("Nyan Cat"))
 .compile
 .drain
 .unsafeRunSync()

fs2 is pull-based

  • "Pull" as opposed to "Push"
  • "Internal" as opposed to "External"
  • "Cold" as opposed to "Hot"
  • Only pull as many elements as requested

How many cats are present?

val randomCat: IO[Cat] = ???

Stream.eval(randomCat)
  .repeat
  .compile
  .take(10)
  .drain

How can we tell?

val randomCat: IO[Cat] = ???

val randomCatResource: Resource[IO, Cat] = Resource.make(
  IO.println("Added a cat") >> randomCat, 
  _ => IO.println("Removed a cat")
  )

Stream.resource(randomCatResource)
  .repeat
  .compile
  .take(10)
  .drain

Performance

Mao

Maru

Bob

Nyan Cat

Chunks

  • Processed atomically
  • Some operations (evalMap, evalScan) break down chunks
  • Favour evalMapChunk and evalScanChunk

Pull vs Push

Welcome back!

  • Concurrency and non-determinism
  • IO and resource safety
  • Streams are programs, not data types
  • Chunking
  • Creating streams from queues
  • Backpressure: pull vs push
  • Concurrency with streams
  • Combining streams

Experimentation

  • Small experiments
  • Make predictions
  • Tweak the code
  • Break the code

Thread Pools

What are they?

Why have several?

[error]    |  program.unsafeRunSync()
[error]    |                     ^
[error]    |Could not find an implicit IORuntime.
[error]    |
[error]    |The following import might fix the problem:
[error]    |
[error]    |  import cats.effect.unsafe.implicits.global

Reacting to errors

// http4s
BlazeClientBuilder.withExecutionContext(ec)

Possibly incorrectly

implicit val ec: ExecutionContext = ExecutionContext.global

Different kinds of work

  • Compute
  • Blocking

Text

Learning to Learn

Spot the problem

Exception in thread "main" java.lang.IndexOutOfBoundsException: 3
        at scala.collection.LinearSeqOps.apply(LinearSeq.scala:117)
        at scala.collection.LinearSeqOps.apply$(LinearSeq.scala:114)
        at scala.collection.immutable.List.apply(List.scala:79)
        at index.App$.lastUserId(App.scala:6)
  def lastUserId(userIds: List[Int]): Int = 
    userIds(userIds.length)

 The collection of user ids is described by the Scala List. This is zero-indexed, so the last user id is found at index N - 1 where N is the length of the list. When an index greater than this number is used, an exception is raised and the program exits.

We're going to think about

  • How to think about problems you've seen
  • How to think about problems you haven't
  • How to learn to solve them
  • How to learn effectively

Abstraction

 The collection of user ids is described by the Scala List. This is zero-indexed, so the last user id is found at index N - 1 where N is the length of the list. When an index greater than this number is used, an exception is raised and the program exits.

Notice we don't mention solid state physics

Concept Map

Notional Machine

     The computer:

  • Finds the length of the list of user ids
  • Gets the user id at the length index
  • Throws an exception
  • Skips the rest of the program

 

  def lastUserId(userIds: List[Int]): Int = 
    userIds(userIds.length)

An abstraction of abstraction

The Problem

Exception in thread "main" java.lang.NoClassDefFoundError: 
  scalacache/CacheConfig$
        at utils.UserIds$.cache(UserIds.scala:12)
        at index.App$.index(App.scala:15)
        at index.index.main(App.scala:13)
Caused by: java.lang.ClassNotFoundException: 
  scalacache.CacheConfig$
        at java.base/jdk.internal.loader...
import utils.UserIds
...
val cache = UserIds.cache[IO]
 

The worst that can happen...

  • It takes several days to resolve the problem.
  • You find something that works, but don't really understand why.
  • Several months later, you see another NoClassDefFoundError.
  • You can't remember how you resolved it before.

Exercise

  • Write your own explanation
  • Draw a concept map to help
    • Use the terms "Scala", "SBT" and "scalacache"

  Scala compiled the program, but fails to run it. Perhaps our Scala compiler didn't compile scalacache properly?

SBT threw an error when running the program. This is a problem with out SBT configuration.
The error originates in scalacache. There's a bug in scalacache, or in the way we're using it.

Explanations

A mental model...

  • is always incomplete
  • may be incorrect
  • is usually good enough

Refining a mental model

  • Draw it on paper
  • Explain it to someone else
  • Cross reference it
    • Read the docs
    • Ask someone more experienced
  • Experiment
  • Review and redraw

Metacognition

Think about your thinking

  • As you solved the problem, what do you spend your day doing?
  • Did your mental model change?
  • Draw concept maps as you go

Review

  • The importance of abstraction
  • Mental models and concept maps
  • How to refine a mental model

Thank you!

Fs2 Streams

By Zainab Ali

Fs2 Streams

  • 229