|
| 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 | +} |
0 commit comments