Blog

JavaScript Generators for Lazy and Infinite Sequences

A normal function runs from start to finish. A generator stops at every yield and hands a value back, keeping its local variables alive until you ask for more.

That single property is what makes infinite sequences safe and lazy pipelines cheap.

Where it pauses

Calling a generator function runs none of its body. You get an object back. The body only advances when you call next().

// a generator pauses at yield
function* three() {
  console.log('start');
  yield 1;
  console.log('resumed once');
  yield 2;
  console.log('resumed twice');
  return 3;
}
const g = three();
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());

It prints:

start
{ value: 1, done: false }
resumed once
{ value: 2, done: false }
resumed twice
{ value: 3, done: true }
{ value: undefined, done: true }

Watch the order of the log lines. Nothing runs until the first next(). The return value comes back with done: true, and every call after that gives undefined.

An infinite sequence that does not hang

A while (true) loop is fine inside a generator, because it is the caller who decides when to stop asking.

// infinite sequence, no infinite loop
function* naturals() {
  let n = 1;
  while (true) yield n++;
}
function* take(iterable, count) {
  let i = 0;
  for (const value of iterable) {
    if (i++ >= count) return;
    yield value;
  }
}
console.log([...take(naturals(), 5)]);

It prints:

[ 1, 2, 3, 4, 5 ]

take pulls five values and returns. The naturals generator is left paused, and it is collected like any other object.

Lazy pipelines

Chaining map and filter on arrays builds a new array at every step. Generator versions pass one value through the whole chain at a time.

// lazy pipeline
function* map(iterable, fn) { for (const v of iterable) yield fn(v); }
function* filter(iterable, fn) { for (const v of iterable) if (fn(v)) yield v; }
const evenSquares = take(filter(map(naturals(), n => n * n), n => n % 2 === 0), 4);
console.log([...evenSquares]);

It prints:

[ 4, 16, 36, 64 ]

No intermediate arrays exist. The chain is a set of paused functions handing values along.

Work happens on demand

You can measure the laziness by counting how often the generator body runs.

// work is done only on demand
let calls = 0;
function* counted() { let n = 1; while (true) { calls++; yield n++; } }
const firstThree = [...take(counted(), 3)];
console.log(firstThree, '| body ran', calls, 'times');

It prints:

[ 1, 2, 3 ] | body ran 4 times

Three values were used and the body ran four times. The extra run is the one that produced the value take saw before it stopped.

Values can go back in

yield is an expression. Whatever you pass to next() becomes its value inside the generator.

// two-way - send values back in
function* adder() {
  let total = 0;
  while (true) {
    const next = yield total;
    if (next === undefined) return total;
    total += next;
  }
}
const sum = adder();
sum.next();                 // run to the first yield
console.log(sum.next(5).value);
console.log(sum.next(10).value);
console.log(sum.next().value);

It prints:

5
15
15

The first next() has nothing to send, since the generator has not reached a yield yet. That is why it is called on its own before the real values start.

What to remember

  • Calling a generator function runs no code. next() does.
  • Local state survives between calls, so a generator is a small state machine.
  • Infinite loops are safe when the consumer controls how many values it takes.
  • next(value) sends data in, which makes the channel two way.

Generators are also how async/await is usually explained. A paused function waiting to be resumed with a value is the same shape, with the resuming done by the promise machinery instead of your code.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Leave a Reply