Let’s do a quick recap of generators (read our primer on generators here). Generator functions return generator objects when invoked. A generator object has a next
method, which returns the next element in the sequence. The next
method returns objects with a { value, done }
shape.
The following example shows an infinite fibonacci number generator. We then instantiate a generator object and read the first eight values in the sequence.
function* fibonacci() {
let previous = 0
let current = 1
while (true) {
yield current
const next = current + previous
previous = current
current = next
}
}
const g = fibonacci()
console.log(g.next()) // <- { value: 1, done: false }
console.log(g.next()) // <- { value: 1, done: false }
console.log(g.next()) // <- { value: 2, done: false }
console.log(g.next()) // <- { value: 3, done: false }
console.log(g.next()) // <- { value: 5, done: false }
console.log(g.next()) // <- { value: 8, done: false }
console.log(g.next()) // <- { value: 13, done: false }
console.log(g.next()) // <- { value: 21, done: false }
Iterators follow a similar pattern (you may read our primer on iterators here). They enforce a contract that dictates we should return an object with a next
method. That method should return sequence elements following a { value, done }
shape. The following example shows a fibonacci
iterable that’s a rough equivalent of the generator we were just looking at.
const fibonacci = {
[Symbol.iterator]() {
let previous = 0
let current = 1
return {
next() {
const value = current
const next = current + previous
previous = current
current = next
return { value, done: false }
}
}
}
}
const sequence = fibonacci[Symbol.iterator]()
console.log(sequence.next()) // <- { value: 1, done: false }
console.log(sequence.next()) // <- { value: 1, done: false }
console.log(sequence.next()) // <- { value: 2, done: false }
console.log(sequence.next()) // <- { value: 3, done: false }
console.log(sequence.next()) // <- { value: 5, done: false }
console.log(sequence.next()) // <- { value: 8, done: false }
console.log(sequence.next()) // <- { value: 13, done: false }
console.log(sequence.next()) // <- { value: 21, done: false }
Let’s reiterate. An iterable should return an object with a next
method: generator functions do just that. The next
method should return objects with a { value, done }
shape: generator functions do that too. What happens if we change the fibonacci
iterable to use a generator function for its Symbol.iterator
property? As it turns out, it just works.
The following example shows the iterable fibonacci
object using a generator function for its iterator. Note how that iterator has the exact same contents as the fibonacci
generator function we saw earlier. We can use yield
, yield*
, and all of the semantics found in generator functions hold.
const fibonacci = {
* [Symbol.iterator]() {
let previous = 0
let current = 1
while (true) {
yield current
const next = current + previous
previous = current
current = next
}
}
}
const g = fibonacci[Symbol.iterator]()
console.log(g.next()) // <- { value: 1, done: false }
console.log(g.next()) // <- { value: 1, done: false }
console.log(g.next()) // <- { value: 2, done: false }
console.log(g.next()) // <- { value: 3, done: false }
console.log(g.next()) // <- { value: 5, done: false }
console.log(g.next()) // <- { value: 8, done: false }
console.log(g.next()) // <- { value: 13, done: false }
console.log(g.next()) // <- { value: 21, done: false }
Meanwhile, the iterable protocol also holds up. To verify that you might use a construct like for..of
, instead of manually creating the generator object. The following example uses for..of
and introduces a circuit breaker to prevent an infinite loop from crashing the program.
for (const value of fibonacci) {
console.log(value)
if (value > 20) {
break
}
}
// <- 1
// <- 1
// <- 2
// <- 3
// <- 5
// <- 8
// <- 13
// <- 21
This was a fun trick. What would you use it for in a real-world program?
Comments