Reputation: 11
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 :
yield someFunction()
: it should call someFunction()
and return a pair object with its return value in value
and whether there are more instructions to come in done
yield * someGenerator()
: it should use the iterator returned by someGenerator()
, call the next()
function on it and return a pair object with, the value
that it got from the iterator, and done
to true
only if both the iterator returned done===true
and that there are no more instructions to comeIn 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
Reputation: 664307
I would expect that the first call to
next()
on themainGameFlow
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
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