Using Custom Iterators and Generators

The built-in collections in JavaScript, like Array, Set, and Map, are all iterable. We can iterate over them, using the for loop, to process the elements. But what about user-defined classes? You may create, for example, a Car class and may want the user of your class to iterate over its wheels or doors. Thankfully, JavaScript makes it quite easy to define custom iterators for user-defined classes. Before we jump in to writing custom iterators, let’s examine iterators for built-in classes.

We can easily iterate over a collection of objects. For example, the following code iterates over an array of strings:

 const​ names = [​'Tom'​, ​'Jerry'​, ​'Tyke'​];
 
 for​(​const​ name ​of​ names) {
  console.log(name);
 }

We can readily use the forof syntax on an Array because it implements a function named [Symbol.iterate]. The previous code produces the expected output:

 Tom
 Jerry
 Tyke

We can’t, however, iterate on arbitrary instances. For example, let’s create a class named CardDeck and iterate over an instance of that class.

 class​ CardDeck {
 constructor​() {
 this​.suitShapes = [​'Clubs'​, ​'Diamonds'​, ​'Hearts'​, ​'Spaces'​];
  }
 }
 
 const​ deck = ​new​ CardDeck();
 
 for​(​const​ suit ​of​ deck) {
  console.log(suit);
 }

The class CardDeck has a field named suitShapes of type Array with four String values in it. deck is an instance of CardDeck that we iterate over using the forof loop. Not so fast, says JavaScript, as we see in this output:

 for(const suit of deck) {
  ^
 
 TypeError: deck is not iterable

The error is fairly descriptive and intuitive; it clearly tells us what’s missing or expected.

Implementing an Iterator

To satisfy JavaScript and allow iteration over an instance of CardDeck, we need to create a method that will serve as an iterator. But that’s easier said than done—buckle your seat belts before looking at the code.

 class​ CardDeck {
 constructor​() {
 this​.suitShapes = [​'Clubs'​, ​'Diamonds'​, ​'Hearts'​, ​'Spaces'​];
  }
 
  [Symbol.iterator]() {
 let​ index = -1;
 const​ self = ​this​;
 return​ {
  next() {
  index++;
 return​ {
  done: index >= self.suitShapes.length,
  value: self.suitShapes[index]
  };
  }
  };
  }
 }

The code implements a method with the special name [Symbol.iterator]. JavaScript looks for this method to be present in an instance to use that instance as an interator. That’s the good news. Unfortunately, though, the code is very verbose and has many levels of nesting, enough to drown us—hang on to those }s and climb to safety; we’ll revisit this method shortly after some preparation.

We will significantly improve this code shortly. Don’t feel perturbed at the sight of the verbosity in that code—you almost never have to write such code thanks to generators, as you will see soon.

First, even though this code exposes the ugly bare metals, let’s use it to understand how the iterator works. A flowchart of the steps JavaScript takes to perform the iteration is shown.

images/iterator.png

We defined a method with a special name [Symbol.iterator](). When we pass an instance of the modified class CardDeck to for...of, JavaScript will immediately look for, find, and invoke this special method.

In the [Symbol.iterator]() method, we return an object with one method named next(). On the calling side, internally for...of invokes the next() method and checks on the object returned by next() to see if that object’s done property is false. By convention, a value of true means the end of the iteration; false tells it to continue with the iteration.

If next() returned an object with the done property set to false, then the value property is used as the current value through the iteration. After the current value is used in the loop, the iteration continues with another call to the next() method. This continues until the value of done in the result returned by next() turns out to be true.

Now it’s time to put on your underwater breathing apparatus—we’re going to dive into the [Symbol.iterator]() method.

Let’s start with the first two lines of the method:

 let​ index = -1;
 const​ self = ​this​;

As soon as for...of calls the iterator method, the method initializes a variable index to a value of -1. The function will return an object to the caller, but that object needs access to the instance of CardDeck—for this purpose we save the this reference for the instance to a variable self. The self variable will be available in the lexical scope for the object we’ll soon create.

Let’s now look at the next step in the iterator method.

 return​ {
 };

After defining index and self, the iterator quickly returns an object. At this point, the caller of the iterator method, for...of, will examine this returned object and look for a next() method in that object. If it finds the method, then for...of calls the next() method. Let’s take a look at the next() method that is perched nicely inside the object we just returned.

 next() {
  index++;
 return​ {
  done: index >= self.suitShapes.length,
  value: self.suitShapes[index]
  };
 }

When for...of calls the next() method, the method increments the variable index that’s in its lexical scope and, in turn, returns an object with two properties: done and value.

At this moment in the execution sequence, the value of index is 0. Thus, the value of the done property is false and the value of the value property is ’Club’, which is the first value in the suitShapes field in the instance referenced by the self variable.

for...of will assign the property value to the iteration variable and examine the value of done. Since the value is false, for...of will call the next() method yet again. The value of index now increments to 1. This sequence of execution will continue until the next() function returns an object with the done property set to true.

Let’s rerun the code with the newly added iterator method and see the output of the iteration:

 Clubs
 Diamonds
 Hearts
 Spaces

If we set aside the verbosity, we can appreciate the dynamic nature of this code, its power, and flexibility. We will soon see how this can help us create more dynamic, lazy iterators. But first, we have to clean up the code and make it more fluent and concise. In short, we have to remove the noise.

Using Yield

Let’s put into words what the iterator method is doing. As the caller iterates over the object, the iterator yields the next value. To achieve that, we wrote quite a bit of code. We can get rid of most of it using the special yield keyword.

The caller of the iterator function should know if it should merely expect an object with the next() function or if it should do some extra work to process the result returned by yield. To help guide the caller, JavaScript relies on a special syntax—if the iterator will use yield, then the method should be decorated or marked with a *.

Let’s convert the previous implementation of the iterator method from using the object with next() to a much more fluent and concise version using yield.

 *[Symbol.iterator]() {
 for​(​const​ shape ​of​ ​this​.suitShapes) {
 yield​ shape;
  }
 }

That’s a lot better—fewer moving parts and fewer levels of nesting. The code looks almost like functions we normally write. Using a simple for loop, we iterate over the values in the suitShapes array. Within the loop, we use the yield keyword to pass the value over to the caller, taking a pause in the iteration and giving an opportunity for the caller to process the value.

yield greatly simplifies the implementation of the iterator. We can use for, while, or other forms of iterations, or even simply place multiple yield calls within the function, like so:

 *[Symbol.iterator]() {
 yield​ ​this​.suitShapes[0];
 yield​ ​this​.suitShapes[1];
 yield​ ​this​.suitShapes[2];
 yield​ ​this​.suitShapes[3];
 }

When the execution runs into a yield, it switches the flow of execution to the caller side. When the caller is done with processing the yielded value, the execution resumes right after the yield call that was already processed.

The code is almost good except for the use of the somewhat verbose [Symbol.iterator]. The generators can cure that.

Using Generators

A generator, as the name indicates, generates values. For a function to serve as a generator, its name should lead with a * and its body should have one or more yield calls. Let’s convert the iterator method to a simple generator.

 *suits() {
 for​(​const​ color ​of​ ​this​.suitShapes) {
 yield​ color;
  }
 }

We replaced the *[Symbol.iterator]() syntax with *suits() and the rest of the function stays intact—we may also use the multiple yield version if desired. This change will break the iterator. The class CardDeck no longer implements an iterator function. So, we can’t quite perform for(const suit of deck) anymore. Instead we have to call the generator function directly to perform the iteration, like so:

 const​ deck = ​new​ CardDeck();
 
 for​(​const​ suit ​of​ deck.suits()) {
  console.log(suit);
 }

On the one hand, we can’t iterate directly on the object, unless we write an iterator method in the class. On the other hand, we can have multiple generators—for example, one for suits, one for pips like Ace, King, Queen, and so forth.

Here’s the implementation of a pips generator for the CardDeck class:

 *pips() {
 yield​ ​'Ace'​;
 yield​ ​'King'​;
 yield​ ​'Queen'​;
 yield​ ​'Jack'​;
 
 for​(​let​ i = 10; i > 1; i--) {
 yield​ i.toString();
  }
 }

Using separate calls to yield, we return the non-number pips first and then loop through to return the number pips. We can use this generator much like how we used the suits generator.

 for​(​const​ pip ​of​ deck.pips()) {
  process.stdout.write(pip + ​', '​);
 }
 console.log();

Let’s quickly take a look at the output from using the pips() generator.

 Ace, King, Queen, Jack, 10, 9, 8, 7, 6, 5, 4, 3, 2,

Each of the generators we created so far created a series of values. That’s nice, but what if we want to create suits and pips, all in one series? We’ll explore an option for that next.

Combining Generators

In the CardDeck class, we have two generators: one to create the suits and the other for pips. It would be a shame if we have to duplicate the code to create one series that contains both suits and pips. Thankfully, we don’t have to endure such guilt—JavaScript provides a way to combine generators.

Let’s create a method, suitsAndPips(), in the CardDeck class.

 *suitsAndPips() {
 yield​* ​this​.suits();
 yield​* ​this​.pips();
 }

In the suitsAndPips() method, we want to return the series of suits first and then the series of pips. That’s exactly what we do, using the yield* syntax. While yield returns a single value, yield* explores the given collection and yields one value at a time from the collection.

Let’s use the new suitsAndPips() method to iterate over the entire series.

 for​(​const​ value ​of​ deck.suitsAndPips()) {
  process.stdout.write(value + ​' '​);
 }

The output from the call to the suitsAndPips() method shows the combined values:

 Clubs Diamonds Hearts Spaces Ace King Queen Jack 10 9 8 7 6 5 4 3 2

In the suitsAndPips() method, we used yield* on the result of a generator. We may also use yield* on any iterable, like Array. Let’s apply this knowledge to refactor the CardDeck class to use yield* in all three methods.

 class​ CardDeck {
 constructor​() {
 this​.suitShapes = [​'Clubs'​, ​'Diamonds'​, ​'Hearts'​, ​'Spaces'​];
  }
 
  *suits() {
 yield​* ​this​.suitShapes;
  }
 
  *pips() {
 yield​* [​'Ace'​, ​'King'​, ​'Queen'​, ​'Jack'​];
 
 yield​* Array.​from​(​new​ Array(9), (ignore, index) => 10 - index);
 
 //or using regular functions
 //yield* Array.from(
 // new Array(9), function(ignore, index) { return 10 - index; });
 
 //the above two use functional style. We may also use a more verbose
 //yield* Array.from(Array(11).keys()).reverse().splice(0, 9);
  }
 
  *suitsAndPips() {
 yield​* ​this​.suits();
 yield​* ​this​.pips();
  }
 }

The refactored suits() method operates directly on the suitShapes array. Within the pips() method we first work on the array [’Ace’, ’King’,...]. Then we need numbers from 10 to 2 in descending order. There are multiple ways to achieve this. One approach is to use the functional style with arrow functions—you’ll learn about this style in Chapter 5, Arrow Functions and Functional Style. We may also use the functional style with a regular function for the values, as shown in the commented-out part. The last commented-out line shows a solution without using the functional style. You may discover more ways to generate the array of descending numbers. Pick the one you’re most comfortable with.

The iterators we’ve seen so far worked off a collection of bounded size. However, iterators are flexible enough to allow iteration over unbounded or unknown size, as you’ll see next.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.113.197