Lazy Pipelines

The Course
•
4 min read

Array.map eagerly creates a result array. A generator transformation can instead yield one transformed value for each requested input. Compose small synchronous generators to filter and map a source without materializing intermediate arrays.

Laziness changes when side effects happen and when errors become visible. A pipeline can be constructed successfully but fail only when consumed. A consumer that asks for one result should not cause the entire source to be processed. This is useful for large finite data sources and bounded workers.

Do not confuse laziness with low total memory in every API. Spreading the entire pipeline into an array collects everything. An infinite source still cannot be fully collected. Likewise, the final task pool bounds active mapper calls but retains all output for ordered return; it is not a streaming-output API.

Use explicit loops inside generator helpers while learning. The yield sites show exactly which work happens for each request, and generator cleanup composes with early loop exit.

Predict, run, and explain

Write the output and trace the important state changes before checking it. Then run the program. Each editor is independent. Async examples end with main() so this editor waits for their work.

Predict, then run JavaScript
function* doubled(values) {
  for (const value of values) {
    console.log(`read ${value}`);
    yield value * 2;
  }
}

const pipeline = doubled([1, 2, 3]);

console.log("created");
console.log(pipeline.next().value);
pipeline.return();
Check your prediction
created
read 1
2

Observe work that a consumer never requested

A lazy transform performs work only as downstream demand advances it. This can avoid processing unused items, but it also changes when effects and errors occur. A function returning an iterator has not necessarily completed its calculations when it returns.

Composition order matters. Filtering before transforming can reduce mapper calls; transforming before filtering may be required if the predicate depends on the transformed value. Preserve the intended values and order before claiming an optimization.

Observe work that a consumer never requested: trace JavaScript
let calls = 0;

function* doubled(values) {
  for (const value of values) {
    calls++;
    yield value * 2;
  }
}

const cursor = doubled([1, 2, 3]);

console.log(calls);
console.log(cursor.next().value);
console.log(calls);
cursor.return();
Check your prediction
0
2
1

Only one transformation ran. The unused inputs never reached the multiplication. This is useful evidence of laziness, beyond merely returning an object with next.

Observe work that a consumer never requested: contrast JavaScript
const values = [1, 2, 3, 4];
let first = 0;
let second = 0;
const a = values
  .filter(n => n > 2)
  .map(n => {
    first++;
    return n * 2;
  });
const b = values
  .map(n => {
    second++;
    return n * 2;
  })
  .filter(n => n > 4);

console.log(a.join(","), b.join(","));
console.log(first, second);
Check your prediction
6,8 6,8
2 4

These predicates are deliberately equivalent for this transformation. Equal outputs do not mean equal work. For a different predicate or effectful mapper, reordering may change the contract.

Try the rule on new inputs

Count mapper calls for zero, one and all requested outputs. Add a failure on a later unused input and explain when it should become observable. Do not confuse lazy input consumption with constant result memory.

Your turn

Exercise

Lazy Pipelines: practise

Write a generator positive(values) that yields only values greater than zero. Consume [-1, 0, 2, 3] and print 2,3 using array spread and join. The generator must not mutate its input.

Your workspace JavaScript
// Yield matching values; collect only at the final display boundary.
Hint 1: the idea

A generator does not need to accumulate an output array.

Hint 2: a first step

Use for/of and an if condition around yield.

Show one solution
function* positive(values) {
  for (const value of values)
    if (value > 0) {
      yield value;
    }
}

console.log([...positive([-1, 0, 2, 3])].join(","));
2,3

Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?

Change it and test it

Add a transformation that squares each value, then compose it after positive. Log input reads and stop after one yielded result. Which values were actually examined? Explain how this differs from mapping and filtering an entire array first.

Was this article helpful?

Your response is saved on this device.