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)
}
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)
}
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"
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]
}
}
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}`)
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)
}
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)
}
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)