Re-visit JavaScript Iterations (using Iterators and Generators)
Introduction
After coming to Uber, I am having a lot of fun with JavaScript lately. While I am still learning, I wanted to share with you some fun stuffs around loops and iterations.
Iterations are a fundamental part of any Algorithmic design. Now there are many ways to learn loops and conditionals (or such fundamentals as such), but here, I wanted to take a particular way of thinking about Iterations and generalizing the same idea over a wide variety of day to day use cases. So, without further due let’s begin.
If you simply console.log()
the Array.prototype
, expand on it and go little down, you will find a Symbol.iterator function. like this, where the Symbol is nothing but a key to get a reference to the function.
Now, with this property, we can get hold of an iterator which can loop through the array.
Note: The Iterator function when called should be bound to the Array in context, else it will fail to create the iterator. For example, in the above example instead of gettingfunItr
If we just get a reference to funActs[Symbol.iterator]
and call it later to get a reference. It won’t work as expected. In this case, we can do what we want by expressing the function to be something like this const createIterator = funActs[Symbol.iterator].bind(funActs)
Default Array Iterator
By definition, Iterators has a special .next()
function associated with them. Which when called returns a value from the collection (Array). Each time .next()
is called, it returns the consecutive items from the array one at a time, till the end of the array. Something like this,
Please note that return value of .next()
produces a result (Object) which has two keys to it. First, is the key called value
which can be present or absent depending on whether there are more items to be looped over in the collection. Second, it has a mandatory done
boolean variable which indicates whether there are more values to be found in the collection, or we are done iterating over the collection.
Iterable Interface
We can summarize what we learned so far with the following interface for an Iterable (Array is a Iterable element). Basically, it needs to have a Symbol.iterator function which returns an Iterator which basically is nothing but a Object
with a .next()
function attached to it. The next function when called returns the result of this format {value: any, done: boolean}
.
In JavaScript, we have a special iteration syntax called for..of
which abide by this iterable contract. This makes iterating over objects using a for..of
loop a breeze. Take a look,
Creating a Custom Array Iterator
We have gone through the hard work of learning all the details around iterables because now we can start to play with it, and modify it to create our own custom iterator.
So, let’s say someone asks you to iterate over the Array in a reverse fashion. Without arguing about an index based for loops which definitely can achieve this, let’s see how we can do the same by creating our own very first custom iterator. First, let’s just start with .next()
function, when we try to get the iterator,
For making it work with for..of
loop, we will just decorate this with a Symbol.iterator function so that it looks like an array an Iterable, then we can use for..of
loop as usual and it will print the values in reverse order.
Generators for Iterations
While custom iterators are a useful tool, their creation requires careful programming due to the need to explicitly maintain their internal state. Generator functions provide a powerful alternative: they allow you to define an iterative algorithm by writing a single function whose execution is not continuous. Generator functions are written using the function*
syntax (which allows us to use the magic yield keyword to pause execution)
Generators allow us to write iterators with a much more concise syntax and give us more flexibility with each iteration.
let’s refactor this using a generator. We’ll just go ahead and take all the pieces from above and use them inside of our reverseGen, which is the generator function this time,
Taking Control over Iterations
Please note that while discussing the Iterables specially around Generators, we only focused on having the .next()
function as it will be used most often. But there are other methods which allows you to control the behavior of the iterator. For example, if we use the .return()
method anytime in an iterator, the iterator will immediately complete and any further call to .next()
will be ignored. Take a look,
We can also customize the logic inside the generator however we want which can automatically affect how the iteration works out overall. For example, here say when the length of the array is left is one, we don’t want to loop anymore. So, in our example, we simply return from the generator when the length is one. This will not loop over the last item and skip it. Take a look,
Yielding over nested Iterables (yield*)
One tremendous benefit of function generators is the ability to iterate over other nested generators. If the value we are yielding itself is another generator then we can use the special syntax yield* which will ensure to loop over all the values for the nested generator inside the yield and then will move on to executing other yields. Let’s take a simple example, to clarify what I mean,
In this example, as you can see clearly, our someOtherGen() is an independent generator which generates “Hello” as the first value. Then as it needs to get all values from reverseGen() one at a time so we used the syntax yield* and lastly, after its completion the outer generator continues as usual and produces last value independent of reverseGen() as “World”. So, you can see the benefits of using Generators within generators for handling complex execution scenarios.
Generators for generating Data
We can cleverly leverage the way generators work, to gnerate custom data very easily. Let’s take an example of a range operator which takes on a format like range(start, end, inc)
and produces all values from start to finish with an increment (inc). It doesn’t exist in JS language as such, but let’s see how we can easily add a functionality like range() in JS with the help of generators.
This comes also very handy when you want to generate a Array prefilled with some data (based on custom logic), like this,
Generators for managing State
The last aspect we will explore about Generators in this post, is their ability hold and manage internal state for an application/variable. Because we can trap state inside of our generator functions and also a way to pause and execute when needed, we can do things like create state machines without any external library or code.
Here is a simple example of a Increment/Decrement counter based State Machine implemented using Generators,
Conclusion
Well if you are with me so far, we covered some good stuffs, we started off with a simple Iterable interface and the contract which holds true for many Iterable Data Structures. We then looked at Custom Iterators and how you can create any type of loops you wish. Then we looked at Generators giving you a natural iterable interface which yields values everything .next() is called on it. We re-implemented our custom Iterator using function generators. Lastly, we looked at the benefits of using generators, nested loops, data generation and state machine kind of applications which we can achieve very easily.
Bonus: I believe having this foundational knowledge is very useful for dealing with various kinds of task at hand. At times, you can get creative as develop some patterns which are unique and useful. So, to conclude, I leave you with one last example of creative use of generators for working with collections with methods like of .map(), .filter()