tenshi
tenshi

Reputation: 26374

Unable to chain iterators into a new iterator that isn't iterable

I've got two iterators that may or may not be iterable. If they were both guaranteed to be iterable, I could probably use an array and call .values() to get another iterable iterator:

[...iter1, ...iter2].values(); // simple :)

However, I'm struggling to find a way to chain them together if they aren't iterable. I considered using a generator function, but that would return a generator and not an iterator. Ideally, the returned iterator should also not be iterable.

This returns an iterable iterator and only works for iterable iterators:

const iter1 = [1, 2, 3].values();
const iter2 = [4, 5, 6].values();

console.log([...[...iter1, ...iter2].values()]);

This works for iterators that aren't iterable but instead returns a generator:

const iter1 = [1, 2, 3].values();
const iter2 = [4, 5, 6].values();

console.log([...(function* () {
  let item;
  while (!(item = iter1.next()).done) yield item.value;
  while (!(item = iter2.next()).done) yield item.value;
})()]);

So how would I take two iterators and chain them together to create a new iterator?


Maybe an XY problem; I'm concerned about iterability since TypeScript has separate interfaces: Iterator and IterableIterator. Is it implying that you can create an iterator that isn't iterable, or are all iterators in JavaScript iterable, and TypeScript is just being weird?

Upvotes: 3

Views: 369

Answers (4)

trincot
trincot

Reputation: 351021

If you have iterators that are not iterable, then the reasonable thing to do since ECMAScript 2025 is to turn them into instances of Iterator with Iterator.from. Such instances are called iterator helpers, and are iterable iterators. Then the iterator helper methods become available, such as flatMap. Here is how you can chain iterators even when originally they are not iterable:

// Function to help this demo:
function createNonIterableIterator(...values) {
    let i = 0;
    return {
        next: () => ({ value: values[i++], done: i > values.length })
    };
}

const iter1 = createNonIterableIterator(1, 2, 3);
const iter2 = createNonIterableIterator("a", "b");
const iter3 = createNonIterableIterator(false, true);

const chained = [iter1, iter2, iter3].values().flatMap(Iterator.from);

// The result iterator will also be iterable:
console.log(...chained);

Upvotes: 0

vitaly-t
vitaly-t

Reputation: 25930

An elegant solution, using iter-ops, which can concatenate both iterables and iterators:

import {pipe, concat} from 'iter-ops';

const iter1 = [1, 2, 3].values(); // test iterator 1
const iter2 = [4, 5, 6].values(); // test iterator 2

const i = pipe([], concat(iter1, iter2)); //=> IterableIterator<number>

console.log(...i); //=> 1 2 3 4 5 6

To test it with proper Iterator-only (non-iterable) inputs, let's create a helper below, and replace our iter1 and iter2 inputs:

function createIterator(values: number[]): Iterator<number> {
    let index = 0;
    return {
        next() {
            if (index < values.length) {
                return {value: values[index++], done: false};
            }
            return {value: undefined, done: true}
        }
    };
}

const iter1 = createIterator([1, 2, 3]); //=> Iterator<number>
const iter2 = createIterator([4, 5, 6]); //=> Iterator<number>

Concatenating the updated iter1 and iter2 will produce the same result.

Upvotes: 1

kikon
kikon

Reputation: 8890

I'll post the obvious answer that despite being old-fashioned and baroque, seems to fit the requirements:

class ConsIterable{
    constructor(...iterators){
        this.iterators = iterators;
        this.current = 0;
    }
    next(){
        let result;
        do{
            result = this.iterators[this.current].next();
            if(result.done){
                this.current++;
            }
        }while(result.done && this.current < this.iterators.length);
        return result;
    }
}

const iter1 = [1, 2, 3].values();
const iter2 = [4, 5, 6].values();

const chained_iterable = new ConsIterable(iter1, iter2);
let nxt;
do{
    nxt = chained_iterable.next();
    console.log(nxt);
}while(!nxt.done)

Upvotes: 3

tenshi
tenshi

Reputation: 26374

Ideally the returned iterator shouldn't be iterable as well, but if we mix the two possibilities mentioned in the post, we'll achieve:

const iter1 = [1, 2, 3].values();
const iter2 = [4, 5, 6].values();

const result = [];
let item;
while (!(item = iter1.next()).done) result.push(item.value);
while (!(item = iter2.next()).done) result.push(item.value);

const chained = result.values(); // but still iterable

However the returned iterator is still iterable. Honestly, I don't even know if you can even have an iterator that isn't iterable in JavaScript. I was under the impression that you could since TypeScript provides separate interfaces for them.

Deleting (with delete keyword) @@iterator also doesn't seem to work (?).

Upvotes: 0

Related Questions