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.
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.
We typically hear of two such evaluation models: eager and lazy.
Eager computations happen immediately, whereas lazy computations happen only upon access.
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
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
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
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
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
The three behaviors are summarized below:
| Eager | Lazy | |
|---|---|---|
| Memoized | `val, Eval.now` | `lazy val, Eval.later` |
| Not Memoized | - | `def, Eval.always` |
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
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
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
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
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
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.
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) }
}
}
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)
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
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]
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]
The most important difference between the two:
Cons of List is eagerCons of Stream is lazyLike lists, streams are monadic. Non-empty streams (e.g. zippers) are also commonly used comonads. More on this later.
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
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
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>)
Use the cons smart constructor to implement a sine wave sampled at multiples of π/3 radians.
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 }
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)
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
...
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]
}
}
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]
}
}
fromdef from(i: Int): Stream[Int] = cons(i, from(i + 1))
Stream.from(0).take(4)
//???
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))))
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)
Stream(1,2,3).map(foo).filter(bar).take(1)
//1
//11
//2
//12
//3
//13
//res1: Stream[Int] = Stream(12)
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))
}
}
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))
}
}
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.
Read Chapter 6 of Functional Programming in Scala.
| Lecture 5: Laziness and Streams | 1 |
|---|---|
| - | 2 |
| Eval Monad | 3 |
| - | 4 |
| - | 5 |
| - | 6 |
| - | 7 |
| - | 8 |
| - | 9 |
| - | 10 |
| - | 11 |
| - | 12 |
| - | 13 |
| - | 14 |
| - | 15 |
| - | 16 |
| - | 17 |
| - | 18 |
| - | 19 |
| Streams | 20 |
| - | 21 |
| - | 22 |
| - | 23 |
| - | 24 |
| - | 25 |
| Exercise | 26 |
| - | 27 |
| - | 28 |
| - | 29 |
| Exercise | 30 |
| - | 31 |
Example: from |
32 |
| - | 33 |
| Example: Transposition | 34 |
| - | 35 |
| foldRight | 36 |
| - | 37 |
| - | 38 |
| Homework | 39 |
| Table of Contents | t |
|---|---|
| Exposé | ESC |
| Full screen slides | e |
| Presenter View | p |
| Source Files | s |
| Slide Numbers | n |
| Toggle screen blanking | b |
| Show/hide slide context | c |
| Notes | 2 |
| Help | h |