Lecture 5: Laziness and Streams

Presenter Notes

In this course we will explore a series of methods for separating program description from program evaluation.

This line of inquiry leads ultimately to the Free monad in lecture 13.

Presenter Notes

Eval Monad

cats.Eval is a monad that allows us to abstract over different models of evaluation.

It is somewhat similar to scala.concurrent.Future from the standard library and fs2.Task from the Functional Streams for Scala library.

Presenter Notes

We typically hear of two such evaluation models: eager and lazy.

Eager computations happen immediately, whereas lazy computations happen only upon access.

Presenter Notes

We can see this using a computation with a visible side-effect:

val x = { println("Computing x"); 1+1 }
//Computing x
//x: Int = 2
x
//res0: Int = 2
x
//res1: Int = 2

Presenter Notes

By contrast, defs are lazy and not memoized:

def y = { println("Computing y"); 1+1}
//y: Int
y
//Computing y
//res2: Int = 2
y
//Computing y
//res3: Int = 2

Presenter Notes

Last but not least, lazy vals are lazy and memoized:

lazy val z = { println("Computing z"); 1+1}
//z: Int = <lazy>
z
// Computing z
//res4: Int = 2
z
//res5: Int = 2

Presenter Notes

Eval has three subtypes: Eval.Now, Eval.Later, and Eval.Always:

import cats.Eval
val now = Eval.now({ println("foo"); 1+1})
//foo
//now: cats.Eval[Int] = Now(2)
val later = Eval.later({ println("foo"); 1+1})
//later: cats.Eval[Int] = cats.Later@24a2a9b8
later.value
//foo
//res0: Int = 2
later.value
//res1: Int = 2

Presenter Notes

val always = Eval.always({ println("foo"); 1+1})
//always: cats.Eval[Int] = cats.Always@773adb25
always.value
//foo
//res2: Int = 2
always.value
//foo
//res3: Int = 2

Presenter Notes

The three behaviors are summarized below:

Eager Lazy
Memoized `val, Eval.now` `lazy val, Eval.later`
Not Memoized - `def, Eval.always`

Presenter Notes

Evals map and flatMap methods add computations to a chain:

val greeting = Eval.always { println("Step 1")
  "Hello"
  }.map { str =>
    println("Step 2")
    str + " world"
  }
//greeting: cats.Eval[String] = cats.Eval$$anon$8@411b9c5f
greeting.value
//Step 1
//Step 2
//res0: String = Hello world

Presenter Notes

Eval also supports for comprehensions:

val ans = for {
    a <- Eval.now { println("Calculating A") ; 40 }
    b <- Eval.now { println("Calculating B") ; 2 }
  } yield {
    println("Adding A and B"); a+b
  }
//Calculating A
//ans: cats.Eval[Int] = cats.Eval$$anon$8@f636c08

Presenter Notes

Note that, while the semantics of the originating Eval instances are maintained, mapping functions are always called lazily on demand:

ans.value
//Calculating B
//Adding A and B
//res0: Int = 42
ans.value
//Calculating B
//Adding A and B
//res1: Int = 42

Presenter Notes

We can use Eval's memoize method to memoize a chain of computations.

val saying = Eval.always { println("Step 1") ; "The cat" }
  .map { str => println("Step 2") ; str + " sat on" }.memoize
  .map { str => println("Step 3") ; str + " the mat" }
//saying: cats.Eval[String] = cats.Eval$$anon$8@24c1a639

Presenter Notes

Calculations before the call to memoize are cached, whereas calculations after the call retain their original semantics:

saying.value
//Step 1
//Step 2
//Step 3
//res0: String = The cat sat on the mat
saying.value
//Step 3
//res1: String = The cat sat on the mat

Presenter Notes

One useful property of Eval is that its map and flatMap methods are trampolined.

This means we can nest calls to map and flatMap arbitrarily without consuming stack frames.

We’ll illustrate this by comparing Eval to Option.

Presenter Notes

The loopM method below creates a loop through a monad’s flatMap:

import cats.Monad
import cats.syntax.flatMap._
import scala.language.higherKinds
def stackDepth: Int = Thread.currentThread.getStackTrace.length
def loopM[M[_] : Monad](m: M[Int], count: Int): M[Int] = {
  println(s"Stack depth $stackDepth")
  count match {
    case 0 => m
    case n => m.flatMap { _ => loopM(m, n - 1) }
  }
}

Presenter Notes

When we run loopM with an Option we can see the stack depth slowly increasing:

import cats.std.option._
import cats.syntax.option._
loopM(1.some, 3)
//Stack depth 45
//Stack depth 52
//Stack depth 59
//Stack depth 66
//res0: Option[Int] = Some(1)

Presenter Notes

Now let’s see the same thing using Eval. The trampoline keeps the stack depth constant:

loopM(Eval.now(1), 3).value
//Stack depth 45
//Stack depth 49
//Stack depth 49
//Stack depth 49
//res1: Int = 1

Presenter Notes

Streams

Recall the definition of a simple list:

sealed trait List[+A] //base trait
case object Nil extends List[Nothing]
case class Cons[+A](head: A,
                    tail: List[A])
  extends List[A]

Presenter Notes

A Stream is nothing other than a lazily evaluated list:

trait Stream[+A] //base trait
case object Empty extends Stream[Nothing]
case class Cons[+A](h: () => A,
                    t: () => Stream[A])
  extends Stream[A]

Presenter Notes

The most important difference between the two:

  • the tail of Cons of List is eager
  • the tail of Cons of Stream is lazy

Like lists, streams are monadic. Non-empty streams (e.g. zippers) are also commonly used comonads. More on this later.

Presenter Notes

Note the type has been inferred as Cons[Int] rather than Stream[Int]:

val s = Cons(() => 1, () => Cons(() => 2, () => Empty))
//res0 = Cons(<function0>,<function0>)
s.h
//res1: () => Int = <function0>
s.h()
//res2: Int = 1

Presenter Notes

The smart constructor memoizes and hides the thunks as well as assisting with type inference:

def cons[A](hd: => A, tl: => Stream[A]):
  Stream[A] = {
    lazy val head = hd
    lazy val tail = tl
    Cons(() => head, () => tail)
    }
def empty[A]: Stream[A] = Empty

Presenter Notes

Note that the call-by-name syntax in the smart constructor removes need for thunks:

val s = cons(1, cons(2, empty))
//s: Stream[Int] = Cons(<function0>,<function0>)

Presenter Notes

Exercise

Use the cons smart constructor to implement a sine wave sampled at multiples of π/3 radians.

Presenter Notes

Presenter Notes

Here's one approach. Create the top half of the circle 'by hand', then use it to create the bottom half :

def sinePos: Stream[Double] = ???
def sineNeg: Stream[Double] = sinePos.map { d => -1*d }

Presenter Notes

def sine: Stream[Double] =
  Stream.cons(0,
    Stream.cons(1.0/2,
      Stream.cons(math.sqrt(3)/2,
        Stream.cons(1.0,
          Stream.cons(math.sqrt(3)/2,
            Stream.cons(1.0/2, sineNeg)
  )))))
sine.take(32)

Presenter Notes

0.0
0.5
0.8660254037844386
1.0
0.8660254037844386
0.5
-0.0
-0.5
-0.8660254037844386
-1.0
-0.8660254037844386
-0.5
0.0
0.5
...

Presenter Notes

Exercise

We can turn an infinite Stream into a finite Stream with take.

foo.take(6) will insert an Empty after the sixth element of foo.

trait Stream[+A] {
  def take(n: Int): Stream[A] = this match {
    case cons(head, lazyTail) if ??? => ???
      case cons(head, lazyTail) if ??? => ???
      case Empty => empty[A]
  }
}

Presenter Notes

Presenter Notes

trait Stream[+A] {
  def take(n: Int): Stream[A] = this match {
    case cons(head, lazyTail) if n>0 =>
        cons(head, lazyTail.take(n-1))
      case cons(head, lazyTail) if n<=0 => empty[A]
      case Empty => empty[A]
  }
}

Presenter Notes

Example: from

def from(i: Int): Stream[Int] = cons(i, from(i + 1))
Stream.from(0).take(4)
//???

Presenter Notes

from(0).take(4)
cons(0, from(1).take(3))
cons(0, cons(1, from(2).take(2)))
cons(0, cons(1, cons(2, from(3).take(1))))
cons(0, cons(1, cons(2, cons(3, from(4).take(0)))))
cons(0, cons(1, cons(2, cons(3, Empty))))

Presenter Notes

Example: Transposition

def foo(i: Int) = {println(i); i + 10}
def bar(i: Int) = {println(i); i % 2 == 0}
List(1,2,3).map(foo).filter(bar)
//1
//2
//3
//11
//12
//13
//res0: List[Int] = List(12)

Presenter Notes

Stream(1,2,3).map(foo).filter(bar).take(1)
//1
//11
//2
//12
//3
//13
//res1: Stream[Int] = Stream(12)

Presenter Notes

foldRight

Recall from our folds tutorial that foldRight on a list looks like this:

trait List[+A] {
  def foldRight[B](z: B)
    (f: (A, B) => B): B =
    this match {
      case Nil => z
      case Cons(a, tail) =>
            f(a, tail.foldRight(z)(f))
    }
  }

Presenter Notes

We can use the same pattern to create an (unsafe) foldRight for our Stream:

trait Stream[+A] {
  def foldRight[B](z: => B)
      (f: (A, => B) => B): B =
    this match {
        case Empty => z
        case cons(head, lazyTail) =>
          f(head, lazyTail.foldRight(z)(f))
      }
}

Presenter Notes

Here is the stack-safe trampolined version from Cats:

def foldRight[A, B](lb: Eval[B])
  (f: (A, Eval[B]) => Eval[B]): Eval[B] =
  Now(this).flatMap { s =>
    if (s.isEmpty) lb
    else f(s.head, Eval.defer(s.tail.foldRight(lb)(f)))
  }

Note that we don't use pattern matching to deconstruct the stream, since that would needlessly force evaluation of the tail.

Presenter Notes

Homework

Read Chapter 6 of Functional Programming in Scala.

Presenter Notes