Before you begin
Why this matters
Programs often take values one at a time: numbers from a counter, orders from a queue, lines from a file. Whether each value is computed in advance or on request decides when the code that makes it runs, whether a sequence without end is possible, and whether you can read the values a second time. A loop over a used-up sequence gives no error, only an empty or short result.
What this helps you reason about
- Predict the order of the output when a loop reads a sequence whose steps print.
- Put a limit on a sequence without end, and say how many values it computes.
- Explain why a second loop over the same generator prints nothing.
A pancake stand can cook a whole stack before the first customer arrives, or cook one pancake each time a customer asks. Programs choose in the same way. This lesson shows TypeScript arrays, which hold every value at once, and generators, which compute the next value only when a loop asks for it.
Already know this? The short version
- An array is eager: every value is computed when it is made. A generator is lazy: calling a function* runs nothing and returns an iterator, and each next() runs the body up to the next yield.
- A paused generator keeps its frame, its variables, and its place, so code before a yield runs on the request for that value, and code after it waits for the next request.
- A generator with while (true) is fine when the loop breaks; a loop that tests each value asks for one value more than it keeps, and break finishes the generator.
- A generator object is used up after one loop: a second for...of over it runs zero times. Call the generator function again, or keep the values in an array.
Words in this lesson (5)
- eager collection
- A collection, such as an array or a list, whose values are all computed and stored when it is made.
- lazy sequence
- A sequence whose next value is computed only when something asks for it, one value at a time.
- iterator
- An object with a next method. Each call to next gives an object { value, done }: done is false while values remain, and true when there are no more.
- generator
- A function written function* that returns an iterator. Each call to next runs the function until its next yield, gives that value, and pauses there.
- side effect
- A change that code makes besides giving its value, such as changing a variable or printing a line.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / All at once
When is each value computed?
A pancake stand can cook in two ways. It can cook a stack of pancakes before it opens, or cook one pancake each time a customer asks for one. An eager collection works the first way: an array has every value computed and stored when it is made. A lazy sequence works the second way: its next value is computed only when something asks for it.
A generator is a lazy sequence written as a function. function* marks it. Calling pancakes() runs none of its body: it returns an iterator, the object with a next method that for...of uses, as in For loops and iteration protocols. Each request runs the body until it reaches yield n. yield gives n to the loop and pauses the function there. So the loop below prints cook 1, serve 1, cook 2, serve 2: each pancake is cooked when the loop asks for it.
function* pancakes() {
for (let n = 1; n <= 3; n++) {
console.log("cook " + n);
yield n;
}
}
for (const p of pancakes()) {
console.log("serve " + p); // after each cook
} An array computes all its values when it is made; a generator computes each value when the loop asks for it.
Coming from Python: function* and yield work like a Python generator function. The star is the marker, and next() returns { value, done } instead of raising StopIteration.
Precisely: asking for every value
[...pancakes()] builds an array from a generator. The three dots, a spread, ask the generator for every value, so all three pancakes are cooked before the array exists. Array.from(pancakes()) does the same. Use either one only with a generator that ends.
Experiment
Watch a generator cook one pancake per request
Calling pancakes() runs none of its body. Each time for...of asks for a value, the body runs from where it stopped up to the next yield, and yield hands n to the loop and pauses. [...pancakes()] asks for every value first, to build an array, so all the cooking happens before the loop starts.
Why it matters A generator computes each value when it is asked for. Anything that asks for every value at once, such as [...g], turns it back into an eager collection.
Predict the order of the six lines. Then change of pancakes() to of [...pancakes()], and predict again.
function* pancakes() {
for (let n = 1; n <= 3; n++) {
console.log("cook " + n);
yield n;
}
}
for (const p of pancakes()) {
console.log("serve " + p);
}
What to look for
This debugger often stops on the same line twice; that is normal. The pancakes frame appears above <top level> only when the loop asks for a value. At the yield line, the output shows cook 1. Then the pancakes frame is gone, and <top level> shows p = 1 at the serve line. When the loop asks again, pancakes comes back at its for line with n still 1, and the update makes n 2: the frame kept its variables while it was paused. The run prints cook 1, cook 2, cook 3, and only then serve 1, serve 2, serve 3. The pancakes frame runs from cook 1 to cook 3 without leaving, and serve starts after it ends.
Expected output
cook 1
serve 1
cook 2
serve 2
cook 3
serve 302 / State
What does an iterator remember?
A generator remembers where it is. While it is paused, its frame keeps its local variables and the line where it stopped. The next request continues from the line after yield, with those variables as they were. That position and those variables are the state of the iterator.
You can also keep the state yourself. counter below returns an iterator object. Its next method reads and changes count, a variable that it keeps from the code where it was written: a closure, as in Closures and captured variables. A generator does this work for you.
function counter(last: number) {
let count = 0;
return {
next() {
count++;
if (count > last) {
return { value: undefined, done: true };
}
return { value: count, done: false };
},
};
}
const it = counter(2);
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false } An iterator keeps its state between requests: a paused generator keeps its frame, and a hand-written iterator keeps variables that its next method can reach.
03 / Side effects
When does the code inside a step run?
A step of a lazy sequence is the code that runs to produce one value. A side effect in that code, such as a print or a change to a variable, happens when the value is asked for, not when the sequence is made:
- Making the generator, const stack = pancakes(), runs nothing. Nothing is cooked before the first next().
- Code before a yield runs on the request for that value. Code after a yield runs on the next request, so it is one value late.
- If nothing asks, nothing runs.
let cooked = 0;
function* pancakes() {
for (let n = 1; n <= 3; n++) {
cooked++; // a side effect in the step
yield n;
}
}
const stack = pancakes();
console.log(cooked); // 0: nothing has run yet
stack.next();
console.log(cooked); // 1 Code inside a generator runs when a value is asked for: nothing runs when the generator is made, and code after a yield waits for the next request.
04 / Without end
How does a sequence without end stop?
A generator does not have to end. while (true) with a yield inside describes ticket numbers without end. That is safe, because the generator pauses at each yield and computes a number only when asked. The loop that reads it must stop by itself, with break, or it never ends.
The loop below breaks when ticket is 4. To see 4, it must ask for 4, so the generator computes one number more than the loop prints. When a for...of loop ends with break, it also tells the generator to finish.
function* ticketNumbers() {
let next = 1;
while (true) {
yield next;
next++;
}
}
for (const ticket of ticketNumbers()) {
if (ticket > 3) {
break; // 4 was made, but not printed
}
console.log("ticket " + ticket);
} A generator without end is safe when the loop that reads it stops; a loop that tests each value asks for one value more than it keeps.
Precisely: iterator helpers
Recent versions of JavaScript add methods to iterators, such as take(n), which stops after n values without asking for another. This lesson uses break, which every version has.
Experiment
Stop an endless generator with break
while (true) never ends by itself, but the generator pauses at each yield, so it computes a number only when the loop asks. The loop must ask for 4 to see that 4 is too large. break ends the loop and finishes the generator.
Why it matters An endless generator is safe when the code that reads it stops. A loop that tests each value asks for one value more than it keeps.
Predict the lines printed, and the largest number the generator makes. Then delete the three lines of the if, and predict again.
function* ticketNumbers() {
let next = 1;
while (true) {
yield next;
next++;
}
}
for (const ticket of ticketNumbers()) {
if (ticket > 3) {
break;
}
console.log("ticket " + ticket);
}
console.log("stall closed");
What to look for
At the yield line, ticketNumbers shows next = 1. When the loop asks again, the frame comes back at next++, then yields 2. The generator makes 4 too: next shows 4 at the yield line, and the loop’s test sees ticket = 4 and runs break, so ticket 4 is never printed. The run prints ticket 1, 2, 3, then stall closed. After the edit, the loop never stops. The output fills with ticket lines, stall closed is never printed, and the stepper ends with Stopped after 240 recorded stops.
Expected output
ticket 1
ticket 2
ticket 3
stall closed05 / Reading twice
What happens when you read an iterator twice?
A generator object is used up after one full pass. Once it has given its last value, every later next() gives done: true. A second for...of loop over the same object gets done at once, and its body runs zero times, with no error. A loop that ends with break finishes the generator too.
The generator function is not used up. Calling orders() again makes a new generator object that starts at the beginning. When you need the values twice, call the function twice, or put them in an array once with [...orders()] and read the array as often as you like.
const queue = orders();
for (const order of queue) {
console.log(order); // plain, lemon, sugar
}
for (const order of queue) {
console.log(order); // nothing: queue is done
}
for (const order of orders()) {
console.log(order); // a new generator: all three
} A generator object can be read once; call the generator function again, or keep the values in an array, to read them twice.
Coming from Java: A generator object is like a Java Iterator, not like a List: it cannot start again, so ask the generator function for a new one.
Experiment
Read one generator twice
queue is one generator object. The first loop asks it for values until it is done. A done generator stays done, so the second loop’s first request already says done: true, and its body runs zero times. orders() makes a new generator object that starts at the beginning.
Why it matters A generator object can be read once. Call the generator function again for a new one, or keep the values in an array.
Predict the lines printed. Then change the second for (const order of queue) to for (const order of orders()), and predict again.
function* orders() {
yield "plain";
yield "lemon";
yield "sugar";
}
const queue = orders();
for (const order of queue) {
console.log("grill: " + order);
}
for (const order of queue) {
console.log("till: " + order);
}
console.log("closed");
What to look for
The panel shows queue as {}, so the output is the evidence. In the first loop, the orders frame appears at each yield line in turn. In the second loop, the stepper stops twice on its for line and goes on to closed: the orders frame never appears, and order is never set. The run prints grill: plain, grill: lemon, grill: sugar, then closed. After the edit, the second loop gets a new generator, the orders frame appears again from its first yield, and three till lines follow the grill lines.
Expected output
grill: plain
grill: lemon
grill: sugar
closed06 / Try it
Can you explain a fresh example?
Work out when each fill line runs before you look at the loops. The next lesson, Stack and heap, shows where values like these live while a program runs.
function* tastings() {
for (let n = 1; ; n++) {
console.log("fill cup " + n);
yield n;
}
}
const cups = tastings();
console.log("tray ready");
for (const cup of cups) {
if (cup === 2) {
break;
}
console.log("hand out cup " + cup);
}
for (const cup of cups) {
console.log("late cup " + cup);
}
What is printed, in order?
Reveal the reasoning
tray ready, fill cup 1, hand out cup 1, fill cup 2. Making the generator on the line const cups = tastings() runs nothing, so tray ready comes first. The first loop asks for 1, which is filled and handed out, then asks for 2, which is filled before the test sees it and breaks. break finishes the generator, so the second loop gets done at once and prints no late cup.
Practice tickets
- ITER-101 · Every jar of jam gets the same batch number → Free
- ITER-102 · The pre-order screen shows fewer trays than were ordered → Sign in · Pro
- ITER-103 · A potato goes missing every time a sack is closed → Sign in · Pro