Skip to content

Commit d482144

Browse files
author
Javier de Silóniz Sandino
committed
First batch of exercises regarding Streams
1 parent 4026f3f commit d482144

File tree

5 files changed

+651
-1
lines changed

5 files changed

+651
-1
lines changed

src/main/scala/fpinscalalib/FPinScalaLibrary.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ object FPinScalaLibrary extends Library {
1515
override def sections = scala.collection.immutable.List(
1616
GettingStartedWithFPSection,
1717
FunctionalDataStructuresSection,
18-
ErrorHandlingSection
18+
ErrorHandlingSection,
19+
StrictnessAndLazinessSection
1920
)
2021
}
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
package fpinscalalib
2+
3+
import fpinscalalib.customlib.laziness._
4+
import fpinscalalib.customlib.laziness.Stream
5+
import fpinscalalib.customlib.laziness.Stream._
6+
import org.scalatest.{FlatSpec, Matchers}
7+
import fpinscalalib.customlib.laziness.ExampleHelper._
8+
9+
/** @param name sctriness_and_laziness
10+
*/
11+
object StrictnessAndLazinessSection extends FlatSpec with Matchers with org.scalaexercises.definitions.Section {
12+
13+
/**
14+
* = Strict and non-strict functions =
15+
*
16+
* Non-strictness is a property of a function. To say a function is `non-strict` just means that the function may
17+
* choose not to evaluate one or more of its arguments. In contrast, a `strict` function always evaluates its
18+
* arguments. Unless we tell it otherwise, any function definition in Scala will be strict. As an example, consider
19+
* the following function:
20+
*
21+
* {{{
22+
* def square(x: Double): Double = x * x
23+
* }}}
24+
*
25+
* When you invoke `square(41.0 + 1.0)`, the function `square` will receive the evaluated value of `42.0` because
26+
* it's strict. If you invoke `square(sys.error("failure"))`, you’ll get an exception before square has a chance to do
27+
* anything, since the `sys.error("failure")` expression will be evaluated before entering the body of square.
28+
*
29+
* For example, the short-circuiting Boolean functions `&&` and `||`, found in many programming languages including
30+
* Scala, are non-strict. You can think of them as functions that may choose not to evaluate their arguments. The
31+
* function `&&` takes two `Boolean` arguments, but only evaluates the second argument if the first is `true`:
32+
*
33+
* {{{
34+
* false && { println("!!"); true } // does not print anything
35+
* }}}
36+
*
37+
* And `||` only evaluates its second argument if the first is `false`:
38+
*
39+
* {{{
40+
* true || { println("!!"); false } // doesn't print anything either
41+
* }}}
42+
*
43+
* Another example of non-strictness is the `if` control construct in Scala. It can be thought of as a function
44+
* accepting three parameters: a condition of type `Boolean`, an expression of some type `A` to return in the case that
45+
* the condition is `true`, and another expression of the same type `A` to return if the condition is `false`. We'd
46+
* say that the if function is strict in its condition parameter, since it’ll always evaluate the condition to
47+
* determine which branch to take, and non-strict in the two branches for the `true` and `false` cases,
48+
* since it’ll only evaluate one or the other based on the condition. We can re-implement it the following way:
49+
*
50+
* {{{
51+
* def if2[A](cond: Boolean, onTrue: () => A, onFalse: () => A): A =
52+
* if (cond) onTrue() else onFalse()
53+
* }}}
54+
*
55+
*
56+
*
57+
* Play a little bit with it, note what happens when you force a `true` or a `false` condition on the `if2` call:
58+
*/
59+
60+
def if2Assert(res0: Boolean): Unit = {
61+
if2(res0, () => true, () => { sys.error("Exception occurred: if2 call went through the false branch") })
62+
}
63+
64+
/**
65+
* The arguments we’d like to pass unevaluated have a `() =>` immediately before their type. A value of type
66+
* `() => A` is a function that accepts zero arguments and returns an `A`. In general, the unevaluated form of an
67+
* expression is called a `thunk`, and we can force the thunk to evaluate the expression and get a result. We do
68+
* so by invoking the function, passing an empty argument list, as in `onTrue()` or `onFalse()`. Likewise, callers
69+
* of `if2` have to explicitly create thunks, and the syntax follows the same conventions as the function literal
70+
* syntax we’ve already seen. But this is such a common case that Scala provides some nicer syntax:
71+
*
72+
* {{{
73+
* def if2[A](cond: Boolean, onTrue: => A, onFalse: => A): A =
74+
* if (cond) onTrue else onFalse
75+
*
76+
* if2(false, sys.error("fail"), 3)
77+
* }}}
78+
*
79+
* With either syntax, an argument that’s passed unevaluated to a function will be evalu- ated once for each place
80+
* it’s referenced in the body of the function. That is, Scala won’t (by default) cache the result of evaluating an
81+
* argument:
82+
*
83+
* {{{
84+
* def maybeTwice(b: Boolean, i: => Int) = if (b) i+i else 0
85+
*
86+
* scala> val x = maybeTwice(true, { println("hi"); 1 + 41 })
87+
* hi
88+
* hi
89+
* x: Int = 84
90+
* }}}
91+
*
92+
* Here, i is referenced twice in the body of maybeTwice, and we’ve made it particularly obvious that it’s evaluated
93+
* each time by passing the block `{ println("hi"); 1+41 }`, which prints hi as a side effect before returning a
94+
* result of `42`. The expression `1 + 41` will be computed twice as well. We can cache the value explicitly if we
95+
* wish to only evaluate the result once, by using the `lazy` keyword:
96+
*
97+
* {{{
98+
* def maybeTwice2(b: Boolean, i: => Int) = {
99+
* lazy val j = i
100+
* if (b) j + j else 0
101+
* }
102+
*
103+
* scala> val x = maybeTwice2(true, { println("hi"); 1 + 41 })
104+
* hi
105+
* x: Int = 84
106+
* }}}
107+
*
108+
* Adding the `lazy` keyword to a `val` declaration will cause Scala to delay evaluation of the right-hand side of that
109+
* `lazy val` declaration until it’s first referenced. It will also cache the result so that subsequent references to
110+
* it don’t trigger repeated evaluation.
111+
*
112+
* = Lazy lists =
113+
*
114+
* We’ll explore how laziness can be used to improve the efficiency and modularity of functional programs using lazy
115+
* lists, or streams, as an example.
116+
*
117+
* {{{
118+
* sealed trait Stream[+A]
119+
* case object Empty extends Stream[Nothing]
120+
* case class Cons[+A](h: () => A, t: () => Stream[A]) extends Stream[A]
121+
*
122+
* object Stream {
123+
* def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = {
124+
* lazy val head = hd
125+
* lazy val tail = tl
126+
* Cons(() => head, () => tail)
127+
* }
128+
* def empty[A]: Stream[A] = Empty
129+
*
130+
* def apply[A](as: A*): Stream[A] =
131+
* if (as.isEmpty) empty else cons(as.head, apply(as.tail: _*))
132+
* }
133+
* }}}
134+
*
135+
* This type looks identical to our List type, except that the Cons data constructor takes `explicit` thunks
136+
* `(() => A and () => Stream[A])` instead of regular strict values. If we wish to examine or traverse the Stream,
137+
* we need to force these thunks as we did earlier in our definition of `if2`. For example:
138+
*
139+
* {{{
140+
* def headOption: Option[A] = this match {
141+
* case Empty => None
142+
* case Cons(h, t) => Some(h()) // explicit forcing of h using h()
143+
* }
144+
* }}}
145+
*
146+
*
147+
* Now let's write a few helper functions to make inspecting streams easier, starting with a function to convert a
148+
* `Stream` to a `List` (which will force its evaluation):
149+
*/
150+
151+
def streamToListAssert(res0: List[Int]): Unit = {
152+
def toList[A](s: Stream[A]): List[A] = s match {
153+
case Cons(h,t) => h() :: t().toListRecursive
154+
case _ => List()
155+
}
156+
157+
val s = Stream(1, 2, 3)
158+
toList(s) shouldBe res0
159+
}
160+
161+
/**
162+
* Let's continue by writing the function `take` for returning the fist `n` elements of a `Stream`. Note that in the
163+
* following implementation we're using `Stream`'s smart constructors `cons` and `empty`, defined as follows:
164+
*
165+
* {{{
166+
* def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = {
167+
* lazy val head = hd
168+
* lazy val tail = tl
169+
* Cons(() => head, () => tail)
170+
* }
171+
*
172+
* def empty[A]: Stream[A] = Empty
173+
* }}}
174+
*/
175+
176+
def streamTakeAssert(res0: Int): Unit = {
177+
def take[A](s: Stream[A], n: Int): Stream[A] = s match {
178+
case Cons(h, t) if n > 0 => cons[A](h(), t().take(n - res0))
179+
case Cons(h, _) if n == 0 => cons[A](h(), Stream.empty)
180+
case _ => Stream.empty
181+
}
182+
183+
take(Stream(1, 2, 3), 2).toList shouldBe List(1, 2)
184+
}
185+
186+
/**
187+
* `drop` is similar to `take`, but instead skips the first `n` elements of a `Stream`:
188+
*/
189+
190+
def streamDropAssert(res0: Int): Unit = {
191+
def drop[A](s: Stream[A], n: Int): Stream[A] = s match {
192+
case Cons(_, t) if n > 0 => t().drop(n - res0)
193+
case _ => s
194+
}
195+
196+
drop(Stream(1, 2, 3, 4, 5), 2).toList shouldBe List(3, 4, 5)
197+
}
198+
199+
/**
200+
* We can also implement `takeWhile` for returning all starting elements of a `Stream` that match the given predicate:
201+
*/
202+
203+
def streamTakeWhileAssert(res0: List[Int], res1: List[Int]): Unit = {
204+
def takeWhile[A](s: Stream[A], f: A => Boolean): Stream[A] = s match {
205+
case Cons(h,t) if f(h()) => cons(h(), t() takeWhile f)
206+
case _ => Stream.empty
207+
}
208+
209+
takeWhile(Stream(1, 2, 3, 4, 5), (x: Int) => x < 3).toList shouldBe res0
210+
takeWhile(Stream(1, 2, 3, 4, 5), (x: Int) => x < 0).toList shouldBe res1
211+
}
212+
213+
/**
214+
* = Separating program description from evaluation =
215+
*
216+
* Laziness lets us separate the description of an expression from the evaluation of that expression. This gives us
217+
* a powerful ability — we may choose to describe a “larger” expression than we need, and then evaluate only a portion
218+
* of it. As an example, let’s look at the function exists that checks whether an element matching a `Boolean`
219+
* function exists in this Stream:
220+
*
221+
* {{{
222+
* def exists(p: A => Boolean): Boolean = this match {
223+
* case Cons(h, t) => p(h()) || t().exists(p)
224+
* case _ => false
225+
* }
226+
* }}}
227+
*
228+
* Note that `||` is non-strict in its second argument. If `p(h())` returns `true`, then exists terminates the
229+
* traversal early and returns true as well. Remember also that the tail of the stream is a `lazy val`. So not only
230+
* does the traversal terminate early, the tail of the stream is never evaluated at all!
231+
*
232+
* We can implement a `foldRight` similar to that of `List`, but expressed in a lazy way:
233+
*
234+
* {{{
235+
* def foldRight[B](z: => B)(f: (A, => B) => B): B = this match {
236+
* case Cons(h,t) => f(h(), t().foldRight(z)(f))
237+
* case _ => z
238+
* }
239+
* }}}
240+
*
241+
* This looks very similar to the `foldRight` we wrote for `List`, but note how our combining function `f` is
242+
* non-strict in its second parameter. If `f` chooses not to evaluate its second parameter, this terminates the
243+
* traversal early. We can see this by using `foldRight` to implement `exists`:
244+
*
245+
* {{{
246+
* def exists(p: A => Boolean): Boolean = foldRight(false)((a, b) => p(a) || b)
247+
* }}}
248+
*
249+
* Let's use this to implement `forAll`, which checks that all elements in the `Stream` match a given predicate. Note
250+
* that the implementation will terminate the traversal as soon as it encounters a nonmatching value.
251+
*/
252+
253+
def streamForAllAssert(res0: Boolean): Unit = {
254+
def forAll[A](s: Stream[A], f: A => Boolean): Boolean =
255+
s.foldRight(res0)((a, b) => f(a) && b)
256+
257+
forAll(Stream(1, 2, 3), (x: Int) => x % 2 == 0) shouldBe false
258+
forAll(Stream("a", "b", "c"), (x: String) => x.size > 0) shouldBe true
259+
}
260+
261+
/**
262+
* Let's put `foldRight` to good use, by implementing `takeWhile` based on it:
263+
*
264+
* {{{
265+
* def takeWhile_1(f: A => Boolean): Stream[A] =
266+
* foldRight(empty[A])((h,t) =>
267+
* if (f(h)) cons(h,t)
268+
* else empty)
269+
* }}}
270+
*
271+
* We can also do the same with `headOption`:
272+
*
273+
* {{{
274+
* def headOption: Option[A] = foldRight(None: Option[A])((h,_) => Some(h))
275+
* }}}
276+
*
277+
* Implementations for `map`, `filter`, `append` and `flatMap` using `foldRight` should sound familiar already:
278+
*
279+
* {{{
280+
* def map[B](f: A => B): Stream[B] = foldRight(empty[B])((h,t) => cons(f(h), t))
281+
*
282+
* def filter(f: A => Boolean): Stream[A] = foldRight(empty[A])((h,t) =>
283+
* if (f(h)) cons(h, t)
284+
* else t)
285+
*
286+
* def append[B>:A](s: => Stream[B]): Stream[B] = foldRight(s)((h,t) => cons(h,t))
287+
*
288+
* def flatMap[B](f: A => Stream[B]): Stream[B] = foldRight(empty[B])((h,t) => f(h) append t)
289+
* }}}
290+
*
291+
* Note that these implementations are incremental — they don’t fully generate their answers. It’s not until some
292+
* other computation looks at the elements of the resulting `Stream` that the computation to generate that `Stream`
293+
* actually takes place. Let's look at a simplified program trace for the next piece of code.
294+
*
295+
* {{{
296+
* Stream(1, 2, 3, 4).map(_ + 10).filter(_ % 2 == 0)
297+
* }}}
298+
*
299+
* We'll convert that expression to a `List` to force evaluation. Try to follow what's happening in each step:
300+
*/
301+
302+
def streamTraceAssert(res0: Int, res1: Stream[Int], res2: Stream[Int], res3: Int, res4: Stream[Int], res5: Int): Unit = {
303+
val startingPoint = Stream(1, 2, 3, 4).map(_ + 10).filter(_ % 2 == 0).toList
304+
305+
// Apply map to the first element:
306+
val step1 = cons(res0, Stream(2,3,4).map(_ + 10)).filter(_ % 2 == 0).toList
307+
// Apply filter to the first element:
308+
val step2 = res1.map(_ + 10).filter(_ % 2 == 0).toList
309+
// Apply map to the second element:
310+
val step3 = cons(12, res2.map(_ + 10)).filter(_ % 2 == 0).toList
311+
// Apply filter to the second element. Produce the first element of the result:
312+
val step4 = 12 :: Stream(3,4).map(_ + 10).filter(_ % 2 == 0).toList
313+
val step5 = 12 :: cons(res3, res4.map(_ + 10)).filter(_ % 2 == 0).toList
314+
val step6 = 12 :: Stream(4).map(_ + 10).filter(_ % 2 == 0).toList
315+
val step7 = 12 :: cons(res5, Stream[Int]().map(_ + 10)).filter(_ % 2 == 0).toList
316+
// Apply filter to the fourth element and produce the final element of the result.
317+
val step8 = 12 :: 14 :: Stream[Int]().map(_ + 10).filter(_ % 2 == 0).toList
318+
// map and filter have no more work to do, and the empty stream becomes the empty list.
319+
val finalStep = 12 :: 14 :: List()
320+
321+
startingPoint shouldBe step1
322+
step1 shouldBe step2
323+
step2 shouldBe step3
324+
step3 shouldBe step4
325+
step4 shouldBe step5
326+
step5 shouldBe step6
327+
step6 shouldBe step7
328+
step7 shouldBe step8
329+
step8 shouldBe finalStep
330+
}
331+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package fpinscalalib.customlib.laziness
2+
3+
object ExampleHelper {
4+
def if2[A](cond: Boolean, onTrue: () => A, onFalse: () => A): A = if (cond) onTrue() else onFalse()
5+
}

0 commit comments

Comments
 (0)