Skip to content
Merged
14 changes: 2 additions & 12 deletions Lazy Evaluation/Lazy Evaluation/task.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

## Lazy Evaluation

The proposed `Stream` implementation suffers from a serious potential performance
The proposed `LazyList` implementation suffers from a serious potential performance
problem: If `tail` is called several times, the corresponding stream
will be recomputed each time.

Expand All @@ -15,7 +15,7 @@ We call this scheme *lazy evaluation* (as opposed to *by-name evaluation* in
the case where everything is recomputed, and *strict evaluation* for normal
parameters and `val` definitions.)

### Lazy Evaluation in Scala
### Lazy Evaluation in Scala

Haskell is a functional programming language that uses lazy evaluation by default.

Expand All @@ -24,16 +24,6 @@ with the `lazy val` form:

lazy val x = expr

## Lazy Vals and Streams

Using a lazy value for `tail`, `Stream.cons` can be implemented more efficiently:

def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] {
def head = hd
lazy val tail = tl
}

## Exercise

Complete the `y` variable declaration for it to be lazy.
14 changes: 14 additions & 0 deletions Lazy Evaluation/Lazy Lists/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

lazy val `Lazy-Lists` = (project in file("."))
.settings(
scalaSource in Compile := baseDirectory.value / "src",
scalaSource in Test := baseDirectory.value / "test",
libraryDependencies ++= Seq(
"org.scala-exercises" %% "exercise-compiler" % "0.6.7",
"org.scala-exercises" %% "definitions" % "0.6.7",
"com.chuusai" %% "shapeless" % "2.3.7",
"org.scalatest" %% "scalatest" % "3.2.9",
"org.scalacheck" %% "scalacheck" % "1.15.4",
"org.scalatestplus" %% "scalacheck-1-14" % "3.2.2.0",
"com.github.alexarchambault" %% "scalacheck-shapeless_1.14" % "1.2.5"
))
18 changes: 18 additions & 0 deletions Lazy Evaluation/Lazy Lists/src/LazyEvaluation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import scala.collection.compat.immutable.LazyList

object LazyEvaluation {
var rec = 0
def llRange(lo: Int, hi: Int): LazyList[Int] = {
rec = rec + 1
if (lo >= hi) LazyList.empty
else LazyList.cons(lo, llRange(lo + 1, hi))
}
def main(args: Array[String]): Unit = {
llRange(1, 10).take(3).toList
println(rec)
llRange(1, 10).take(5).toList
println(rec)
llRange(1, 10).take(3).toList
println(rec)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ files:
- name: src/LazyEvaluation.scala
visible: true
placeholders:
- offset: 208
length: 23
placeholder_text: /*call the streamRange for the rest of the list*/
- offset: 218
length: 19
placeholder_text: /*call the llRange for the rest of the list*/
- name: test/Test.scala
visible: false
151 changes: 151 additions & 0 deletions Lazy Evaluation/Lazy Lists/task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@

## Motivation

Consider the following program that finds the second prime number between 1000 and 10000:

((1000 to 10000) filter isPrime)(1)

This is *much* shorter than the recursive alternative:


def nthPrime(from: Int, to: Int, n: Int): Int =
if (from >= to) throw new Error("no prime")
else if (isPrime(from))
if (n == 1) from else nthPrime(from + 1, to, n - 1)
else nthPrime(from + 1, to, n)

def secondPrime(from: Int, to: Int) = nthPrime(from, to, 2)

But from a standpoint of performance, the first version is pretty bad; it constructs
*all* prime numbers between `1000` and `10000` in a list, but only ever looks at
the first two elements of that list.

Reducing the upper bound would speed things up, but risks that we miss the
second prime number altogether.

## Delayed Evaluation

However, we can make the short-code efficient by using a trick:

- Avoid computing the tail of a sequence until it is needed for the evaluation
result (which might be never)

This idea is implemented in a new class, the `LazyList`.

LazyLists are similar to lists, but their elements are evaluated only ''on demand''.

## Defining LazyLists

LazyLists are defined from a constructor `LazyList.cons`.

For instance,

val xs = LazyList.cons(1, LazyList.cons(2, LazyList.empty))

## LazyList Ranges

Let's try to write a function that returns a `LazyList` representing a range of numbers
between `lo` and `hi`:

def llRange(lo: Int, hi: Int): LazyList[Int] =
if (lo >= hi) LazyList.empty
else LazyList.cons(lo, llRange(lo + 1, hi))

Compare to the same function that produces a list:

def listRange(lo: Int, hi: Int): List[Int] =
if (lo >= hi) Nil
else lo :: listRange(lo + 1, hi)

The functions have almost identical structure yet they evaluate quite differently.

- `listRange(start, end)` will produce a list with `end - start` elements and return it.
- `llRange(start, end)` returns a single object of type `LazyList` with `start` as head element.
- The other elements are only computed when they are needed, where “needed” means that someone calls `tail` on the stream.

## Methods on LazyLists

`LazyList` supports almost all methods of `List`.

For instance, to find the second prime number between 1000 and 10000:

(llRange(1000, 10000) filter isPrime)(1)

The one major exception is `::`.

`x :: xs` always produces a list, never a lazy list.

There is however an alternative operator `#::` which produces a lazy list.

x #:: xs == LazyList.cons(x, xs)

`#::` can be used in expressions as well as patterns.

## Implementation of LazyLists

The implementation of lazy lists is quite close to the one of lists.

Here's the class `LazyList`:

final class LazyList[+A] ... extends ... {
override def isEmpty: Boolean = ...
override def head: A = ...
override def tail: LazyList[A] = ...
}

As for lists, all other methods can be defined in terms of these three.

Concrete implementations of streams are defined in the `LazyList.State` companion object.
Here's a first draft:

private object State {
object Empty extends State[Nothing] {
def head: Nothing = throw new NoSuchElementException("head of empty lazy list")
def tail: LazyList[Nothing] = throw new UnsupportedOperationException("tail of empty lazy list")
}

final class Cons[A](val head: A, val tail: LazyList[A]) extends State[A]
}

The only important difference between the implementations of `List` and `LazyList`
concern `tail`, the second parameter of `LazyList.cons`.

For lazy lists, this is a by-name parameter: the type of `tail` starts with `=>`. In such
a case, this parameter is evaluated by following the rules of the call-by-name model.

That's why the second argument to `LazyList.cons` is not evaluated at the point of call.

Instead, it will be evaluated each time someone calls `tail` on a `LazyList` object.

In Scala 2.13, LazyList (previously Stream) became fully lazy from head to tail. To make it possible,
methods (`filter`, `flatMap`...) are implemented in a way where the head is not being evaluated if is
not explicitly indicated.

For instance, here's `filter`:

object LazyList extends SeqFactory[LazyList] {
private def filterImpl[A](ll: LazyList[A], p: A => Boolean, isFlipped: Boolean): LazyList[A] = {
// DO NOT REFERENCE `ll` ANYWHERE ELSE, OR IT WILL LEAK THE HEAD
var restRef = ll // val restRef = new ObjectRef(ll)
newLL {
var elem: A = null.asInstanceOf[A]
var found = false
var rest = restRef // var rest = restRef.elem
while (!found && !rest.isEmpty) {
elem = rest.head
found = p(elem) != isFlipped
rest = rest.tail
restRef = rest // restRef.elem = rest
}
if (found) sCons(elem, filterImpl(rest, p, isFlipped)) else State.Empty
}
}

## Exercise

Consider the modification of `llRange` given in the code editor. When you write
`llRange(1, 10).take(3).toList` what is the value of `rec`?

Be careful, head is evaluating too!
14 changes: 14 additions & 0 deletions Lazy Evaluation/Lazy Lists/test/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import LazyEvaluation.{llRange, rec}
import org.scalatest.matchers.should.Matchers
import org.scalatest.refspec.RefSpec

class Test extends RefSpec with Matchers{
def `test encoding`(): Unit = {
llRange(1, 10).take(3).toList
rec shouldBe 4
llRange(1, 10).take(1).toList
rec shouldBe 6
llRange(1, 10).take(2).toList
rec shouldBe 9
}
}
7 changes: 0 additions & 7 deletions Lazy Evaluation/Stream Ranges/build.sbt

This file was deleted.

21 changes: 0 additions & 21 deletions Lazy Evaluation/Stream Ranges/src/LazyEvaluation.scala

This file was deleted.

Loading