Let’s Get Lazy

If you’re new to lazy evaluation, you probably have used it without really calling it that. Many languages support short-circuit evaluation for conditions. If the evaluation of an argument in a condition with multiple && or || is enough to determine the truth, then the remaining arguments in that expression are not evaluated. Here’s an example of a simple short-circuit evaluation.

Parallel/shortCircuit.scala
 
def​ expensiveComputation() = {
 
println(​"...assume slow operation..."​)
 
false
 
}
 
 
def​ evaluate(input: ​Int​) = {
 
println(s​"evaluate called with $input"​)
 
if​(input >= 10 && expensiveComputation())
 
println(​"doing work..."​)
 
else
 
println(​"skipping"​)
 
}
 
 
evaluate(0)
 
evaluate(100)

The expensiveComputation function will not be executed from within the evaluate function if the value of the parameter is less than 10, as we see in the output:

 
evaluate called with 0
 
skipping
 
evaluate called with 100
 
...assume slow operation...
 
skipping

We can say that the program was pretty lazy in evaluating the expensiveComputation method. The function is evaluated only if input is 10 or greater. The program is not utterly lazy, however. To see this let’s change that code just a bit.

 
val​ perform = expensiveComputation()
 
if​(input >= 10 && perform)

We first invoked the expensiveComputation function, stored the result in an immutable variable named perform, and then used that value in the condition. When we run this modified version, even if the value of perform is not needed or used, the program eagerly evaluates the function as we see from the output—how sad.

 
evaluate called with 0
 
...assume slow operation...
 
skipping
 
evaluate called with 100
 
...assume slow operation...
 
skipping

Scala didn’t look ahead to see that the result of this computation may not really be needed, but we can tell it to be lazy, to postpone evaluating the value until it’s really needed. And doing that takes almost no effort—after all, you don’t want to work hard to be lazy.

Let’s make one more change to the code that would enable lazy evaluation of the variable.

 
lazy​ ​val​ perform = expensiveComputation()
 
if​(input >= 10 && perform)

We declared the immutable variable perform as lazy. This tells the Scala compiler to delay the binding of the variable to its value, until that value is used. If we never used that value, then the variable goes unbound, and thus, the function to yield the value is never evaluated. We can see this behavior from the output:

 
evaluate called with 0
 
skipping
 
evaluate called with 100
 
...assume slow operation...
 
skipping

You can mark any variable as lazy and the binding to its value will be postponed until the first use of that variable.

That seems nifty, why not mark all variables as lazy in that case? The reason is side effects. Suppose that the computations that will yield the values for lazy variables don’t have any side effects—that is, they don’t affect any external state nor are they affected by any external state. In that case, we should have no problem with the order in which they’re evaluated. However, if the computations have side effects, then the order in which the variables are bound matters. To understand this better, let’s look at an example with two lazy variables.

Parallel/lazyOrder.scala
 
import​ scala.io._
 
 
def​ read = StdIn.readInt()
 
 
lazy​ ​val​ first = read
 
lazy​ ​val​ second = read
 
 
if​(Math.random() < 0.5)
 
second
 
 
println(first - second)

The read function reads an Int value from the console. We call that function twice but assign the result of the calls to two variables, first and second, respectively. Since the two variables are declared as lazy neither of them will be bound to their values at this time. Then, if the value of the call to random is less than 0.5, the second variable is referenced in the body of the if statement, and this causes the variable second to be bound and evaluated before the first variable. When we run this code, about half the time the variable second will be referenced first and so it will be bound first. About the other half the time, however, the variable first will be bound before the variable second. As a result, the values read from the calls to the read function will be bound to the two variables in a random order. The result in this case is that non-commutative computations will become unpredictable.

Let’s run the code twice, each time with exactly the same input, 1 and 2 in that sequence, and take a look at the output:

 
> scala lazyOrder.scala
 
1
 
2
 
1
 
> scala lazyOrder.scala
 
1
 
2
 
-1
 
>

It takes no effort to declare variables lazy, but you do have to be quite careful what you make lazy.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.209.112