ponyfoo.com

Mixing Generators Into Iterators

Fix

As it turns out, iterators can be written using generators. This can lead to some interesting use cases. Read on to understand the synergistic properties between these two JavaScript iteration concepts.

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?

Further Reading

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments (6)

Yimi wrote

I don’t know what to do with that knowledge but I love it!

John Doe wrote

The syntax of this code is really hard to understand.

const fibonacci = {
  [Symbol.iterator]() {
    let previous = 0

Care to elaborate on that?

Nicolás Bevacqua wrote

That’s a const binding named fibonacci, with a Symbol property that’s assigned as a computed property name and that’s also using the method definition shorthand syntax, followed by the method body and a let binding named previous which is assigned an initial value of 0.

Most of these things are ES6 features. You can learn more about all these things in the linked articles:

Oh, I also have a book on the subject! It’s free to read online. Hope that helps. 😅

John Doe wrote

OK thanks a lot for the fast answer; this is more or less what I inferred should be the case, using lowest-denominator syntax:

const iterator_key = Symbol.iterator;
cont fibonacci = {};
fibonacci[ iterator_key ] = function () { 
  let previous = 0;
  ...
};

I’m not sure I’m overly happy with that “you can leave out the function keyword in this case”. Sure it makes writing things easier and does remove some clutter. But it also makes the syntax more complex without introducing a new feature. Besides, parens and braces are also used for other things, so there’s still no unique syntax. Well, I guess you get used to it.

Moritz Kröger wrote

A bit late to the party, but you can also keep the function keyword in the previous example and have it more expressive.

const fibonacci = {
  [Symbol.iterator]: function () {
    let previous = 0
    ...
  }
}
Andrzej Kopeć wrote

Hi!

I’ve been always thinking that generators are just a syntactic sugar for creating own iterators but your post suggests that generator object and real iterator differ (a lot?). So what are the differences between them?