5

In the following, the .next() can show the last value: { value: 3, done: true }:

function* genFn() {
  yield 1;
  yield 2;
  return 3;
}

const iter = genFn();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());

But not if used as an iterable:

function* genFn() {
  yield 1;
  yield 2;
  return 3;
}

const iter = genFn();
console.log([...iter]);

It seems whatever return value or no return is used, meaning return undefined, that value is not used if it is the iterable protocol and therefore also the iterator protocol.

nonopolarity
  • 146,324
  • 131
  • 460
  • 740

1 Answers1

3

I think in other words, is return someValue in a generator function anti-pattern?

No, but you should only use it when it makes sense. In addition to calling .next() manually, yield* will produce it.

function* a() {
    yield 1;
    yield 2;
    return 3;
}

function* b() {
    console.log(yield* a());
}

console.log([...b()]);

One very practical example of where it made sense was pre-async-functions, where yield could be used as an await and you’d still want to return a value. The same concept still applies when writing similar patterns that aren’t based on promises/thenables.

Recursion without being limited by the JavaScript call stack, for example:

function* sillyAdd(a, b) {
    return b === 0
        ? a
        : yield sillyAdd(a + 1, b - 1);
}

const restack = f => (...args) => {
    const stack = [f(...args)];
    let ret = undefined;

    while (stack.length !== 0) {
        let {value, done} = stack[stack.length - 1].next(ret);

        if (done) {
            stack.pop();
            ret = value;
        } else {
            stack.push(value);
        }
    }

    return ret;
};

console.log(restack(sillyAdd)(2, 100000));
console.log('(it’s synchronous)');

Online parsing by holding state in a paused function:

function* isBalanced() {
    let open = 0;

    for (let c; c = yield;) {
        if (c === '(') {
            open++;
        } else if (c === ')') {
            open--;

            if (open < 0) {
                return false;
            }
        }
    }

    return open === 0;
}

class Parser {
    constructor(generator) {
        this.generator = generator();
        const initial = this.generator.next();
        this.done = initial.done;
        this.result = initial.value;
    }

    write(text) {
        if (this.done) {
            return;
        }

        for (const c of text) {
            const {value, done} = this.generator.next(c);

            if (done) {
                this.done = true;
                this.result = value;
                break;
            }
        }
    }

    finish() {
        if (this.done) {
            return this.result;
        }

        const {value, done} = this.generator.next();

        if (!done) {
            throw new Error('Unexpected end of input');
        }

        return value;
    }
}

const p = new Parser(isBalanced);

// the product of these could be too large to fit in memory
const chunk = '()'.repeat(1000);

for (let i = 0; i < 100; i++) {
    p.write(chunk);
}

console.log(p.finish());
Ry-
  • 218,210
  • 55
  • 464
  • 476
  • 2
    interesting `yield* a()` will produce it, but what is it used for? – nonopolarity Jan 04 '20 at 23:54
  • 1
    @nopole: Anything, like a normal return value. `yield` can pause a function. Sometimes you still want to get something from the end of that function. Maybe it seems weird because it doesn’t come up a lot, and that’s true. You probably don’t use the `(yield)` form very often either. – Ry- Jan 05 '20 at 00:04