DEV Community

Cover image for Using Iterators and Generators in JavaScript
nuel ikwuoma
nuel ikwuoma

Posted on

Using Iterators and Generators in JavaScript

The word 'iterable' appears in many programming paradigm, it simply can be assumed as any data-structure that can be passed to a loop, to extract its contents. Many types in javascript are iterable, the likes of which includes string, array, set e.t.c
A quick example would be iterating over the Array type, so we can safely call the Array type an iterable

let weekends = ["fri", "sat", "sun"];   // Array type
for(let day of weekends) {
    console.log("its " + day)
}
Enter fullscreen mode Exit fullscreen mode

Before we begin to implement our own custom iterable, lets quickly take a look at generators. A generator in javascript is an function expression with a yield statement, its quite different from a normal function and the yield statement should be inside the function block that defines the generator and not in an enclosing function. A quick demo of generator for yielding fibonacci sequence looks thus:

 function * fibonacci (rng) {
     let a = 0, b = 1, nxt;
     for(let i=2; i < rng; i++) {
        nxt = a + b;
        a = b;
        b = nxt
        yield nxt;    // 'yield' the next number in the fibonacci sequence
     }
 }

// using the fibinacci generator above to yield first 10 sequence
for(let val of fibonacci(10)) {
    if(val > 100) break;    // Note 'break' to prevent an infinite loop
    console.log(val)
}
Enter fullscreen mode Exit fullscreen mode

I apologize if the generator expression above is a little complicated, but the most important to note is how we define the expression with an asterik and how we output value using the yield statement.
One more thing to briefly introduce is the Symbol constructor, in javascript, Symbol defines a unique symbol(constant) and ensures it does not coerce with other Symbols of similar construct. For instance,

let bar = Symbol("bar")
let bar2 = Symbol("bar")
bar == bar2    // returns "false"
Enter fullscreen mode Exit fullscreen mode

Notice that the two Symbol definition above does not coerce.
Now, lets assume we creating a custom type we would call Matrix, to store a series of numbers, we would define a custom javascript class thus:

class Matrix {
    constructor(width, height, element = (x, y) => undefined) {
        this.width = width
        this.height = height
        this._content = []

        for(let y=0; y < height; y++) {
            for(let x=0; x < width; x++) {
                this._content[y*width + x] = element(x, y)
            }
        }
    }

    get(x, y) {
        return this._content[y*this.width + x]
    }
}
Enter fullscreen mode Exit fullscreen mode

We can instantiate an 3 by 3 matrix object and pass some arbitrary values thus:

let matrix = new Matrix(3, 3, (x, y) => `x: ${x}, y: ${y}`)
Enter fullscreen mode Exit fullscreen mode

To get through the values defined in the matrix type, a naive approach would look thus;

for(let val of matrix._content) {
    console.log(val)
}
Enter fullscreen mode Exit fullscreen mode

This seems to work, but the underscore which precedes the content instance property should remind us not to access that property directly from outside the class in which it is defined, so how do we make the Matrix type iterable, there are quite a few ways to implement this, but i claim that the generator approach is quite easy to implement and reason about, it goes this:

Matrix.prototype[Symbol.iterator] = function* () {
    for(let y=0; y< this.height; y++) {
        for(let x=0; x < this.width; x++) {
            yield {x, y, value: this._content[y * this.width + x]}
        }
    }
}

// now we can create the object and iterate directly without directly accessing its internals

let matrix2 = new Matrix(3, 3, (x, y) => `x: ${x}, y: ${y}`)
for(let {x, y, value} of matrix2) {
    console.log(x, y, value)
}
Enter fullscreen mode Exit fullscreen mode

What just happened?
First, we defined a property in Matrix prototype named Symbol.iterator, this is what is called when we try to get values from an iterable type inside a loop.

Second, since generator maintains its state everytime we yield from it, we use that to ensure we return appropriate values upon each iteration.

Now, it should be clear how we can use iterators and the less-favoured generator expression to write more robust custom types.

Thanks for reading, would appreciate your feedbacks

Top comments (0)