yield * with generator function without yield

Here is my understanding on the exact mechanism of the yield * operator as opposed to the yield when I think of it after reading documentation and playing with it :

When calling next()on the iterator returned by a generator function, if it encounters :

In other words, yield * delegates the next step of the iterator to another generator function. I would therefore expect that someFunction() being just a normal generator, it would pause its execution (and the one of its caller) even if it does not have a yield statement in it, but only a return statement or even no return statement at all.

But it seems that it is not the case.

Have a look at this example where we use generators for a game flow, the goal being that each time we yield, we can pause the execution to send the new game state to the client, for instance. So the mainGameFlow generator will delegate to other generators just like function calls, but we want the execution being paused between each step :

function* mainGameFlow() {
  console.log('   (mainGameFlow) Will give money')
  yield* giveMoney()
  console.log('   (mainGameFlow) Will give card')
  yield* giveCard()
}

function* giveMoney() {
  console.log('      (giveMoney) Giving money to player')
}

function* giveCard() {
  console.log('      (giveCard) Giving card to player')
  // if(card has effect)...
  console.log('      (giveCard) Will apply card\'s effects')
  yield* applyCardEffect()
}

function* applyCardEffect() {
  console.log('         (applyCardEffect) Applying card effect')
}

console.log('Will get iterator from generator')
const iterator = mainGameFlow()
console.log('Will launch iterator first step')
iterator.next()
console.log('Iterator paused')

I would expect that the first call to next() on the mainGameFlow iterator would pause its execution just after the logging of 'Giving money to player'. Because when a generator just returns, it stops its flow just like when it yields. But here instead, all the logging lines are reached and the main iterator is paused only after the whole flow happened.

My question is : do you think there is a problem in my code ? If not, do you know a better documentation than the MDN on yield * that would clearly make understandable why the flow continues in this use case ?

Upvotes: 1

Views: 3020

Answers (2)

Bergi
Bergi

Reputation: 664307

I would expect that the first call to next() on the mainGameFlow iterator would pause its execution just after the logging of 'Giving money to player'. Because when a generator just returns, it stops its flow just like when it yields

Yes - the giveMoney() generator that encountered the return statement does stop. But the outer generator, iterator, still needs to produce a value - and since giveMoney() is done, the yield* statement resumes the execution of the mainGameFlow code, to run until it meets the next yield statement.

Upvotes: 0

Fullstack Guy
Fullstack Guy

Reputation: 16908

This section on MDN explains it nicely (read the third point):

Once paused on a yield expression, the generator's code execution remains paused until the generator's next() method is called. Each time the generator's next() method is called, the generator resumes execution, and runs until it reaches one of the following:

  • A yield, which causes the generator to once again pause and return the generator's new value. The next time next() is called, execution
    resumes with the statement immediately after the yield.
  • throw is used to throw an exception from the generator. This halts execution of the generator entirely, and execution resumes in the
    caller (as is normally the case when an exception is thrown).
  • The end of the generator function is reached. In this case, execution of the generator ends and an IteratorResult is returned to the caller in which the value is undefined and done is true.
  • A return statement is reached. In this case, execution of the generator ends and an IteratorResult is returned to the caller in
    which the value is the value specified by the return statement and
    done is true.

The yield * expression is used with generator functions or iterables. If the iterable is empty or the generator function has no yield keyword, the iterator returned from the main generator function call will be completed with the first call to next() and will never pause in between the yield * calls.

function* parentGenerator(){
  console.log("parentGenerator:: start");
  yield* childGeneratorOne();
  yield* childGeneratorTwo();
  console.log("parentGenerator:: end");
}

function* childGeneratorOne(){
  //does not yield a value, so it is complete
  console.log("childGeneratorOne:: start");
}

function* childGeneratorTwo(){
  //does not yield a value, so it is complete
  console.log("childGeneratorTwo:: start");
}

const itr = parentGenerator();
console.log(itr.next());

So in your case, when you invoke the mainGameFlow() generator function, the control goes to the giveMoney() generator function with the help of yield * expression, which has no yield keyword.

It returns an IteratorResult where the value is undefined and done is true and hence is never paused and goes on to the next line of execution.

The same happens with giveCard() call, which also returns an IteratorResult which has an undefined as the value and done is true, so the function never pauses and runs to completion, as in all cases done is true.

You can test this theory by placing a yield 1 in your giveMoney() generator function. The main generator function will be paused until you call next() again, this is because the result from the giveMoney() is { value: 1, done: false} and it is paused until done is true:

function* mainGameFlow() {
  console.log('   (mainGameFlow) Will give money')
  yield* giveMoney()
  console.log('   (mainGameFlow) Will give card')
  yield* giveCard()
}

function* giveMoney() {
  console.log('      (giveMoney) Giving money to player')
  //paused here until next() is called again
  yield 1;
}

function* giveCard() {
  console.log('      (giveCard) Giving card to player')
  console.log('      (giveCard) Will apply card\'s effects')
  yield* applyCardEffect()
}

function* applyCardEffect() {
  console.log('         (applyCardEffect) Applying card effect')
}

console.log('Will get iterator from generator')
const iterator = mainGameFlow()
console.log('Will launch iterator first step')
iterator.next()
console.log('Iterator paused')

Upvotes: 2

Related Questions