J-Cake
J-Cake

Reputation: 1618

Why does this generator function break?

I'm playing around with ES6 Generators, because they've been quite the hype recently. My goal is to have a generator which yields a subset of a larger generator and stops. However, when the generator is called again, instead of repeating the sequence, it continues. Much like an ES6 Generator. In other words, I have a nested generator.

const a = function* (): Generator<number> {
    for (let i = 0; i < 100; i++)
        yield i;

    for (const i of a())
        yield i;
}

const b = function* (gen: Generator<number>, subsetSize: number): Generator<number> {
    let i = 0;
    for (const j of gen) {
        if (i++ > subsetSize)
            return;
        else
            yield j;
    }
    console.log("Done");
}

const gen = a();

for (let i = 0; i < 150; i++)
    for (const j of b(gen, 10))
        console.log(j);

What I'm expecting this code to do is print the numbers 0-10, print Done, then print 10-20, print Done and so on. However, the actual output is 0-10 then Done repeatedly. I'm not sure why, nor how I would get the result I'm looking for.

Upvotes: 1

Views: 243

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074088

It's because of the return in the for (const j of gen) { of b, and the fact that you have for (const j of b(gen, 10)) called in a loop.

When you return out of a for-of loop, it calls the return method of the generator being iterated (gen in this case, the one from a). That finishes the generator, so all subsequent attempts to use that generator (because the calls to b reuse gen and those calls are in a loop) just keep returning that same value. (break from the loop will also call return on the generator.)

Here's a simpler example:

function* example() {
    let i = 0;
    while (true) {
        yield i++;
    }
}

function use(gen) {
    let counter = 0;
    console.log("Starting for-of on gen");
    for (const value of gen) {
        console.log(value);
        if (value > 5) {
            console.log("Returning early");
            return;
        }
    }
    console.log("Done with for-of on gen");
}

const gen = example();
use(gen);
use(gen);
use(gen);
.as-console-wrapper {
    max-height: 100% !important;
}

If you don't want that automatic behavior of for-of, you might call next on the generator directly rather than using for-of. (If you want 0-9, "Done", 10-19, "Done", etc, you also have to tweak your counter and move where you're outputting "Done".) Example:

const a = function* ()/*: Generator<number>*/ {
    for (let i = 0; i < 100; i++)
        yield i;

    for (const i of a())
        yield i;
}

const b = function* (gen/*: Generator<number>*/, subsetSize/*: number*/)/*: Generator<number>*/ {
    let i = 0;
    let result;
    while (!(result = gen.next()).done) {
        yield result.value;
        if (++i >= subsetSize) {
            console.log("Done");
            return;
        }
    }
}

const gen = a();

for (let i = 0; i < /*150*/3; i++)
    for (const j of b(gen, 10))
        console.log(j);
.as-console-wrapper {
    max-height: 100% !important;
}

Alternatively, you could wrap the generator in an object that only forwarded next calls (and implemented [Symbol.iterator]):

const continuingIterator = it => {
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            return it.next();
        },
    };
};

Then it would be:

for (const j of continuingIterator(gen)) {

Live Example:

const continuingIterator = it => {
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            return it.next();
        },
    };
};

const a = function* ()/*: Generator<number>*/ {
    for (let i = 0; i < 100; i++)
        yield i;

    for (const i of a())
        yield i;
}

const b = function* (gen/*: Generator<number>*/, subsetSize/*: number*/)/*: Generator<number>*/ {
    let i = 0;
    for (const j of continuingIterator(gen)) {
        yield j;
        if (++i >= subsetSize) {
            console.log("Done");
            return;
        }
    }
}

const gen = a();

for (let i = 0; i < /*150*/3; i++)
    for (const j of b(gen, 10))
        console.log(j);
.as-console-wrapper {
    max-height: 100% !important;
}

Upvotes: 4

Related Questions