A Look at ES6 Generators

By Amitai B.

Feb 4, 2017

One of the more complicated notions presented in ES6 is generators. This is a feature with a lot of power that enhances the JavaScript language, especially in areas such as asynchronous programming.

In this blog post, I will describe these generators and how to use them in the real world.

Iterator

In order to understand generators, we need first to learn what is an iterator.

An iterator is a design pattern to go over elements in a collection without it knowing anything about the collection structure and the elements themselves.

An iterator has two methods:

  • next() – which returns the next item of the collection
  • hasNext() – returns true or false whether the collection has the next item.

A simple implementation of an iterator can be as follows:

function Iterator(collection) {
    let index = 0;
    return {
        next: () => {
            return collection[index++];
        },
        hasNext: () => {
            return index < collection.length;
        }
    }
}

const iter = Iterator([1,2,3]);
while (iter.hasNext()) {
    console.log(iter.next()); // 1 2 3
}

For..of

Another cool feature of ES6 is the “for..of” loop. This operator creates a loop with iterator objects. This is a JS object that implements the iterator JavaScript protocol. The protocol defines how to implement the iterator design pattern. The object has a next method that returns an object with two keys:

  • Value
  • Done (boolean)

JavaScript objects such as Array, Map and Set implement this protocol. If we want to do a simple implementation of our iterator object, it can be done as follows:

function Iterator(collection) {
    let index = 0;
    const iter = {};
    iter[Symbol.iterator] = () => {
        return {
            next: () => {
                if (index < collection.length) {
                    return {
                        value: collection[index++],
                        done: false
                    };
                } else {
                    return {
                        value: null,
                        done: true
                    };
                }
            }
        }
    }

    return iter;
}

const iter = Iterator([2,21,33]);
for (let x of iter) {
    console.log(x); // 2 21 33
}

The for..of operator looks for the Symbol.iterator of the object and uses it to iterate.

Generators

Generators help make the creation of generators easier. Instead of all the boilerplate code above, we can use them in this way:

function* Iterator(collection) {
    let index = 0;
    while (index < collection.length) {
        yield collection[index++];
    }
}

const iter = Iterator([22,29,35]);

for (let x of iter) {
    console.log(x); // 22 29 35
}

As we can see, the implementation is much cleaner with much less code. But, what’s going on here? Let’s start by defining generators:

A generator is a special type of function that works as a factory for iterators. A function becomes a generator if it contains one or more yield expressions and if it uses the function* syntax.

This is a very simple generator:

function* simpleGenerator(){
    yield 'a';
    yield 'b';
    yield 'c';
}

const iter = simpleGenerator();

console.log(iter.next()); // { value: 'a', done: false }
console.log(iter.next()); // { value: 'b', done: false }
console.log(iter.next()); // { value: 'c', done: false }
console.log(iter.next()); // { value: undefined, done: true }

Its definition contains “function*” and inside it has the yield keyword. The generator manages its state and returns upon any call the next value. Each returned value has two elements: *value* and done. As we can see, as long as it has next value it returns it until it doesn’t and then it returns done: true.

Bidirectional communication

Generators can receive parameters and use them:

function* foo(x) {
   const y = yield x;
   const z = yield y;
   return z;
}

const gen = foo(1);

console.log(gen.next());    // { value: 1, done: false }
console.log(gen.next(2));   // { value: 2, done: false }
console.log(gen.next(3));   // { value: 3, done: true }

The first yield returns the first parameter that the generator receives in its creation (x), then y gets the value from the next() methods, and so does z. The yield plays two roles, return the next element to the caller, and receive parameter to the generator. As you can see, I returned z, instead of “yielding.” The result was that the done is true and the generator finished it elements.

Generators and Asynchronous

One of the great feature of generators is that they are asynchronous. When we use the yield keyword, it waits until it receives a value and then continues. It allows us to do async tasks (like fetching data from storage, ajax calls).

For example:

function asyncCall(x) {
    setTimeout(() => {
        gen.next(x + 5)
    }, 1000);
}

function* asyncGenerator() {
    let val = yield asyncCall(5)
    console.log('Value =', val);    // Value = 10
    val = yield asyncCall(10)
    console.log('Value =', val);    // Value = 15
}

const gen = asyncGenerator();
gen.next();

I use the generator object (gen) to return the value inside the generator. Imagine that instead of using setTimeout, it was fetch request.
But, there is something awkward here: We use the generator inside the asyncCall, and it is very inconvenient and weird. We will see how we can improve it with the use of promises.

Generators with promises

When we think about ES6 and async programming, we think about promises. The combination between them is very powerful and allows us to avoid the callback hell. You can refresh your memory about promises in my previous blog post.

It will look like this (inspired by Kyle Simpson’s great article):

function asyncCall(x) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(x + 5)
        }, 1000);
    });
}

function runGenerator(generator) {
    const gen = generator();
    let ret;
    (function iterate(val){
        ret = gen.next(val);
        if (!ret.done) {
            ret.value.then(iterate);
        }
    })();
}

runGenerator(function* asyncGenerator() {
    let val = yield asyncCall(5)
    console.log('Value =', val);    // Value = 10
    val = yield asyncCall(10)
    console.log('Value =', val);    // Value = 15
});

The first thing to notice is that we took the generator out of the asyncCall and changed things to return a promise. The next thing is the use of the helper method that deals with the promise (runGenerator). This method gets a generator as a parameter, activates it and handles its returned values. Take a few minutes to understand this implementation because it is not that obvious. Of course, this is a very naive implementation that does not deal with errors, and other issues. Thankfully, there are bunch of open source libraries (co, Q, bluebird) that implement it and we can use them instead of using this operation.

Introducing Co

Co is a generator based control flow goodness for nodejs and the browser, using promises while letting you write non-blocking code in a nice-ish way. The best way to demonstrate it is:

var co = require('co');

co(function *(){
  // yield any promise
  var result = yield Promise.resolve('co');
  console.log('hello', result); //hello co
});

Co gets generator (as our runGenerator) and executes it in order to use it in our previous example:

var co = require('co');

function asyncCall(x) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(x + 5)
        }, 1000);
    });
}

co(function* asyncGenerator() {
    let val = yield asyncCall(5)
    console.log('Value =', val);    // Value = 10
    val = yield asyncCall(10)
    console.log('Value =', val);    // Value = 15
});

Looks much cleaner. We can use this method to do async programming calls as if they were synchronous.

Async/Await

ES7 Async/Await take this methodology to the next level and even conceal the use of the generator based control flow. The code simply looks like this:

function asyncCall(x) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(x + 5)
        }, 1000);
    });
}

(async function asyncGenerator() {
    let val = await asyncCall(5)
    console.log('Value =', val);    // Value = 10
    val = await asyncCall(10)
    console.log('Value =', val);    // Value = 15
})();

We simply replaced the function* with async function and yield with await.

There is a lot of resemble async/await to generators, and it is no wonder that babel transpiler uses generators in order to implement async/await.

References

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators

ES2015 Iterators and Generators – Dan Shappir

https://github.com/tj/co

Leave a Reply

Your email address will not be published.