Building Iterators
Dina
Posted on April 4, 2022
As programmers, one of the very first thing we learn is "the loop". There is always an array or list or collection which needs to be looped through, an object or map or dictionary whose keys and values require traversing. Iterations are key programming concept.
Arrays and Maps are collections of things and it should to be defined explicitly prior to iterating. You can start out with an empty array or a declaration and you can push items to it. Example:
const things = ['headphone', 'keyboard', 'mouse']
const friends = {
tomo: 'nuxt expert',
jt: 'writes nasty sqls',
deco: 'the leader',
van: 'php guru'
}
things.push('cables')
// one of the ways of iterating arrays
for (const thing of things) {
console.log(thing)
}
// iterate the key of objects
for (const name in friends) {
console.log(`${name} - ${friends[name]}`)
}
There are many ways of iterating over an array or an object. To name a few:
-
for(const i = 0; i < arr.length; i+=1)
-
for...of
MDN -
for...in
MDN -
while()
MDN -
Array.forEach
MDN -
Array.map
MDN -
Array.filter
MDN -
Array.reduce
MDN
One thing about arrays or objects(Map, Set etc) is that you kind of know what you are getting. You could push things into an array but you know what you pushed. Its not dynamic. If something is in array, its there for good until you remove it. Also, its taking up that space in the memory.
Iterator protocol
What if you had a dynamic array that calculated what value
you get in the next
iteration? What if that worked based on a formula that you've built? You need to use iterator pattern. You'll notice that its very simple to implement. Its a protocol that is well known among JS programmers and also followed in other languages too. An iterator is an object that has next()
method. Calling next()
function on the object gives us the iterator result which is an object with two properties - done
which is a boolean to hold the status of the iterator and value
to hold whatever you want to return. Let's build a simple range iterator. This range iterator will allow us to create a range of numbers by providing a start, end and step.
// iterator protocol: an agreed interface
function numberRangeIterator(start, end, step) {
let index = start
return {
next() {
if (index > end) {
return { done: true, value: 'thanks for using me' } // value is optional here but you can use it to return meta info
}
const value = index
index += step
return { done: false, value }
}
}
}
const iterator = numberRangeIterator(3, 30, 3)
let iteratorResult = iterator.next()
while (!iteratorResult.done) {
console.log(iteratorResult.value)
iteratorResult = iterator.next()
}
You see? Its very simple and yet powerful. Two things to note:
- the
next
function should return and object withdone: true
to indicate that there are no more elements. But it's not mandatory, you can have an iterator that runs forever! - you can have
done: false
or return just{value}
and above code will just work fine.
function randomNumberIterator() {
return {
next() {
return { done: false, value: Math.random() }
}
}
}
const rIterator = randomNumberIterator()
let rIteratorResult = rIterator.next()
while (!rIteratorResult.done) {
console.log(rIteratorResult.value)
rIteratorResult = rIterator.next()
}
While I cannot think of when you'd use the iterator above, I just wanted to demonstrate an iterator that can generate random numbers infinitely.
Iterable protocol
Iterable protocol goes one step further by defining a standard within JS language for any object to return an iterator. An iterable
is an object that implements an iterator method called [Symbol.iterator]
. The best thing about using iterables over the iterator which we talked about above is that it allows us to use JS native apis for looping over the array such as for...of
. Let's build our numberRangeIterator
as an iterable.
class NumberRange {
constructor(start, end, step) {
this.start = start
this.end = end
this.step = step
}
// for an object/class to classify as iterable
// it has to implement [Symbol.iterator]
[Symbol.iterator]() {
let index = this.start
return {
next: () => {
if (index > this.end) {
return { done: true }
}
const value = index
index += this.step
return { value }
}
}
}
}
const myRange = new NumberRange(3, 30, 3)
for (const num of myRange) {
console.log(num)
}
It was almost the same amount of code to define the iterable class and we reused most of our code. However, the beauty is in the way we consume the iterator. Using for...of
makes it look so clean and concise. I prefer this over the while
loop above. But it doesn't stop here. There are other ways you can consume this iterable. You can use it with spread operator.
const myRange2 = new NumberRange(5, 20, 4)
console.log(...myRange2) // prints 5 9 13 17
Or, destructure and assign it
const myRange2 = new NumberRange(5, 20, 4)
const [first, second, third] = myRange2
console.log(first, second, third) // prints 5 9 13
There are other JS built-in APIs that accept iterables where you can pass your iterables such as Array.from(iterable)
, Set([iterable])
, Promise.all(iterable)
and even stream.Readable.from(iterable)
.
Read more about iterators here. You can pretty much treat it like your regular array but dynamic in nature and it will calculate your values only when it needs to. Things get a bit hairy though, when you start getting into the territory of async iterators but that is for another day.
Posted on April 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.