Skip to content

Instantly share code, notes, and snippets.

@waynebaylor
Created March 26, 2021 14:27

Revisions

  1. waynebaylor created this gist Mar 26, 2021.
    187 changes: 187 additions & 0 deletions iterators.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,187 @@
    # Iterators

    An iterator provides a way to iterate over a collection of values. One of the most common uses for an iterator is in [for...of loops](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of). A for...of loop will loop over the values given to it, but how does it know what order to use?

    For example, why does this for...of loop start at index 0? Why not start at the end and go backwards?

    ```typescript
    const arrayOfFruits = ['apple', 'orange', 'banana', 'pear'];

    for (const val of arrayOfFruits) {
    console.log(val);
    }

    // Expected output:
    // apple
    // orange
    // banana
    // pear
    ```

    The answer is that `Array` has a built-in iterator and it's implemented to start from index 0 and move forward through the array. While that's a useful way to iterate over the elements of an array, it isn't the _only_ way to iterate over them. We could implement our own iterator that chooses a different order.

    To implement a custom iterator there are some [rules you must follow](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol). All iterators must implement an interface like:

    ```typescript
    interface Iterator {
    next: () => {
    done: boolean;
    value: any;
    };
    }
    ```

    Here is an iterator that iterates through an array backwards:

    ```typescript
    class BackwardsIterator {
    values: any[];
    index: number;

    constructor(values: any[]) {
    this.values = values;
    this.index = values.length;
    }

    next() {
    if (this.index - 1 < 0) {
    return { done: true };
    }

    this.index--;

    return {
    done: false,
    value: this.values[this.index],
    };
    }
    }
    ```

    You can see it in action by repeatedly calling its `next()` method:

    ```typescript
    const arrayOfFruits = ['apple', 'orange', 'banana', 'pear'];
    const backwardsIter = new BackwardsIterator(arrayOfFruits);

    let result = backwardsIter.next();
    while (!result.done) {
    console.log(result.value);
    result = backwardsIter.next();
    }

    // Expected output:
    // pear
    // banana
    // orange
    // apple
    ```

    # Iterable

    Now that we have a custom iterator, let's see how we can get it to work with for...of loops. When we pass a collection or object to a for...of loop it will look for a [property that returns an iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol). Specifically it will look for the special property `[Symbol.iterator]` that when executed should return an iterator. Objects with this property are called **iterable**. Iterables must implement an interface like:

    ```typescript
    interface Iterable {
    [Symbol.iterator]: () => Iterator;
    }
    ```

    Let's create our own iterable class that holds a list of words and uses `BackwardsIterator`:

    ```typescript
    class WordList {
    words: string[] = [];

    add(word: string) {
    this.words.push(word);
    }

    [Symbol.iterator]() {
    return new BackwardsIterator(this.words);
    }
    }
    ```

    By giving `WordList` the special method `[Symbol.iterator]` we've made it iterable and you can use it directly in for...of loops. Another important detail is that we return a new iterator each time `[Symbol.iterator]` is called. This allows you to iterate as many times as you want without those iterators interfering with each other.

    ```typescript
    const places = new WordList();
    places.add('New York');
    places.add('Paris');
    places.add('Mexico City');

    for (const p of places) {
    console.log(p);
    }

    // Expected output:
    // Mexico City
    // Paris
    // New York
    ```

    # Generators

    Implementing iterators and iterables can be verbose sometimes, but luckily the language provides an alternative syntax that makes it easier to express the behavior we want. Instead of writing iterables that provide iterators (like above) we can create something called a **generator** which is both an iterable _and_ an iterator. Generators also give us some special syntax for returning values.

    The following generator function does the same thing as our `BackwardsIterator` and `WordList` classes:

    ```typescript
    function* backwardsGeneratorFunction(values: any[]) {
    let index = values.length;

    while (index - 1 >= 0) {
    index--;
    yield values[index];
    }
    }
    ```

    Notice the `*` in `function*` and the use of the `yield` keyword. Generator functions are declared with `function*`, which means you can't make an arrow function a generator function. The `yield` keyword is how a generator "returns" values to the caller and can only be used inside a generator function (just like `await` can only be used inside an `async` function). When you execute a generator function it returns a **generator**.

    ```typescript
    const powersOfTwo = [2, 4, 8, 16, 32];

    const backwardsGenerator = backwardsGeneratorFunction(powersOfTwo);

    // backwardsGenerator is an iterator
    console.log(typeof backwardsGenerator.next);

    // Expected output:
    // function

    // backwardsGenerator is also iterable
    console.log(typeof backwardsGenerator[Symbol.iterator]);

    // Expected output:
    // function

    console.log(backwardsGenerator.next());
    console.log(backwardsGenerator.next());

    // Expected output:
    // {value: 32, done: false}
    // {value: 16, done: false}
    ```

    Generators are special in that they pause execution when they get to a `yield` statement. We called `next()` twice and each time the generator ran until it hit `yield`, returned the yielded value, and then paused.

    Since a generator is both an iterable and an iterator we can use it directly in for...of loops:

    ```typescript
    const powersOfTwo = [2, 4, 8, 16, 32];

    const backwardsGenerator = backwardsGeneratorFunction(powersOfTwo);

    for (const val of backwardsGenerator) {
    console.log(val);
    }

    // Expected output:
    // 32
    // 16
    // 8
    // 4
    // 2
    ```