Generator Delegation
In the previous section, we showed calling regular functions from inside a generator, and how that remains a useful technique for abstracting away implementation details (like async Promise flow). But the main drawback of using a normal function for this task is that it has to behave by the normal function rules, which means it cannot pause itself with yield
like a generator can.
It may then occur to you that you might try to call one generator from another generator, using our run(..)
helper, such as:
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
// "delegating" to `*foo()` via `run(..)`
var r3 = yield run( foo );
console.log( r3 );
}
run( bar );
We run *foo()
inside of *bar()
by using our run(..)
utility again. We take advantage here of the fact that the run(..)
we defined earlier returns a promise which is resolved when its generator is run to completion (or errors out), so if we yield
out to a run(..)
instance the promise from another run(..)
call, it automatically pauses *bar()
until *foo()
finishes.
But there’s an even better way to integrate calling *foo()
into *bar()
, and it’s called yield
-delegation. The special syntax for yield
-delegation is: yield * __
(notice the extra *
). Before we see it work in our previous example, let’s look at a simpler scenario:
function *foo() {
console.log( "`*foo()` starting" );
yield 3;
yield 4;
console.log( "`*foo()` finished" );
}
function *bar() {
yield 1;
yield 2;
yield *foo(); // `yield`-delegation!
yield 5;
}
var it = bar();
it.next().value; // 1
it.next().value; // 2
it.next().value; // `*foo()` starting
// 3
it.next().value; // 4
it.next().value; // `*foo()` finished
// 5
Note: Similar to a note earlier in the chapter where I explained why I prefer function *foo() ..
instead of function* foo() ..
, I also prefer — differing from most other documentation on the topic — to say yield *foo()
instead of yield* foo()
. The placement of the *
is purely stylistic and up to your best judgment. But I find the consistency of styling attractive.
How does the yield *foo()
delegation work?
First, calling foo()
creates an iterator exactly as we’ve already seen. Then, yield *
delegates/transfers the iterator instance control (of the present *bar()
generator) over to this other *foo()
iterator.
So, the first two it.next()
calls are controlling *bar()
, but when we make the third it.next()
call, now *foo()
starts up, and now we’re controlling *foo()
instead of *bar()
. That’s why it’s called delegation — *bar()
delegated its iteration control to *foo()
.
As soon as the it
iterator control exhausts the entire *foo()
iterator, it automatically returns to controlling *bar()
.
So now back to the previous example with the three sequential Ajax requests:
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
// "delegating" to `*foo()` via `yield*`
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
The only difference between this snippet and the version used earlier is the use of yield *foo()
instead of the previous yield run(foo)
.
Note: yield *
yields iteration control, not generator control; when you invoke the *foo()
generator, you’re now yield
-delegating to its iterator. But you can actually yield
-delegate to any iterable; yield *[1,2,3]
would consume the default iterator for the [1,2,3]
array value.
Why Delegation?
The purpose of yield
-delegation is mostly code organization, and in that way is symmetrical with normal function calling.
Imagine two modules that respectively provide methods foo()
and bar()
, where bar()
calls foo()
. The reason the two are separate is generally because the proper organization of code for the program calls for them to be in separate functions. For example, there may be cases where foo()
is called standalone, and other places where bar()
calls foo()
.
For all these exact same reasons, keeping generators separate aids in program readability, maintenance, and debuggability. In that respect, yield *
is a syntactic shortcut for manually iterating over the steps of *foo()
while inside of *bar()
.
Such manual approach would be especially complex if the steps in *foo()
were asynchronous, which is why you’d probably need to use that run(..)
utility to do it. And as we’ve shown, yield *foo()
eliminates the need for a sub-instance of the run(..)
utility (like run(foo)
).
Delegating Messages
You may wonder how this yield
-delegation works not just with iterator control but with the two-way message passing. Carefully follow the flow of messages in and out, through the yield
-delegation:
function *foo() {
console.log( "inside `*foo()`:", yield "B" );
console.log( "inside `*foo()`:", yield "C" );
return "D";
}
function *bar() {
console.log( "inside `*bar()`:", yield "A" );
// `yield`-delegation!
console.log( "inside `*bar()`:", yield *foo() );
console.log( "inside `*bar()`:", yield "E" );
return "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B
console.log( "outside:", it.next( 2 ).value );
// inside `*foo()`: 2
// outside: C
console.log( "outside:", it.next( 3 ).value );
// inside `*foo()`: 3
// inside `*bar()`: D
// outside: E
console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: 4
// outside: F
Pay particular attention to the processing steps after the it.next(3)
call:
- The
3
value is passed (through theyield
-delegation in*bar()
) into the waitingyield "C"
expression inside of*foo()
. *foo()
then callsreturn "D"
, but this value doesn’t get returned all the way back to the outsideit.next(3)
call.- Instead, the
"D"
value is sent as the result of the waitingyield *foo()
expression inside of*bar()
— thisyield
-delegation expression has essentially been paused while all of*foo()
was exhausted. So"D"
ends up inside of*bar()
for it to print out. yield "E"
is called inside of*bar()
, and the"E"
value is yielded to the outside as the result of theit.next(3)
call.
From the perspective of the external iterator (it
), it doesn’t appear any differently between controlling the initial generator or a delegated one.
In fact, yield
-delegation doesn’t even have to be directed to another generator; it can just be directed to a non-generator, general iterable. For example:
function *bar() {
console.log( "inside `*bar()`:", yield "A" );
// `yield`-delegation to a non-generator!
console.log( "inside `*bar()`:", yield *[ "B", "C", "D" ] );
console.log( "inside `*bar()`:", yield "E" );
return "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B
console.log( "outside:", it.next( 2 ).value );
// outside: C
console.log( "outside:", it.next( 3 ).value );
// outside: D
console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: undefined
// outside: E
console.log( "outside:", it.next( 5 ).value );
// inside `*bar()`: 5
// outside: F
Notice the differences in where the messages were received/reported between this example and the one previous.
Most strikingly, the default array
iterator doesn’t care about any messages sent in via next(..)
calls, so the values 2
, 3
, and 4
are essentially ignored. Also, because that iterator has no explicit return
value (unlike the previously used *foo()
), the yield *
expression gets an undefined
when it finishes.
Exceptions Delegated, Too!
In the same way that yield
-delegation transparently passes messages through in both directions, errors/exceptions also pass in both directions:
function *foo() {
try {
yield "B";
}
catch (err) {
console.log( "error caught inside `*foo()`:", err );
}
yield "C";
throw "D";
}
function *bar() {
yield "A";
try {
yield *foo();
}
catch (err) {
console.log( "error caught inside `*bar()`:", err );
}
yield "E";
yield *baz();
// note: can't get here!
yield "G";
}
function *baz() {
throw "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// outside: B
console.log( "outside:", it.throw( 2 ).value );
// error caught inside `*foo()`: 2
// outside: C
console.log( "outside:", it.next( 3 ).value );
// error caught inside `*bar()`: D
// outside: E
try {
console.log( "outside:", it.next( 4 ).value );
}
catch (err) {
console.log( "error caught outside:", err );
}
// error caught outside: F
Some things to note from this snippet:
- When we call
it.throw(2)
, it sends the error message2
into*bar()
, which delegates that to*foo()
, which thencatch
es it and handles it gracefully. Then, theyield "C"
sends"C"
back out as the returnvalue
from theit.throw(2)
call. - The
"D"
value that’s nextthrow
n from inside*foo()
propagates out to*bar()
, whichcatch
es it and handles it gracefully. Then theyield "E"
sends"E"
back out as the returnvalue
from theit.next(3)
call. - Next, the exception
throw
n from*baz()
isn’t caught in*bar()
— though we didcatch
it outside — so both*baz()
and*bar()
are set to a completed state. After this snippet, you would not be able to get the"G"
value out with any subsequentnext(..)
call(s) — they will just returnundefined
forvalue
.
Delegating Asynchrony
Let’s finally get back to our earlier yield
-delegation example with the multiple sequential Ajax requests:
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
Instead of calling yield run(foo)
inside of *bar()
, we just call yield *foo()
.
In the previous version of this example, the Promise mechanism (controlled by run(..)
) was used to transport the value from return r3
in *foo()
to the local variable r3
inside *bar()
. Now, that value is just returned back directly via the yield *
mechanics.
Otherwise, the behavior is pretty much identical.
Delegating “Recursion”
Of course, yield
-delegation can keep following as many delegation steps as you wire up. You could even use yield
-delegation for async-capable generator “recursion” — a generator yield
-delegating to itself:
function *foo(val) {
if (val > 1) {
// generator recursion
val = yield *foo( val - 1 );
}
return yield request( "http://some.url/?v=" + val );
}
function *bar() {
var r1 = yield *foo( 3 );
console.log( r1 );
}
run( bar );
Note: Our run(..)
utility could have been called with run( foo, 3 )
, because it supports additional parameters being passed along to the initialization of the generator. However, we used a parameter-free *bar()
here to highlight the flexibility of yield *
.
What processing steps follow from that code? Hang on, this is going to be quite intricate to describe in detail:
run(bar)
starts up the*bar()
generator.foo(3)
creates an iterator for*foo(..)
and passes3
as itsval
parameter.- Because
3 > 1
,foo(2)
creates another iterator and passes in2
as itsval
parameter. - Because
2 > 1
,foo(1)
creates yet another iterator and passes in1
as itsval
parameter. 1 > 1
isfalse
, so we next callrequest(..)
with the1
value, and get a promise back for that first Ajax call.- That promise is
yield
ed out, which comes back to the*foo(2)
generator instance. - The
yield *
passes that promise back out to the*foo(3)
generator instance. Anotheryield *
passes the promise out to the*bar()
generator instance. And yet again anotheryield *
passes the promise out to therun(..)
utility, which will wait on that promise (for the first Ajax request) to proceed. - When the promise resolves, its fulfillment message is sent to resume
*bar()
, which passes through theyield *
into the*foo(3)
instance, which then passes through theyield *
to the*foo(2)
generator instance, which then passes through theyield *
to the normalyield
that’s waiting in the*foo(3)
generator instance. - That first call’s Ajax response is now immediately
return
ed from the*foo(3)
generator instance, which sends that value back as the result of theyield *
expression in the*foo(2)
instance, and assigned to its localval
variable. - Inside
*foo(2)
, a second Ajax request is made withrequest(..)
, whose promise isyield
ed back to the*foo(1)
instance, and thenyield *
propagates all the way out torun(..)
(step 7 again). When the promise resolves, the second Ajax response propagates all the way back into the*foo(2)
generator instance, and is assigned to its localval
variable. - Finally, the third Ajax request is made with
request(..)
, its promise goes out torun(..)
, and then its resolution value comes all the way back, which is thenreturn
ed so that it comes back to the waitingyield *
expression in*bar()
.
Phew! A lot of crazy mental juggling, huh? You might want to read through that a few more times, and then go grab a snack to clear your head!