Let's begin by demonstrating when you might need an iterator. Suppose you are implementing a data structure that can be iterated upon, let's say a tuple (fixed-length array). Your users will most likely want to transverse through the tuple by the usual order (first position, second position and so on...), so how would they do that? An example would be:
This approach is very bad! Our user needs to know implementation details to know how to iterate though the tuple. It also doesn't offer any protection, there are no safeguards against misuse of our tuple, e.g accessing a non-existing index of the value array. Furthermore, if we are not careful in the
getValues method, we may allow our users to mutate the internals of our class since when returning an array we are effectively returning only a reference to said array.
We can avoid this error by cloning the array so that any changes we make to the array outside of the Tuple class will not be reflected to the internal representation in our class, but this approach is very bad for performance and memory usage.
We may solve the problems above by implementing a
getValue method which returns a value of the tuple according to some private state.
This way is more safer than the previous implementation, but we will need to implement some method to allow resetting the iteration. This reset necessity is error prone, since we may forget to reset the index at the end of an iteration and get some unexpected behavior when doing another unrelated iteration. Another problem is: what we should do when calling
getValue more times than there are elements in the tuple without resetting the index? In the implementation above I threw an error, but this might not be the best decision. We could return another value (like undefined) but this also is problematic, see Clean Code, and should be avoided whenever possible.
We can effectively solve these problems utilizing iterators.
next() method that returns an Object with the properties:
- value: the next value in the iteration sequence. If present when
done === true, then it is the iterator's return value.
- done: a boolean which indicates if the sequence has finished or not.
After an iterator returns an Object with
done === true and its return value, any additional calls to
next() should simply return
In Typescript, we need to include at least
es2015 in the
lib options of our
tsconfig.json to have type support for iterators and iterables. We have the following interface for an iterator:
Notice that you can pass arguments to
next(), however this is not usual.
There are two other optional methods in the iterator interface,
return allows you to signal to the iterator that it should complete (setting done to true) and return its return value. Throw allows you to pass an error to the iterator which it may know how to handle. These two methods are more useful when you are dealing not with basic iterator but instead with a generator. I will explore generators in another post.
An iterable is any object which implements the
@@iterator method. This means that the object (or any object in it's prototype chain) must have a method, indexed by the
Symbol.iterator key, that returns an iterator.
Symbol.iterator is a a well-known Symbol, meaning that it is a built-in Symbol used internally by JS engine,
for... of for example uses
Symbol.iterator. You can think that an iterable is any object that you can iterate with a
for... of loop.
Many JS built-in data structures are iterables, such as
Arrays, Maps and Sets. Notice, however, that
Object isn't an iterable by default. Note that an iterable may have multiple iterators. In this (unusual) situation we define the default iterator as the one returned by
Besides the iterable interface, we have another interface called IterableIteror. This is useful for generators.
We will now see how we can implement an iterator for our Tuple example. While it is a simple example, it gives us an idea on how we may tackle harder scenarios.
Look how simple our Tuple is. We effectively separated the logic and state of transversing the structure from the tuple itself. The
TupleIterator implementation is the following:
First we need to initialize the control states,
done. Whenever the user calls
next, we check to see if the iterator is completed and if yes we simply return
If we have reached the end of the tuple, return the length of the tuple as the return value while setting
done to true. This is an example of how you can use the return value. We could have returned
undefined as well without a problem, it is up to you to decide what to return. In a more complex structure, we could allow the user to cancel the iteration process (through the
return method) and return how many items were iterated upon.
If the two if's above are false, then we just get the next value and update our index for the next iteration.
Notice how we solved the problems we pointed during the introduction, we aren't exposing any Tuple's internal representation to our user, they can't unsafely modify the representation (actually they can because of Typescript
private keyword only enforces privacy at compile time, if we want to truly enforce privacy then we can use the proposal for private fields).
Our Tuple class is simple and only contains what matters, we would only need to implement a method to get an individual value of the tuple given an index to really have something usable. If we ever want to change the iteration logic, we can extend the class and override the
@@iterator method to return another type of iterator while maintaining everything else the same.
To use our implementation, it is as simple as the following:
In this example, we will see an implementation of the breadth-first search algorithm on a binary tree using iterators. This is just for illustration purposes, in the real world it would be better to implement this as a generator.
First we will define our binary tree:
Very simple implementation, each node contains a value and up to two children. Our tree is just a wrapper around the root node, we could implement insertion and other operations but I won't in order to not pollute the example.
Now for our iterator:
Our iterator receives a node from the tree and does some basic initialization. We will return the number of nodes iterated in the process as return value of our iterator, so we need to keep track of this in the
currentRow variable is an array that will save the currentRow we are iterating. Usually when implementing BFS, we use a queue, but to avoid installing a dependency or implementing another structure to our example, our iterator simply saves a row and when needed gets another row via the
getNewRow method (requires ES2019 for the
Array.flat()). This is good enough for our purposes.
The bulk of our iterator is the
next() method. First we check if the iterator is completed and if not check if we reached the end of our current row. If positive, then get another row and check is this new row isn't empty. If yes then our iteration is completed, set the flag and return the number of nodes that were iterated upon. If the iteration isn't completed, then get the next value and update our local index and node counter.
As an exercise, feel free to implement a depth-first search iterator in our tree.
Although iterators are old (they appeared in 2015) many people don't use/known them. Iterators are the building blocks for generators with which we can build some cool stuff, like cancelable asynchronous functions and coroutines. In fact, when the
async/await syntax didn't exist, people emulated it with generators. I will cover generators in my next post, until then stay safe and happy Christmas!