felfel.dev

Introduction to javascript iterables, iterators, and generators

August 9, 2016 • ☕️ 7 min read

Iterators and Iterables:

ES6 (aka ES2015) has introduced two new concepts in javascript: iterables, and iterators. This is a brief on what iterables, iterators, and generators are and how to make use of them.

What are iterables?

In general, it is a data structure that allows its data to be consumed. It does so by implementing a method whose key is Symbol.iterator which returns an iterator.

Why do we need iterables?

Pre-ES6 in Javascript if you have some data source, depending on how you structure your data, you need to consume it in a different (sometimes hacky!) way. 

Iterating and consuming the data of an Array is different from a key/value object and as ES6 is adding new data sources like Map and Set, so it doesn’t make sense and is not practical to implement different ways of consuming all those different data sources. So the language must have an interface that all the data sources can implement, so all the consumers have the same way to consume data.

Iterables image source here, not exactly but inspired from it

How to implement an iterable?

To call any object iterable, it needs to implement the iterable interface, which as per ecmascript specification: spec 1

So for an object to be an iterable, it needs a method that returns an iterator, and this methods property is @@iterator (Symbol.iterator).

What is an iterator?

Iterator is a way to pull data from a data structure on one-at-a-time fashion, it needs to implement the iterator interface. spec 2 source

Hint: the iterator interface has another method called return(), which is being called when the iteration reaches the last value, or stopped manually by calling it explicitly or for example break; a for loop.

And the iteratorResult that returns from the iterator next() method needs to follow this interface: spec 3 source

So each time we call next() on an iterator we should get {value: the current iteration value, done: false/true}

As we see in the above first image, one of the ways to consume iterables is for..of loop, so lets use it on one of the language built-in iterables, an array.

const arr = [1,4,2];
for(v of arr){
    console.log(arr); // 1 4 2
}

But can we call .next to pull values from an array:

const arr = [1,4,2];
arr.next() // Error

Why? because it is an iterable not an iterator, and to be able to use .next we need to call the iterator method this iterable’s Symbol.iterator returns:

const arr = [1,4,2];
const iter = arr[Symbol.iterator]();
iter.next(); // {value: 1, done: false}
iter.next(); // {value: 4, done: false}
iter.next(); // {value: 2, done: false}
iter.next(); // {value: undefined, done: true}

Symbol.iterator represents the property on any object which when we call, the language looks to find a method that will construct an iterator instance for consuming that object’s values. Many objects come with a default one defined.

Custom Iterators

In addition to the standard built-in iterators, you can make your own.  All it takes to make them work with ES6’s consumption facilities (e.g., the for..of loop and the  operator) is to adhere to the proper interface(s).

Let’s try constructing an iterator that produces the infinite series of numbers in the Fibonacci sequence:

var Fib = {
 [Symbol.iterator]() {
     var n1 = 1, n2 = 1;
     return {
         next() {
             var current = n2;
             n2 = n1;
             n1 = n1 + current;
             return { value: current, done: false };
         },
         return(v) {
             console.log('Done');
             return { value: v, done: true };
         }
     };
  }
};
for (var v of Fib) {
    console.log( v );
    if (v > 50) break;
}
// 1 1 2 3 5 8 13 21 34 55
// Done

Here we can see the benefit of the optional iterator’s return(..) method, it is actually so useful as it works as a built-in stopIteration method, it is defined as sending a signal to an iterator that the consuming code is complete and will not be pulling any more values from it, so it is being called anyway once we finished iterating, it is the one returning {done:true}, but we can call it manually as well.

Consuming Iterators:

1- for .. of loop:

We can use the new es6 for..of loop to consume iterators, but it must be an iterable as well, we easily can make any iterator an iterable by providing it with the Symbol.iterator property that returns the iterator itself:

var it = {
    // make the `it` iterator an iterable
    [Symbol.iterator]() { return this; },
    next() { .. },
    ...
};

2-  spread/rest operator:

We can use es6 spread operator to consume iterators. In the above Fibonacci snippet, we can do :

[…Fib] // [1, 1, 2, 3, 5, 8, 13, 21, 34]

We can use iterators with array destructuring as well:

[a, b] = […Fib] // a = 1, b = 1
[a,b, …c] = […Fib] // a=1, b=1, c = [3, 5, 8, 13, 21, 34]

Generators:

Pre ES6 there was no way in javascript to pause executing a function at some point, then come later to the same point and resume the function execution. But with generator functions we can do that, because generator functions when executed return an iterator, and instead of stopping at each value/element and return iteratorResult with each .next(), generator functions stop at each yield keyword and return the value next to it if any.

To define a generator function you need to add * between the keyword function and the function’s name.

function *foo() {
    yield 1
    yield 2
    yield 3
}
const iter = foo();
iter.next(); // {value: 1, done: false}
iter.next(); // {value: 2, done: false}
iter.next(); // {value: 3, done: false}

All iterators created by generators are also iterables, as generators assign the Symbol.iterator property by default, so we can use for..of with it:

function *foo() {
    yield 1
    yield 2
    yield 3
}
const iter = foo();
for(v of iter){
 console.log(v) // 1 2 3
}

You can write generator functions in different ways:

function declaration:

function *genFunc() { ··· }
const genObj = genFunc();

function expression:

const genFunc = function *() { ··· };
const genObj = genFunc();

ES6 method shorthand in object literals:

 const obj = {
     *generatorMethod() {···}
 };
 const genObj = obj.generatorMethod();

yield delegation (aka recursive yield):

If you check the first image, you will notice we added also yield* as one of the iterable interface consumers, this pattern is used when you want to pass a generator (or any iterable) to one of your generator’s yield keywords:

function *foo() {
    yield *[1,2,3];
}
const iter = foo();
iter.next(); // {value: 1, done: false}
iter.next(); // {value: 2, done: false}
iter.next(); // {value: 3, done: false}
iter.next(); // {value: undefined, done: true}

So as we see in this example, instead of returning the whole array, it is returning an array iterator which we can call next() to pull each value of it.

And we can pass another generator function as well:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
}
function *bar() {
    yield *foo(); 
}
const iter = bar();
iter.next(); // {value: 1, done: false}
iter.next(); // {value: 2, done: false}
iter.next(); // {value: 3, done: false}
iter.next(); // {value: undefined, done: true}

Now Let’s visit the Fibonacci example again but implement it with generators this time:

var FibGen = {
    [Symbol.iterator]() {
        var n1 = 1, n2 = 1;
        return function* fib () {
            var current = n2;
            n2 = n1;
            n1 = n1 + current;
            yield current;
            yield* fib();
         }();
     }
}
for (var v of FibGen) {
    console.log( v );
    if(v > 50) break;
}

That was a quick overview of iterables, iterators, and generators in javascript, if you want to read more, here are some resources:

Iterators and Generators - Understanding ES6
ES6-Organization - You Don’t Know JS - ES6 and Beyond
Iterables and Iterators - ExploringJS
Generators - ExploringJS
Iterators and Generators - MDN