Introduction
The concept of lazy evaluation is not difficult. So, this time, I will focus on its usefulness.
Background Knowledge
If you are confused between expression and evaluation, please refer to the following article: https://jurogrammer.tistory.com/129
Definition
Lazy evaluation is a strategy where the evaluation of an expression is delayed until its value is needed. It is usually used to improve performance. Let’s see how delaying evaluation can improve performance.
Example of Performance Improvement
Here is an example where a value is only used if a certain condition is met.
|
|
As you can see, cpuBoundJop is executed even when n is negative. So, running businessLogic always takes 3 seconds, regardless of n. This is unnecessary computation when n is negative. Let’s improve this with lazy evaluation.
How to Implement Lazy Evaluation? (Simulating laziness in eager languages)
Haskell is lazy by default, but most languages are eager. In eager languages, you can implement lazy evaluation by passing a function. The function is evaluated only when called, so you can choose when to evaluate.
Improvement
|
|
Now, the function is only called when n is positive, so the computation is delayed as needed.
A function that returns an expression like this is called a supplier.
To discuss performance further, let’s look at a filter example. We’ll implement the following problem in JavaScript, Ruby, and Java, and see how many times the log is printed.
Filter Example
- Among the natural numbers from 1 to 10
- Greater than 3
- Multiples of 5
- Find the first number
JavaScript
|
|
How many times is console.log called? (excluding the last value check)
The answer is 17 times:
- 1~10 are checked with isGreaterThan3 (10 times)
- The array 4~10 is returned
- 4~10 are checked with isMultipleOf5 (7 times)
Ruby
|
|
Ruby is also 17 times.
Output:
|
|
Java
|
|
In Java, it’s only 7 times! Since we only need the first value, there’s no need to check all values. The calculation is performed at the terminal operation (findFirst). Java implements lazy evaluation properly.
Output:
|
|
Another Use Case: Infinite Stream
In the previous example, we declared a collection of natural numbers from 1 to 10. But what if we want all natural numbers? We can’t store them all in memory! Instead, we can declare something that represents natural numbers and fetch values as needed.
Let’s implement this in JavaScript and Java.
Example
JavaScript yield
yield? https://en.wikipedia.org/wiki/Value_(mathematics) https://en.wikipedia.org/wiki/Yield_(multithreading)
In JavaScript, you can use yield to implement this. The references above help understand the concept.
|
|
This creates an infinite stream of natural numbers using yield.
Java
Java provides the iterate API, which can be written in set-builder notation.
|
|
Python range
In Python, range was eager in version 2.7, but became lazy in version 3.
python 2.7
python 3.7
You can see the difference in output.
Avoidance of Error Conditions
|
|
With lazy evaluation, the result is 2. With eager evaluation, you get a division by zero error. The difference is whether 2/0 is computed or not. It may feel odd, but think of it this way: when getting the size of a collection, you don’t need to know the values, just the count. No need to evaluate the values.
I didn’t know where to use this at first, but it hit me while writing Java test code.
|
|
In assertThrows, the second argument is a supplier that throws the error. If you just pass divide(1,0), the error occurs before assertThrows is called. That’s why lazy evaluation is used here.
The inside of assertThrows is similar to this:
|
|
Finally…
There are some issues to note:
- Memory Issue
- Delaying evaluation means you have to remember the filter function, which takes up memory. If used incorrectly, it can cause memory leaks.
- Repeated Evaluation (Memoization)
- In functional programming, there are no side effects, so the same input always gives the same output. If you’ve already computed f(3) once, you don’t need to compute it again. Memoization is used for this optimization.