JavascriptLoser
JavascriptLoser

Reputation: 1962

JavaScript/ES6: How to return multiple iterators from a class?

I'm implementing a doubly linked list as part of a programming exercise, and I would like to allow the developer to iterate through its nodes both forward and backwards using the for...in notation.

At its most basic, the data structure looks like this:

class DoublyLinkedList {
    constructor(data) {
        if (data) {
            this.head = new DoublyLinkedListNode(data)
        } else {
            this.head = null
        }
    }

    append = (data) => {
        if (!this.head) {
            this.prepend(data)
        } else {
            const newTail = new DoublyLinkedListNode(data)
            let current = this.head
            while(current.next) {
                current = current.next
            }

            current.next = newTail
            newTail.prev = current
        }
    }
}

Next I added the generator functions:

    *values() {
        let current = this.head
        while (current) {
            yield current.data;
            current = current.next;
        }
    }

    *valuesBackward() {
        let currentForwards = this.head
        while (currentForwards.next) {
            currentForwards = currentForwards.next
        }

        const tail = currentForwards
        let currentBackwards = tail
        while (currentBackwards) {
            yield currentBackwards.data
            currentBackwards = currentBackwards.prev
        }
    }

I'm able to add a single, forwards iterator with the following added to the class:

[Symbol.iterator]() { return this.values()}

I tried adding both of the following to the class:

iterateForward = () => [Symbol.iterator] = () => this.valuesBackward()
iterateBackward = () => [Symbol.iterator] = () => this.valuesBackward()

And then tried to iterate using for (node in list.iterateForward()) but this failed with error TypeError: undefined is not a function.

I guess that made sense looking at the code so next I tried:

    iterateForward = () => {
        const vals = this.values()

        const it = {
            [Symbol.iterator]() {
                return vals()
            }
        }
        return it
    }

This didn't error, but the iteration didn't work - the iterator ran zero times.

What am I missing here? Is it possible to achieve what I want?

Upvotes: 3

Views: 1048

Answers (1)

jfriend00
jfriend00

Reputation: 707436

This stuff regularly confuses me so here's a summary we can both refer to.

Here's the background

An iterable is an object that has the [Symbol.iterator] property and then you when you call that property as a function, it returns an iterator object.

The iterator object has a property .next() and each time you call that function, it returns the object with the expected properties {value: x, done: false}. The iterator object will typically keep the state of the iteration in this separate object (thus iterators can be independent of each other).

So to support multiple iterators, you create multiple methods where each method returns an object that is a different iterable, each has it's own [Symbol.iterator] that when called returns a different iterator object.

So, to recap, you:

  1. Call a method that returns an iterable
  2. The iterable is an object that has the [Symbol.iterator] property on it and has access to the original object's data.
  3. When you call the function in that [Symbol.iterator] property, you get an iterator object.
  4. The iterator object contains a .next() method that gets you each item in the sequence and it does that by returning an object like this {value: x, done: false} each time you call .next().

You can skip step 1 and just have your core object have the [Symbol.iterator] property on it. That essentially becomes your default iteration. If you do:

for (let x of myObj) {
    console.log(x);
}

it will access myObj[Symbol.iterator]() to get the iterator. But, if you want to have more than one way to iterate your collection, then you create separate functions that each return their own iterable (their own object with their own [Symbol.iterator] property on them).


In an array, you've got .entries() and .values() as an example of two methods that return different iterables, that make different iterators.

let x = ['a', 'b', 'c'];

for (let v of x.values()) {
    console.log(v);                
}

This gives output:

'a'
'b'
'c'

Or, for .entries():

let x = ['a', 'b', 'c'];

for (let v of x.entries()) {
    console.log(v);                
}

[0, "a"]
[1, "b"]
[2, "c"]

So, each of .values() and .entries() returns a different object that each have a different [Symbol.iterator] that when called as a function returns a different iterator function for their unique sequence.

And, in the case of the array, .values() returns a function that when called give you the exact same iterator as just iterating the array directly (e.g. the [Symbol.iterator] property on the array itself).

Now, for your specific situation

You want to create two methods, let's say .forward() and .backward() that each create an object with a [Symbol.iterator] property that is a function that when called return their unique iterator object.

So, obj.forward() would return an object with a [Symbol.iterator] property that is a function that when called returns the iterator object with the appropriate .next() property to iterate forward and the appropriate starting state.

So, obj.backward() would return an object with a [Symbol.iterator] property that is a function that when called returns the iterator object with the appropriate .next() property to iterate backward and the appropriate starting state.

Here's an example using an array:

class myArray extends Array {
    forward() {
        return this;    // core object already has appropriate forward
                        // [Symbol.iterator] property, so we can use it
    }
    backward() {
        return {
            [Symbol.iterator]: () => {
                let i = this.length - 1;      // maintains state in closure
                return {
                    next: () => {             // get next item in iteration
                        if (i < 0) {
                            return {done: true};
                        } else {
                            return {value: this[i--], done: false};
                        }
                    }
                }
            }
        }
        
    }
}


let x = new myArray('a', 'b', 'c');

console.log("Forward using default iterator")
for (let v of x) {
    console.log(v);
}

console.log("\nUsing .forward()")
for (let v of x.forward()) {
    console.log(v);
}

console.log("\nUsing .backward()")
for (let v of x.backward()) {
    console.log(v);
}

Upvotes: 5

Related Questions