An iterator hands out values one at a time: each call to its next() method returns an object like { value: 1, done: false }, until one says done: true. for...of, spread, and destructuring all read values by calling next(). Often the code reading stops before the end, such as a loop that breaks once it finds what it wanted. Closing the iterator means telling it that no more values will be asked for, so it can clean up: stop a timer, close a connection, release a lock. The iteration protocol, the shape every iterator follows, has an optional method for that: return().
It's like telling a waiter you've had enough before the last course: the kitchen stops cooking for you and clears up, instead of keeping your table laid all night.
function* lines() {
console.log("open");
try {
yield "first";
yield "second";
yield "third";
} finally {
console.log("close"); // runs however the generator ends
}
}
for (const line of lines()) {
console.log(line);
if (line === "second") break;
}
// logs open, first, second, close: break called return(), which ran the finally block
const [first] = lines();
// logs open, close: destructuring read one value, then closed the generator
const manual = lines();
manual.next(); // logs open; { value: "first", done: false }
// nothing closes manual for you: it stays paused at its first yield
manual.return("stopped"); // logs close; { value: "stopped", done: true }
manual.next(); // { value: undefined, done: true }
| When the reading stops | Is return() called? |
|---|---|
for...of ends early: break, return, or an exception thrown in the loop body | Yes |
for await...of ends early in the same ways | Yes, on the async iterator, and the loop waits for the promise it returns |
Destructuring has what it needs before the iterator says done, as const [a, b] = iterator does after two values | Yes |
The iterator says done: true, as at the end of every [...iterator] | No: it has already finished |
The iterator's own next() throws | No: the iterator failed, so it's treated as finished |
Your code calls next() by hand and stops calling it | No, unless you call return() yourself |
On a generator, return() resumes it at the yield where it's paused, as if that yield were a return statement. The generator ends there, but its finally blocks run on the way out, so cleanup written in a finally happens however the generator stops. A generator that hasn't started, because next() was never called, finishes at once: its body never runs, so it has nothing to clean up. Calling return() on a generator that has already finished changes nothing.
return() is optional. Array, string, Map, and Set iterators don't have one, since they hold nothing that needs releasing: [1, 2][Symbol.iterator]().return is undefined. So code that closes an iterator calls return() only when it exists. Generators, sync and async, always have one, and an iterator you write by hand can have one too.
You rely on closing whenever a generator holds something open: a loop over it can break early, and the generator still tidies up. Code that reads from an iterator and stops before the end is expected to do what for...of does, and close it.
Write lazy(iterable). It returns a sequence over any iterable, endless ones included: an object with the methods below.
map(fn) returns a new sequence of fn(value, index) for each value. index counts the values of the sequence map was called on: 0, 1, 2, and so on.
filter(fn) returns a new sequence of the values for which fn(value, index) is truthy, with index counted the same way.
take(n) returns a new sequence of the first n values. It never pulls more than n values, and when it stops early it closes the source's iterator (calls its return()), so a generator's finally blocks run.
toArray() runs the chain and returns an array of its values. A sequence is iterable too, so for...of and spread work on it.
Nothing runs until toArray() or a loop asks for values. Then values go through the chain one at a time: the first value goes through every step before the second is read.
Each method leaves the sequence it's called on unchanged. Running a sequence reads its source again, so a sequence over an array can be run any number of times.
Build the helpers yourself: don't use the built-in iterator helpers (Iterator.prototype.map and the rest), which do the same job.
function* naturals() {
for (let n = 1; ; n++) yield n;
}
lazy(naturals())
.map((n) => n * n)
.filter((n) => n % 2 === 1)
.take(3)
.toArray(); // [1, 9, 25]
const labels = lazy(["a", "b", "c"]).map((letter, i) => letter + i);
labels.toArray(); // ["a0", "b1", "c2"]
[...labels]; // ["a0", "b1", "c2"]
// Endless for lazy code; eager code gets an error instead of a frozen page.
function* naturals() {
for (let n = 1; n < 1000; n++) yield n;
throw new Error("read 1000 values from an endless generator: something isn't lazy");
}
const odd = lazy(naturals())
.map((n) => n * n)
.filter((n) => n % 2 === 1)
.take(3);
expect(odd.toArray()).toEqual([1, 9, 25]);
expect(lazy([1, 2, 3]).toArray()).toEqual([1, 2, 3]);
const double = spy((x) => x * 2);
const doubled = lazy([1, 2, 3]).map(double).filter(() => true);
expect(double).not.toHaveBeenCalled();
expect(doubled.toArray()).toEqual([2, 4, 6]);
expect(double).toHaveBeenCalledTimes(3);
const log = [];
lazy([1, 2])
.map((x) => {
log.push("map " + x);
return x * 10;
})
.filter((x) => {
log.push("filter " + x);
return true;
})
.toArray();
expect(log).toEqual(["map 1", "filter 10", "map 2", "filter 20"]);
// Endless for lazy code; eager code gets an error instead of a frozen page.
function* naturals() {
for (let n = 1; n < 1000; n++) yield n;
throw new Error("read 1000 values from an endless generator: something isn't lazy");
}
const seen = spy((x) => x);
expect(lazy(naturals()).map(seen).take(3).toArray()).toEqual([1, 2, 3]);
expect(seen).toHaveBeenCalledTimes(3);
expect(lazy([1, 2]).take(0).toArray()).toEqual([]);
expect(lazy([1, 2]).take(5).toArray()).toEqual([1, 2]);
expect(lazy(["a", "b", "c"]).map((x, i) => x + i).toArray()).toEqual(["a0", "b1", "c2"]);
expect(lazy([5, 6, 7, 8]).filter((x, i) => i !== 1).toArray()).toEqual([5, 7, 8]);
const indexes = lazy([10, 11, 12, 13])
.filter((x) => x % 2 === 0)
.map((x, i) => i);
expect(indexes.toArray()).toEqual([0, 1]);
const plusOne = lazy(new Set([1, 2, 3])).map((x) => x + 1);
expect([...plusOne]).toEqual([2, 3, 4]);
const seen = [];
for (const value of plusOne) seen.push(value);
expect(seen).toEqual([2, 3, 4]);
const base = lazy([1, 2, 3]);
const doubled = base.map((x) => x * 2);
const big = doubled.filter((x) => x > 2);
expect(base.toArray()).toEqual([1, 2, 3]);
expect(doubled.toArray()).toEqual([2, 4, 6]);
expect(big.toArray()).toEqual([4, 6]);
expect(doubled.toArray()).toEqual([2, 4, 6]);
let closed = false;
function* letters() {
try {
yield "a";
yield "b";
yield "c";
} finally {
closed = true;
}
}
const upper = lazy(letters())
.map((x) => x.toUpperCase())
.take(2);
expect(upper.toArray()).toEqual(["A", "B"]);
expect(closed).toBe(true);
const names = ["map", "filter", "take", "drop", "flatMap", "toArray", "reduce", "forEach", "some", "every", "find"];
const proto = typeof Iterator === "function" ? Iterator.prototype : null;
const saved = new Map();
let result;
let error;
try {
if (proto) {
for (const name of names) {
saved.set(name, Object.getOwnPropertyDescriptor(proto, name));
proto[name] = () => {
throw new Error("used the built-in Iterator.prototype." + name);
};
}
}
result = lazy([1, 2, 3, 4])
.map((x) => x * 10)
.filter((x) => x > 10)
.take(2)
.toArray();
} catch (caught) {
error = caught;
} finally {
for (const [name, descriptor] of saved) {
if (descriptor) Object.defineProperty(proto, name, descriptor);
else delete proto[name];
}
}
expect(error).toBeUndefined();
expect(result).toEqual([20, 30]);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Lazy iterator helpers. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.