Lesson 14 / 25
Lazy Evaluation, Generators and Iterators
Compute values only when they are needed.
Eager versus lazy
Eager evaluation computes values immediately; array.map(f).filter(g) builds a full intermediate array at each step. Lazy evaluation delays work until a value is requested, which allows infinite sequences and stops early when only a few results are needed. Haskell is lazy by default. JavaScript gets laziness from generators (function* with yield) and the iterator protocol; Python from generators and itertools. Newer JavaScript engines also provide iterator helpers such as Iterator.prototype.map and take; check support for your runtime. The trade-off: laziness makes the timing of work, and of any side effects, harder to predict.
Lazy pipelines with generators
Only as many values as requested are ever computed.
function* naturals(): Generator<number> {
let n = 1;
while (true) yield n++; // infinite, but lazy
}
function* mapIter<A, B>(xs: Iterable<A>, f: (a: A) => B) {
for (const x of xs) yield f(x);
}
function* filterIter<A>(xs: Iterable<A>, p: (a: A) => boolean) {
for (const x of xs) if (p(x)) yield x;
}
function* take<A>(xs: Iterable<A>, n: number) {
if (n <= 0) return;
for (const x of xs) {
yield x;
if (--n === 0) return;
}
}
const firstFiveEvenSquares = [
...take(filterIter(mapIter(naturals(), n => n * n), n => n % 2 === 0), 5),
];
// [4, 16, 36, 64, 100]; the infinite source is pulled only as far as neededThe same idea elsewhere
Python: itertools.islice((n * n for n in itertools.count(1) if n % 2 == 0), 5). Haskell: take 5 (filter even (map (^2) [1..])) works directly because lists are lazy.
Quick check: What does lazy evaluation make possible?
- Avoiding the need for functions
- Guaranteed faster code in every case
- Running side effects in a predictable order
- Working with infinite sequences and stopping as soon as enough results exist
Answer
Working with infinite sequences and stopping as soon as enough results exist — Work happens on demand.