Skip to content

Commit b715382

Browse files
authored
Merge pull request #40 from jetbrains-academy/sofia/new_lazy_evaluation
Sofia/new lazy evaluation
2 parents 871540b + 37e5491 commit b715382

File tree

12 files changed

+204
-205
lines changed

12 files changed

+204
-205
lines changed
Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
## Lazy Evaluation
33

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

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

18-
### Lazy Evaluation in Scala
18+
### Lazy Evaluation in Scala
1919

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

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

2525
lazy val x = expr
2626

27-
## Lazy Vals and Streams
28-
29-
Using a lazy value for `tail`, `Stream.cons` can be implemented more efficiently:
30-
31-
def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] {
32-
def head = hd
33-
lazy val tail = tl
34-
35-
}
36-
3727
## Exercise
3828

3929
Complete the `y` variable declaration for it to be lazy.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
lazy val `Lazy-Lists` = (project in file("."))
3+
.settings(
4+
scalaSource in Compile := baseDirectory.value / "src",
5+
scalaSource in Test := baseDirectory.value / "test",
6+
libraryDependencies ++= Seq(
7+
"org.scala-exercises" %% "exercise-compiler" % "0.6.7",
8+
"org.scala-exercises" %% "definitions" % "0.6.7",
9+
"com.chuusai" %% "shapeless" % "2.3.7",
10+
"org.scalatest" %% "scalatest" % "3.2.9",
11+
"org.scalacheck" %% "scalacheck" % "1.15.4",
12+
"org.scalatestplus" %% "scalacheck-1-14" % "3.2.2.0",
13+
"com.github.alexarchambault" %% "scalacheck-shapeless_1.14" % "1.2.5"
14+
))
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import scala.collection.compat.immutable.LazyList
2+
3+
object LazyEvaluation {
4+
var rec = 0
5+
def llRange(lo: Int, hi: Int): LazyList[Int] = {
6+
rec = rec + 1
7+
if (lo >= hi) LazyList.empty
8+
else LazyList.cons(lo, llRange(lo + 1, hi))
9+
}
10+
def main(args: Array[String]): Unit = {
11+
llRange(1, 10).take(3).toList
12+
println(rec)
13+
llRange(1, 10).take(5).toList
14+
println(rec)
15+
llRange(1, 10).take(3).toList
16+
println(rec)
17+
}
18+
}

Lazy Evaluation/Stream Ranges/task-info.yaml renamed to Lazy Evaluation/Lazy Lists/task-info.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ files:
55
- name: src/LazyEvaluation.scala
66
visible: true
77
placeholders:
8-
- offset: 208
9-
length: 23
10-
placeholder_text: /*call the streamRange for the rest of the list*/
8+
- offset: 218
9+
length: 19
10+
placeholder_text: /*call the llRange for the rest of the list*/
1111
- name: test/Test.scala
1212
visible: false

Lazy Evaluation/Lazy Lists/task.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
2+
## Motivation
3+
4+
Consider the following program that finds the second prime number between 1000 and 10000:
5+
6+
((1000 to 10000) filter isPrime)(1)
7+
8+
This is *much* shorter than the recursive alternative:
9+
10+
11+
def nthPrime(from: Int, to: Int, n: Int): Int =
12+
if (from >= to) throw new Error("no prime")
13+
else if (isPrime(from))
14+
if (n == 1) from else nthPrime(from + 1, to, n - 1)
15+
else nthPrime(from + 1, to, n)
16+
17+
def secondPrime(from: Int, to: Int) = nthPrime(from, to, 2)
18+
19+
But from a standpoint of performance, the first version is pretty bad; it constructs
20+
*all* prime numbers between `1000` and `10000` in a list, but only ever looks at
21+
the first two elements of that list.
22+
23+
Reducing the upper bound would speed things up, but risks that we miss the
24+
second prime number altogether.
25+
26+
## Delayed Evaluation
27+
28+
However, we can make the short-code efficient by using a trick:
29+
30+
- Avoid computing the tail of a sequence until it is needed for the evaluation
31+
result (which might be never)
32+
33+
This idea is implemented in a new class, the `LazyList`.
34+
35+
LazyLists are similar to lists, but their elements are evaluated only ''on demand''.
36+
37+
## Defining LazyLists
38+
39+
LazyLists are defined from a constructor `LazyList.cons`.
40+
41+
For instance,
42+
43+
val xs = LazyList.cons(1, LazyList.cons(2, LazyList.empty))
44+
45+
## LazyList Ranges
46+
47+
Let's try to write a function that returns a `LazyList` representing a range of numbers
48+
between `lo` and `hi`:
49+
50+
def llRange(lo: Int, hi: Int): LazyList[Int] =
51+
if (lo >= hi) LazyList.empty
52+
else LazyList.cons(lo, llRange(lo + 1, hi))
53+
54+
Compare to the same function that produces a list:
55+
56+
def listRange(lo: Int, hi: Int): List[Int] =
57+
if (lo >= hi) Nil
58+
else lo :: listRange(lo + 1, hi)
59+
60+
The functions have almost identical structure yet they evaluate quite differently.
61+
62+
- `listRange(start, end)` will produce a list with `end - start` elements and return it.
63+
- `llRange(start, end)` returns a single object of type `LazyList` with `start` as head element.
64+
- The other elements are only computed when they are needed, where “needed” means that someone calls `tail` on the stream.
65+
66+
## Methods on LazyLists
67+
68+
`LazyList` supports almost all methods of `List`.
69+
70+
For instance, to find the second prime number between 1000 and 10000:
71+
72+
(llRange(1000, 10000) filter isPrime)(1)
73+
74+
The one major exception is `::`.
75+
76+
`x :: xs` always produces a list, never a lazy list.
77+
78+
There is however an alternative operator `#::` which produces a lazy list.
79+
80+
x #:: xs == LazyList.cons(x, xs)
81+
82+
`#::` can be used in expressions as well as patterns.
83+
84+
## Implementation of LazyLists
85+
86+
The implementation of lazy lists is quite close to the one of lists.
87+
88+
Here's the class `LazyList`:
89+
90+
final class LazyList[+A] ... extends ... {
91+
override def isEmpty: Boolean = ...
92+
override def head: A = ...
93+
override def tail: LazyList[A] = ...
94+
95+
}
96+
97+
As for lists, all other methods can be defined in terms of these three.
98+
99+
Concrete implementations of streams are defined in the `LazyList.State` companion object.
100+
Here's a first draft:
101+
102+
private object State {
103+
object Empty extends State[Nothing] {
104+
def head: Nothing = throw new NoSuchElementException("head of empty lazy list")
105+
def tail: LazyList[Nothing] = throw new UnsupportedOperationException("tail of empty lazy list")
106+
}
107+
108+
final class Cons[A](val head: A, val tail: LazyList[A]) extends State[A]
109+
}
110+
111+
The only important difference between the implementations of `List` and `LazyList`
112+
concern `tail`, the second parameter of `LazyList.cons`.
113+
114+
For lazy lists, this is a by-name parameter: the type of `tail` starts with `=>`. In such
115+
a case, this parameter is evaluated by following the rules of the call-by-name model.
116+
117+
That's why the second argument to `LazyList.cons` is not evaluated at the point of call.
118+
119+
Instead, it will be evaluated each time someone calls `tail` on a `LazyList` object.
120+
121+
In Scala 2.13, LazyList (previously Stream) became fully lazy from head to tail. To make it possible,
122+
methods (`filter`, `flatMap`...) are implemented in a way where the head is not being evaluated if is
123+
not explicitly indicated.
124+
125+
For instance, here's `filter`:
126+
127+
object LazyList extends SeqFactory[LazyList] {
128+
129+
private def filterImpl[A](ll: LazyList[A], p: A => Boolean, isFlipped: Boolean): LazyList[A] = {
130+
// DO NOT REFERENCE `ll` ANYWHERE ELSE, OR IT WILL LEAK THE HEAD
131+
var restRef = ll // val restRef = new ObjectRef(ll)
132+
newLL {
133+
var elem: A = null.asInstanceOf[A]
134+
var found = false
135+
var rest = restRef // var rest = restRef.elem
136+
while (!found && !rest.isEmpty) {
137+
elem = rest.head
138+
found = p(elem) != isFlipped
139+
rest = rest.tail
140+
restRef = rest // restRef.elem = rest
141+
}
142+
if (found) sCons(elem, filterImpl(rest, p, isFlipped)) else State.Empty
143+
}
144+
}
145+
146+
## Exercise
147+
148+
Consider the modification of `llRange` given in the code editor. When you write
149+
`llRange(1, 10).take(3).toList` what is the value of `rec`?
150+
151+
Be careful, head is evaluating too!
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import LazyEvaluation.{llRange, rec}
2+
import org.scalatest.matchers.should.Matchers
3+
import org.scalatest.refspec.RefSpec
4+
5+
class Test extends RefSpec with Matchers{
6+
def `test encoding`(): Unit = {
7+
llRange(1, 10).take(3).toList
8+
rec shouldBe 4
9+
llRange(1, 10).take(1).toList
10+
rec shouldBe 6
11+
llRange(1, 10).take(2).toList
12+
rec shouldBe 9
13+
}
14+
}

Lazy Evaluation/Stream Ranges/build.sbt

Lines changed: 0 additions & 7 deletions
This file was deleted.

Lazy Evaluation/Stream Ranges/src/LazyEvaluation.scala

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)