Chapter 4. Generators
In Chapter 2, we identified two key drawbacks to expressing async flow control with callbacks:
-
Callback-based async doesn’t fit how our brain plans out steps of a task.
-
Callbacks aren’t trustable or composable because of inversion of control.
In Chapter 3, we detailed how Promises uninvert the inversion of control of callbacks, restoring trustability/composability.
Now we turn our attention to expressing async flow control in a sequential, synchronous-looking fashion. The “magic” that makes it possible is ES6 generators.
Breaking Run-to-Completion
In Chapter 1, we explained an expectation that JS developers almost universally rely on in their code: once a function starts executing, it runs until it completes, and no other code can interrupt and run in between.
As bizarre as it may seem, ES6 introduces a new type of function that does not behave with the run-to-completion behavior. This new type of function is called a generator.
To understand the implications, let’s consider this example:
var
x
=
1
;
function
foo
()
{
x
++
;
bar
();
// <-- what about this line?
console
.
log
(
"x:"
,
x
);
}
function
bar
()
{
x
++
;
}
foo
();
// x: 3
In this example, we know for sure that bar()
runs in between x++
and
console.log(x)
. But what if bar()
wasn’t there? Obviously the result
would be 2
instead of 3
.
Now let’s twist your brain. What if bar()
wasn’t present, but it could
still somehow run between the x++
and console.log(x)
statements? How
would that be possible?
In preemptive multithreaded languages, it would essentially be
possible for bar()
to interrupt and run at exactly the right moment
between those two statements. But JS is not preemptive, nor is it
(currently) multithreaded. And yet, a cooperative form of this
interruption (concurrency) is possible, if foo()
itself could
somehow indicate a pause at that part in the code.
Note
I use the word “cooperative” not only because of the connection
to classical concurrency terminology (see Chapter 1), but because as
you’ll see in the next snippet, the ES6 syntax for indicating a pause
point in code is yield
—suggesting a politely cooperative yielding
of control.
Here’s the ES6 code to accomplish such cooperative concurrency:
var
x
=
1
;
function
*
foo
()
{
x
++
;
yield
;
// pause!
console
.
log
(
"x:"
,
x
);
}
function
bar
()
{
x
++
;
}
Note
You will likely see most other JS documentation/code that will
format a generator declaration as function* foo() { .. }
instead of as
I’ve done here with function *foo() { .. }
—the only difference
being the stylistic positioning of the *
. The two forms are
functionally/syntactically identical, as is a third
function*foo() { .. }
(no space) form. There are arguments for both
styles, but I basically prefer function *foo..
because it then matches
when I reference a generator in writing with *foo()
. If I said only
foo()
, you wouldn’t know as clearly if I was talking about a generator
or a regular function. It’s purely a stylistic preference.
Now, how can we run the code in that previous snippet such that bar()
executes at the point of the yield
inside of *foo()
?
// construct an iterator `it` to control the generator
var
it
=
foo
();
// start `foo()` here!
it
.
next
();
x
;
// 2
bar
();
x
;
// 3
it
.
next
();
// x: 3
OK, there’s quite a bit of new and potentially confusing stuff in those two code snippets, so we’ve got plenty to wade through. But before we explain the different mechanics/syntax with ES6 generators, let’s walk through the behavior flow:
-
The
it = foo()
operation does not execute the*foo()
generator yet, but it merely constructs an iterator that will control its execution. More on iterators in a bit. -
The first
it.next()
starts the*foo()
generator, and runs thex++
on the first line of*foo()
. -
*foo()
pauses at theyield
statement, at which point that firstit.next()
call finishes. At the moment,*foo()
is still running and active, but it’s in a paused state. -
We inspect the value of
x
, and it’s now2
. -
We call
bar()
, which incrementsx
again withx++
. -
We inspect the value of
x
again, and it’s now3
. -
The final
it.next()
call resumes the*foo()
generator from where it was paused, and runs theconsole.log(..)
statement, which uses the current value ofx
of3
.
Clearly, foo()
started, but did not run-to-completion—it paused
at the yield
. We resumed foo()
later, and let it finish, but that
wasn’t even required.
So, a generator is a special kind of function that can start and stop one or more times, and doesn’t necessarily ever have to finish. While it won’t be terribly obvious yet why that’s so powerful, as we go throughout the rest of this chapter, that will be one of the fundamental building blocks we use to construct generators-as-async-flow-control as a pattern for our code.
Input and Output
A generator function is a special function with the new processing model we just alluded to. But it’s still a function, which means it still has some basic tenets that haven’t changed—namely, that it still accepts arguments (aka input), and that it can still return a value (aka output):
function
*
foo
(
x
,
y
)
{
return
x
*
y
;
}
var
it
=
foo
(
6
,
7
);
var
res
=
it
.
next
();
res
.
value
;
// 42
We pass in the arguments 6
and 7
to *foo(..)
as the parameters x
and y
, respectively. And *foo(..)
returns the value 42
back to the
calling code.
We now see a difference with how the generator is invoked compared to a
normal function. foo(6,7)
obviously looks familiar. But subtly, the
*foo(..)
generator hasn’t actually run yet as it would have with a
function.
Instead, we’re just creating an iterator object, which we assign to
the variable it
, to control the *foo(..)
generator. Then we call
it.next()
, which instructs the *foo(..)
generator to advance from
its current location, stopping either at the next yield
or end of the
generator.
The result of that next(..)
call is an object with a value
property
on it holding whatever value (if anything) was returned from *foo(..)
.
In other words, yield
caused a value to be sent out from the generator
during the middle of its execution, kind of like an intermediate
return
.
Again, it won’t be obvious yet why we need this whole indirect iterator object to control the generator. We’ll get there, I promise.
Iteration Messaging
In addition to generators accepting arguments and having return values,
there’s even more powerful and compelling input/output messaging
capability built into them, via yield
and next(..)
.
Consider:
function
*
foo
(
x
)
{
var
y
=
x
*
(
yield
);
return
y
;
}
var
it
=
foo
(
6
);
// start `foo(..)`
it
.
next
();
var
res
=
it
.
next
(
7
);
res
.
value
;
// 42
First, we pass in 6
as the parameter x
. Then we call it.next()
,
and it starts up *foo(..)
.
Inside *foo(..)
, the var y = x ..
statement starts to be processed,
but then it runs across a yield
expression. At that point, it pauses
*foo(..)
(in the middle of the assignment statement!), and essentially
requests the calling code to provide a result value for the yield
expression. Next, we call it.next( 7 )
, which is passing the 7
value
back in to be that result of the paused yield
expression.
So, at this point, the assignment statement is essentially
var y = 6 * 7
. Now, return y
returns that 42
value back as the
result of the it.next( 7 )
call.
Notice something very important but also easily confusing, even to
seasoned JS developers: depending on your perspective, there’s a
mismatch between the yield
and the next(..)
call. In general, you’re
going to have one more next(..)
call than you have yield
statements—the preceding snippet has one yield
and two next(..)
calls.
Why the mismatch?
Because the first next(..)
always starts a generator, and runs to the
first yield
. But it’s the second next(..)
call that fulfills the
first paused yield
expression, and the third next(..)
would fulfill
the second yield
, and so on.
Tale of Two Questions
Actually, which code you’re thinking about primarily will affect whether there’s a perceived mismatch or not.
Consider only the generator code:
var
y
=
x
*
(
yield
);
return
y
;
This first yield
is basically asking a question: “What value
should I insert here?”
Who’s going to answer that question? Well, the first next()
has
already run to get the generator up to this point, so obviously it
can’t answer the question. So, the second next(..)
call must answer
the question posed by the first yield
.
See the mismatch—second-to-first?
But let’s flip our perspective. Let’s look at it not from the generator’s point of view, but from the iterator’s point of view.
To properly illustrate this perspective, we also need to explain that
messages can go in both directions—yield ..
as an expression can
send out messages in response to next(..)
calls, and next(..)
can
send values to a paused yield
expression. Consider this slightly
adjusted code:
function
*
foo
(
x
)
{
var
y
=
x
*
(
yield
"Hello"
);
// <-- yield a value!
return
y
;
}
var
it
=
foo
(
6
);
var
res
=
it
.
next
();
// first `next()`, don't pass anything
res
.
value
;
// "Hello"
res
=
it
.
next
(
7
);
// pass `7` to waiting `yield`
res
.
value
;
// 42
yield ..
and next(..)
pair together as a two-way message passing
system during the execution of the generator.
So, looking only at the iterator code:
var
res
=
it
.
next
();
// first `next()`, don't pass anything
res
.
value
;
// "Hello"
res
=
it
.
next
(
7
);
// pass `7` to waiting `yield`
res
.
value
;
// 42
Note
We don’t pass a value to the first next()
call, and that’s on
purpose. Only a paused yield
could accept such a value passed by a
next(..)
, and at the beginning of the generator when we call the first
next()
, there is no paused yield
to accept such a value. The specification and all
compliant browsers just silently discard anything passed to the first
next()
. It’s still a bad idea to pass a value, as you’re just creating
silently failing code that’s confusing. So, always start a generator
with an argument-free next()
.
The first next()
call (with nothing passed to it) is basically asking
a question: “What next value does the *foo(..)
generator have to
give me?” And who answers this question? The first yield "hello"
expression.
See? No mismatch there.
Depending on who you think about asking the question, there is either
a mismatch between the yield
and next(..)
calls, or not.
But wait! There’s still an extra next()
compared to the number of
yield
statements. So, that final it.next(7)
call is again asking the
question about what next value the generator will produce. But there’s
no more yield
statements left to answer, is there? So who answers?
The return
statement answers the question!
And if there is no return
in your generator—return
is certainly
not any more required in generators than in regular functions—there’s
always an assumed/implicit return;
(aka return undefined;
), which
serves the purpose of default answering the question posed by the
final it.next(7)
call.
These questions and answers—the two-way message passing with yield
and next(..)
—are quite powerful, but it’s not obvious at all how
these mechanisms are connected to async flow control. We’re getting
there!
Multiple Iterators
It may appear from the syntactic usage that when you use an iterator to control a generator, you’re controlling the declared generator function itself. But there’s a subtlety that easy to miss: each time you construct an iterator, you are implicitly constructing an instance of the generator which that iterator will control.
You can have multiple instances of the same generator running at the same time, and they can even interact:
function
*
foo
()
{
var
x
=
yield
2
;
z
++
;
var
y
=
yield
(
x
*
z
);
console
.
log
(
x
,
y
,
z
);
}
var
z
=
1
;
var
it1
=
foo
();
var
it2
=
foo
();
var
val1
=
it1
.
next
().
value
;
// 2 <-- yield 2
var
val2
=
it2
.
next
().
value
;
// 2 <-- yield 2
val1
=
it1
.
next
(
val2
*
10
).
value
;
// 40 <-- x:20, z:2
val2
=
it2
.
next
(
val1
*
5
).
value
;
// 600 <-- x:200, z:3
it1
.
next
(
val2
/
2
);
// y:300
// 20 300 3
it2
.
next
(
val1
/
4
);
// y:10
// 200 10 3
Warning
The most common usage of multiple instances of the same generator running concurrently is not such interactions, but when the generator is producing its own values without input, perhaps from some independently connected resource. We’ll talk more about value production in the next section.
Let’s briefly walk through the processing:
-
Both instances of
*foo()
are started at the same time, and bothnext()
calls reveal avalue
of2
from theyield 2
statements, respectively. -
val2 * 10
is2 * 10
, which is sent into the first generator instanceit1
, so thatx
gets value20
.z
is incremented from1
to2
, and then20 * 2
isyield
ed out, settingval1
to40
. -
val1 * 5
is40 * 5
, which is sent into the second generator instanceit2
, so thatx
gets value200
.z
is incremented again, from2
to3
, and then200 * 3
isyield
ed out, settingval2
to600
. -
val2 / 2
is600 / 2
, which is sent into the first generator instanceit1
, so thaty
gets value300
, then printing out20 300 3
for itsx y z
values, respectively. -
val1 / 4
is40 / 4
, which is sent into the second generator instanceit2
, so thaty
gets value10
, then printing out200 10 3
for itsx y z
values, respectively.
That’s a fun example to run through in your mind. Did you keep it straight?
Interleaving
Recall this scenario from the “Run-to-completion” section of Chapter 1:
var
a
=
1
;
var
b
=
2
;
function
foo
()
{
a
++
;
b
=
b
*
a
;
a
=
b
+
3
;
}
function
bar
()
{
b
--
;
a
=
8
+
b
;
b
=
a
*
2
;
}
With normal JS functions, of course either foo()
can run completely
first, or bar()
can run completely first, but foo()
cannot
interleave its individual statements with bar()
. So, there are only
two possible outcomes to the preceding program.
However, with generators, clearly interleaving (even in the middle of statements!) is possible:
var
a
=
1
;
var
b
=
2
;
function
*
foo
()
{
a
++
;
yield
;
b
=
b
*
a
;
a
=
(
yield
b
)
+
3
;
}
function
*
bar
()
{
b
--
;
yield
;
a
=
(
yield
8
)
+
b
;
b
=
a
*
(
yield
2
);
}
Depending on what respective order the iterators controlling *foo()
and *bar()
are called, the preceding program could produce several
different results. In other words, we can actually illustrate (in a sort
of fake-ish way) the theoretical threaded race conditions
circumstances discussed in Chapter 1, by interleaving the two generator
interations over the same shared variables.
First, let’s make a helper called step(..)
that controls an
iterator:
function
step
(
gen
)
{
var
it
=
gen
();
var
last
;
return
function
()
{
// whatever is `yield`ed out, just
// send it right back in the next time!
last
=
it
.
next
(
last
).
value
;
};
}
step(..)
initializes a generator to create its it
iterator, then
returns a function which, when called, advances the iterator by one
step. Additionally, the previously yield
ed out value is sent right
back in at the next step. So, yield 8
will just become 8
and
yield b
will just be b
(whatever it was at the time of yield
).
Now, just for fun, let’s experiment to see the effects of interleaving
these different chunks of *foo()
and *bar()
. We’ll start with the
boring base case, making sure *foo()
totally finishes before *bar()
(just like we did in Chapter 1):
// make sure to reset `a` and `b`
a
=
1
;
b
=
2
;
var
s1
=
step
(
foo
);
var
s2
=
step
(
bar
);
// run `*foo()` completely first
s1
();
s1
();
s1
();
// now run `*bar()`
s2
();
s2
();
s2
();
s2
();
console
.
log
(
a
,
b
);
// 11 22
The end result is 11
and 22
, just as it was in the Chapter 1
version. Now let’s mix up the interleaving ordering and see how it
changes the final values of a
and b
:
// make sure to reset `a` and `b`
a
=
1
;
b
=
2
;
var
s1
=
step
(
foo
);
var
s2
=
step
(
bar
);
s2
();
// b--;
s2
();
// yield 8
s1
();
// a++;
s2
();
// a = 8 + b;
// yield 2
s1
();
// b = b * a;
// yield b
s1
();
// a = b + 3;
s2
();
// b = a * 2;
Before I tell you the results, can you figure out what a
and b
are
after the preceding program? No cheating!
console
.
log
(
a
,
b
);
// 12 18
Note
As an exercise for the reader, try to see how many other
combinations of results you can get back rearranging the order of the
s1()
and s2()
calls. Don’t forget you’ll always need three s1()
calls and four s2()
calls. Recall the discussion earlier about
matching next()
with yield
for the reasons why.
You almost certainly won’t want to intentionally create this level of interleaving confusion, as it creates incredibly difficult to understand code. But the exercise is interesting and instructive to understand more about how multiple generators can run concurrently in the same shared scope, because there will be places where this capability is quite useful.
We’ll discuss generator concurrency in more detail in “Generator Concurrency”.
Generator-ing Values
In the previous section, we mentioned an interesting use for generators, as a way to produce values. This is not the main focus in this chapter, but we’d be remiss if we didn’t cover the basics, especially because this use case is essentially the origin of the name: generators.
We’re going to take a slight diversion into the topic of iterators for a bit, but we’ll circle back to how they relate to generators and using a generator to generate values.
Producers and Iterators
Imagine you’re producing a series of values where each value has a definable relationship to the previous value. To do this, you’re going to need a stateful producer that remembers the last value it gave out.
You can implement something like that straightforwardly using a function closure (see the Scope & Closures title of this series):
var
gimmeSomething
=
(
function
(){
var
nextVal
;
return
function
(){
if
(
nextVal
===
undefined
)
{
nextVal
=
1
;
}
else
{
nextVal
=
(
3
*
nextVal
)
+
6
;
}
return
nextVal
;
};
})();
gimmeSomething
();
// 1
gimmeSomething
();
// 9
gimmeSomething
();
// 33
gimmeSomething
();
// 105
Note
The nextVal
computation logic here could have been simplified,
but conceptually, we don’t want to calculate the next value (aka
nextVal
) until the next gimmeSomething()
call happens, because in
general that could be a resource-leaky design for producers of more
persistent or resource-limited values than simple number
s.
Generating an arbitrary number series isn’t a terribly realistic example. But what if you were generating records from a data source? You could imagine much the same code.
In fact, this task is a very common design pattern, usually solved by
iterators. An iterator is a well-defined interface for stepping
through a series of values from a producer. The JS interface for
iterators, as it is in most languages, is to call next()
each time you
want the next value from the producer.
We could implement the standard iterator interface for our number series producer:
var
something
=
(
function
(){
var
nextVal
;
return
{
// needed for `for..of` loops
[
Symbol
.
iterator
]
:
function
(){
return
this
;
},
// standard iterator interface method
next
:
function
(){
if
(
nextVal
===
undefined
)
{
nextVal
=
1
;
}
else
{
nextVal
=
(
3
*
nextVal
)
+
6
;
}
return
{
done
:
false
,
value
:
nextVal
};
}
};
})();
something
.
next
().
value
;
// 1
something
.
next
().
value
;
// 9
something
.
next
().
value
;
// 33
something
.
next
().
value
;
// 105
Note
We’ll explain why we need the [Symbol.iterator]: ..
part of
this code snippet in “Iterables”. Syntactically though, two
ES6 features are at play. First, the [ .. ]
syntax is called a
computed property name (see the this & Object Prototypes title of
this series). It’s a way in an object literal definition to specify an
expression and use the result of that expression as the name for the
property. Next, Symbol.iterator
is one of ES6’s predefined special
Symbol
values (see the ES6 & Beyond title of this series).
The next()
call returns an object with two properties: done
is a
boolean
value signaling the iterator’s complete status; value
holds the iteration value.
ES6 also adds the for..of
loop, which means that a standard iterator
can automatically be consumed with native loop syntax:
for
(
var
v
of
something
)
{
console
.
log
(
v
);
// don't let the loop run forever!
if
(
v
>
500
)
{
break
;
}
}
// 1 9 33 105 321 969
Note
Because our something
iterator always returns done:false
,
this for..of
loop would run forever, which is why we put the break
conditional in. It’s totally OK for iterators to be never-ending, but
there are also cases where the iterator will run over a finite set of
values and eventually return a done:true
.
The for..of
loop automatically calls next()
for each iteration—it
doesn’t pass any values in to the next()
—and it will automatically
terminate on receiving a done:true
. It’s quite handy for looping over
a set of data.
Of course, you could manually loop over iterators, calling next()
and
checking for the done:true
condition to know when to stop:
for
(
var
ret
;
(
ret
=
something
.
next
())
&&
!
ret
.
done
;
)
{
console
.
log
(
ret
.
value
);
// don't let the loop run forever!
if
(
ret
.
value
>
500
)
{
break
;
}
}
// 1 9 33 105 321 969
Note
This manual for
approach is certainly uglier than the ES6
for..of
loop syntax, but its advantage is that it affords you the
opportunity to pass in values to the next(..)
calls if necessary.
In addition to making your own iterators, many built-in data
structures in JS (as of ES6), like array
s, also have default
iterators:
var
a
=
[
1
,
3
,
5
,
7
,
9
];
for
(
var
v
of
a
)
{
console
.
log
(
v
);
}
// 1 3 5 7 9
The for..of
loop asks a
for its iterator, and automatically uses
it to iterate over a
’s values.
Note
It may seem a strange omission by ES6, but regular object
s
intentionally do not come with a default iterator the way array
s do.
The reasons go deeper than we will cover here. If all you want is to
iterate over the properties of an object (with no particular guarantee
of ordering), Object.keys(..)
returns an array
, which can then be used like for (var k of Object.keys(obj)) { ..
. Such a for..of
loop over an object’s keys would be similar to a for..in
loop, except that Object.keys(..)
does not include properties from the [[Prototype]]
chain while for..in
does (see the this & Object Prototypes title of this series).
Iterables
The something
object in our running example is called an iterator,
as it has the next()
method on its interface. But a closely related
term is iterable, which is an object
that contains an iterator
that can iterate over its values.
As of ES6, the way to retrieve an iterator from an iterable is that
the iterable must have a function on it, with the name being the
special ES6 symbol value Symbol.iterator
. When this function is
called, it returns an iterator. Though not required, generally each
call should return a fresh new iterator.
a
in the previous snippet is an iterable. The for..of
loop
automatically calls its Symbol.iterator
function to construct an
iterator. But we could of course call the function manually, and use
the iterator it returns:
var
a
=
[
1
,
3
,
5
,
7
,
9
];
var
it
=
a
[
Symbol
.
iterator
]();
it
.
next
().
value
;
// 1
it
.
next
().
value
;
// 3
it
.
next
().
value
;
// 5
..
In the previous code listing that defined something
, you may have
noticed this line:
[
Symbol
.
iterator
]
:
function
(){
return
this
;
}
That little bit of confusing code is making the something
value—the
interface of the something
iterator—also an iterable; it’s now
both an iterable and an iterator. Then, we pass something
to the
for..of
loop:
for
(
var
v
of
something
)
{
..
}
The for..of
loop expects something
to be an iterable, so it looks
for and calls its Symbol.iterator
function. We defined that function
to simply return this
, so it just gives itself back, and the for..of
loop is none the wiser.
Generator Iterator
Let’s turn our attention back to generators, in the context of
iterators. A generator can be treated as a producer of values that we
extract one at a time through an iterator interface’s next()
calls.
So, a generator itself is not technically an iterable, though it’s very similar—when you execute the generator, you get an iterator back:
function
*
foo
(){
..
}
var
it
=
foo
();
We can implement the something
infinite number series producer from
earlier with a generator, like this:
function
*
something
()
{
var
nextVal
;
while
(
true
)
{
if
(
nextVal
===
undefined
)
{
nextVal
=
1
;
}
else
{
nextVal
=
(
3
*
nextVal
)
+
6
;
}
yield
nextVal
;
}
}
Note
A while..true
loop would normally be a very bad thing to
include in a real JS program, at least if it doesn’t have a break
or
return
in it, as it would likely run forever, synchronously, and
block/lock-up the browser UI. However, in a generator, such a loop is
generally totally OK if it has a yield
in it, as the generator will
pause at each iteration, yield
ing back to the main program and/or to
the event loop queue. To put it glibly, “generators put the
while..true
back in JS programming!”
That’s a fair bit cleaner and simpler, right? Because the generator
pauses at each yield
, the state (scope) of the function *something()
is kept around, meaning there’s no need for the closure boilerplate to
preserve variable state across calls.
Not only is it simpler code—we don’t have to make our own iterator
interface—it actually is more reason-able code, because it more
clearly expresses the intent. For example, the while..true
loop tells
us the generator is intended to run forever—to keep generating
values as long as we keep asking for them.
And now we can use our shiny new *something()
generator with a
for..of
loop, and you’ll see it works basically identically:
for
(
var
v
of
something
())
{
console
.
log
(
v
);
// don't let the loop run forever!
if
(
v
>
500
)
{
break
;
}
}
// 1 9 33 105 321 969
But don’t skip over for (var v of something()) ..
! We didn’t just
reference something
as a value like in earlier examples, but instead
called the *something()
generator to get its iterator for the
for..of
loop to use.
If you’re paying close attention, two questions may arise from this interaction between the generator and the loop:
-
Why couldn’t we say
for (var v of something) ..
? Becausesomething
here is a generator, which is not an iterable. We have to callsomething()
to construct a producer for thefor..of
loop to iterate over. -
The
something()
call produces an iterator, but thefor..of
loop wants an iterable, right? Yep. The generator’s iterator also has aSymbol.iterator
function on it, which basically does areturn this
, just like thesomething
iterable we defined earlier. In other words, a generator’s iterator is also an iterable!
Stopping the Generator
In the previous example, it would appear the iterator instance for the
*something()
generator was basically left in a suspended state forever
after the break
in the loop was called.
But there’s a hidden behavior that takes care of that for you. “Abnormal
completion” (i.e., “early termination”) of the for..of
loop—generally caused by a break
, return
, or an uncaught exception—sends a signal to the generator’s iterator for it to terminate.
Note
Technically, the for..of
loop also sends this signal to the
iterator at the normal completion of the loop. For a generator, that’s
essentially a moot operation, as the generator’s iterator had to
complete first so the for..of
loop completed. However, custom
iterators might desire to receive this additional signal from
for..of
loop consumers.
While a for..of
loop will automatically send this signal, you may wish
to send the signal manually to an iterator; you do this by calling
return(..)
.
If you specify a try..finally
clause inside the generator, it will
always be run even when the generator is externally completed. This is
useful if you need to clean up resources (database connections, etc.):
function
*
something
()
{
try
{
var
nextVal
;
while
(
true
)
{
if
(
nextVal
===
undefined
)
{
nextVal
=
1
;
}
else
{
nextVal
=
(
3
*
nextVal
)
+
6
;
}
yield
nextVal
;
}
}
// cleanup clause
finally
{
console
.
log
(
"cleaning up!"
);
}
}
The earlier example with break
in the for..of
loop will trigger the
finally
clause. But you could instead manually terminate the
generator’s iterator instance from the outside with return(..)
:
var
it
=
something
();
for
(
var
v
of
it
)
{
console
.
log
(
v
);
// don't let the loop run forever!
if
(
v
>
500
)
{
console
.
log
(
// complete the generator's iterator
it
.
return
(
"Hello World"
).
value
);
// no `break` needed here
}
}
// 1 9 33 105 321 969
// cleaning up!
// Hello World
When we call it.return(..)
, it immediately terminates the generator,
which of course runs the finally
clause. Also, it sets the returned
value
to whatever you passed in to return(..)
, which is how
"Hello World"
comes right back out. We also don’t need to include a
break
now because the generator’s iterator is set to done:true
, so
the for..of
loop will terminate on its next iteration.
Generators owe their namesake mostly to this consuming produced values use. But again, that’s just one of the uses for generators, and frankly not even the main one we’re concerned with in the context of this book.
But now that we more fully understand some of the mechanics of how they work, we can next turn our attention to how generators apply to async concurrency.
Iterating Generators Asynchronously
What do generators have to do with async coding patterns, fixing problems with callbacks, and the like? Let’s get to answering that important question.
We should revisit one of our scenarios from Chapter 3. Let’s recall the callback approach:
function
foo
(
x
,
y
,
cb
)
{
ajax
(
"http://some.url.1/?x="
+
x
+
"&y="
+
y
,
cb
);
}
foo
(
11
,
31
,
function
(
err
,
text
)
{
if
(
err
)
{
console
.
error
(
err
);
}
else
{
console
.
log
(
text
);
}
}
);
If we wanted to express this same task flow control with a generator, we could do:
function
foo
(
x
,
y
)
{
ajax
(
"http://some.url.1/?x="
+
x
+
"&y="
+
y
,
function
(
err
,
data
){
if
(
err
)
{
// throw an error into `*main()`
it
.
throw
(
err
);
}
else
{
// resume `*main()` with received `data`
it
.
next
(
data
);
}
}
);
}
function
*
main
()
{
try
{
var
text
=
yield
foo
(
11
,
31
);
console
.
log
(
text
);
}
catch
(
err
)
{
console
.
error
(
err
);
}
}
var
it
=
main
();
// start it all up!
it
.
next
();
At first glance, this snippet is longer, and perhaps a little more complex looking, than the callback snippet before it. But don’t let that impression get you off track. The generator snippet is actually much better! But there’s a lot going on for us to explain.
First, let’s look at this part of the code, which is the most important:
var
text
=
yield
foo
(
11
,
31
);
console
.
log
(
text
);
Think about how that code works for a moment. We’re calling a normal
function foo(..)
and we’re apparently able to get back the text
from
the Ajax call, even though it’s asynchronous.
How is that possible? If you recall the beginning of Chapter 1, we had almost identical code:
var
data
=
ajax
(
"..url 1.."
);
console
.
log
(
data
);
And that code didn’t work! Can you spot the difference? It’s the yield
used in a generator.
That’s the magic! That’s what allows us to have what appears to be blocking, synchronous code, but it doesn’t actually block the whole program; it only pauses/blocks the code in the generator itself.
In yield foo(11,31)
, first the foo(11,31)
call is made, which
returns nothing (aka undefined
), so we’re making a call to request
data, but we’re actually then doing yield undefined
. That’s OK,
because the code is not currently relying on a yield
ed value to do
anything interesting. We’ll revisit this point later in the chapter.
We’re not using yield
in a message passing sense here, only in a flow
control sense to pause/block. Actually, it will have message passing,
but only in one direction, after the generator is resumed.
So, the generator pauses at the yield
, essentially asking the
question, “what value should I return to assign to the variable text
?”
Who’s going to answer that question?
Look at foo(..)
. If the Ajax request is successful, we call:
it
.
next
(
data
);
That’s resuming the generator with the response data, which means that
our paused yield
expression receives that value directly, and then as
it restarts the generator code, that value gets assigned to the local
variable text
.
Pretty cool, huh?
Take a step back and consider the implications. We have totally
synchronous-looking code inside the generator (other than the yield
keyword itself), but hidden behind the scenes, inside of foo(..)
, the
operations can complete asynchronously.
That’s huge! That’s a nearly perfect solution to our previously stated problem with callbacks not being able to express asynchrony in a sequential, synchronous fashion that our brains can relate to.
In essence, we are abstracting the asynchrony away as an implementation detail, so that we can reason synchronously/sequentially about our flow control: “Make an Ajax request, and when it finishes print out the response.” And of course, we just expressed two steps in the flow control, but this same capabililty extends without bounds, to let us express however many steps we need to.
Tip
This is such an important realization, just go back and read the last three paragraphs again to let it sink in!
Synchronous Error Handling
But the preceding generator code has even more goodness to yield to
us. Let’s turn our attention to the try..catch
inside the generator:
try
{
var
text
=
yield
foo
(
11
,
31
);
console
.
log
(
text
);
}
catch
(
err
)
{
console
.
error
(
err
);
}
How does this work? The foo(..)
call is asynchronously completing, and
doesn’t try..catch
fail to catch asynchronous errors, as we looked at
in Chapter 3?
We already saw how the yield
lets the assignment statement pause to
wait for foo(..)
to finish, so that the completed response can be
assigned to text
. The awesome part is that this yield
pausing also
allows the generator to catch
an error. We throw that error into the
generator with this part of the earlier code listing:
if
(
err
)
{
// throw an error into `*main()`
it
.
throw
(
err
);
}
The yield
-pause nature of generators means that not only do we get
synchronous-looking return
values from async function calls, but we
can also synchronously catch
errors from those async function calls!
So we’ve seen we can throw errors into a generator, but what about throwing errors out of a generator? Exactly as you’d expect:
function
*
main
()
{
var
x
=
yield
"Hello World"
;
yield
x
.
toLowerCase
();
// cause an exception!
}
var
it
=
main
();
it
.
next
().
value
;
// Hello World
try
{
it
.
next
(
42
);
}
catch
(
err
)
{
console
.
error
(
err
);
// TypeError
}
Of course, we could have manually thrown an error with throw ..
instead of causing an exception.
We can even catch
the same error that we throw(..)
into the
generator, essentially giving the generator a chance to handle it but if
it doesn’t, the iterator code must handle it:
function
*
main
()
{
var
x
=
yield
"Hello World"
;
// never gets here
console
.
log
(
x
);
}
var
it
=
main
();
it
.
next
();
try
{
// will `*main()` handle this error? we'll see!
it
.
throw
(
"Oops"
);
}
catch
(
err
)
{
// nope, didn't handle it!
console
.
error
(
err
);
// Oops
}
Synchronous-looking error handling (via try..catch
) with async code is
a huge win for readability and reason-ability.
Generators + Promises
In our previous discussion, we showed how generators can be iterated asynchronously, which is a huge step forward in sequential reason-ability over the spaghetti mess of callbacks. But we lost something very important: the trustability and composability of Promises (see Chapter 3)!
Don’t worry—we can get that back. The best of all worlds in ES6 is to combine generators (synchronous-looking async code) with Promises (trustable and composable).
But how?
Recall from Chapter 3 the Promise-based approach to our running Ajax example:
function
foo
(
x
,
y
)
{
return
request
(
"http://some.url.1/?x="
+
x
+
"&y="
+
y
);
}
foo
(
11
,
31
)
.
then
(
function
(
text
){
console
.
log
(
text
);
},
function
(
err
){
console
.
error
(
err
);
}
);
In our earlier generator code for the running Ajax example, foo(..)
returned nothing (undefined
), and our iterator control code didn’t
care about that yield
ed value.
But here the Promise-aware foo(..)
returns a promise after making the
Ajax call. That suggests that we could construct a promise with
foo(..)
and then yield
it from the generator, and then the
iterator control code would receive that promise.
But what should the iterator do with the promise?
It should listen for the promise to resolve (fulfillment or rejection), and then either resume the generator with the fulfillment message or throw an error into the generator with the rejection reason.
Let me repeat that, because it’s so important. The natural way to get
the most out of Promises and generators is to yield
a Promise, and
wire that Promise to control the generator’s iterator.
Let’s give it a try! First, we’ll put the Promise-aware foo(..)
together with the generator *main()
:
function
foo
(
x
,
y
)
{
return
request
(
"http://some.url.1/?x="
+
x
+
"&y="
+
y
);
}
function
*
main
()
{
try
{
var
text
=
yield
foo
(
11
,
31
);
console
.
log
(
text
);
}
catch
(
err
)
{
console
.
error
(
err
);
}
}
The most powerful revelation in this refactor is that the code inside
*main()
did not have to change at all! Inside the generator,
whatever values are yield
ed out is just an opaque implementation
detail, so we’re not even aware it’s happening, nor do we need to worry
about it.
But how are we going to run *main()
now? We still have some of the
implementation plumbing work to do, to receive and wire up the yield
ed
promise so that it resumes the generator upon resolution. We’ll start by
trying that manually:
var
it
=
main
();
var
p
=
it
.
next
().
value
;
// wait for the `p` promise to resolve
p
.
then
(
function
(
text
){
it
.
next
(
text
);
},
function
(
err
){
it
.
throw
(
err
);
}
);
Actually, that wasn’t so painful at all, was it?
This snippet should look very similar to what we did earlier with the
manually wired generator controlled by the error-first callback. Instead
of an if (err) { it.throw..
, the promise already splits fulfillment
(success) and rejection (failure) for us, but otherwise the iterator
control is identical.
Now, we’ve glossed over some important details.
Most importantly, we took advantage of the fact that we knew that
*main()
only had one Promise-aware step in it. What if we wanted to be
able to Promise-drive a generator no matter how many steps it has? We
certainly don’t want to manually write out the Promise chain differently
for each generator! What would be much nicer is if there was a way to
repeat (aka loop over) the iteration control, and each time a Promise
comes out, wait on its resolution before continuing.
Also, what if the generator throws out an error (intentionally or
accidentally) during the it.next(..)
call? Should we quit, or should
we catch
it and send it right back in? Similarly, what if we
it.throw(..)
a Promise rejection into the generator, but it’s not
handled, and comes right back out?
Promise-Aware Generator Runner
The more you start to explore this path, the more you realize, “wow,
it’d be great if there was just some utility to do it for me.” And
you’re absolutely correct. This is such an important pattern, and you
don’t want to get it wrong (or exhaust yourself repeating it over and
over), so your best bet is to use a utility that is specifically
designed to run Promise-yield
ing generators in the manner we’ve
illustrated.
Several Promise abstraction libraries provide just such a utility,
including my asynquence library and its runner(..)
, which are
discussed in Appendix A of this book.
But for the sake of learning and illustration, let’s just define our own
standalone utility that we’ll call run(..)
:
// thanks to Benjamin Gruenbaum (@benjamingr on GitHub) for
// big improvements here!
function
run
(
gen
)
{
var
args
=
[].
slice
.
call
(
arguments
,
1
),
it
;
// initialize the generator in the current context
it
=
gen
.
apply
(
this
,
args
);
// return a promise for the generator completing
return
Promise
.
resolve
()
.
then
(
function
handleNext
(
value
){
// run to the next yielded value
var
next
=
it
.
next
(
value
);
return
(
function
handleResult
(
next
){
// generator has completed running?
if
(
next
.
done
)
{
return
next
.
value
;
}
// otherwise keep going
else
{
return
Promise
.
resolve
(
next
.
value
)
.
then
(
// resume the async loop on
// success, sending the resolved
// value back into the generator
handleNext
,
// if `value` is a rejected
// promise, propagate error back
// into the generator for its own
// error handling
function
handleErr
(
err
)
{
return
Promise
.
resolve
(
it
.
throw
(
err
)
)
.
then
(
handleResult
);
}
);
}
})(
next
);
}
);
}
As you can see, it’s a quite a bit more complex than you’d probably want to author yourself, and you especially wouldn’t want to repeat this code for each generator you use. So, a utility/library helper is definitely the way to go. Nevertheless, I encourage you to spend a few minutes studying that code listing to get a better sense of how to manage the generator + Promise negotiation.
How would you use run(..)
with *main()
in our running Ajax
example?
function
*
main
()
{
// ..
}
run
(
main
);
That’s it! The way we wired run(..)
, it will automatically advance the
generator you pass to it, asynchronously until completion.
Note
The run(..)
we defined returns a promise which is wired to
resolve once the generator is complete, or receive an uncaught exception
if the generator doesn’t handle it. We don’t show that capability here,
but we’ll come back to it later in the chapter.
ES7: async and await?
The preceding pattern—generators yield
ing Promises that then control
the generator’s iterator to advance it to completion—is such a
powerful and useful approach, it would be nicer if we could do it
without the clutter of the library utility helper (aka run(..)
).
There’s probably good news on that front. At the time of this writing, there’s early but strong support for a proposal for more syntactic addition in this realm for the post-ES6, ES7-ish timeframe. Obviously, it’s too early to guarantee the details, but there’s a pretty decent chance it will shake out similar to the following:
function
foo
(
x
,
y
)
{
return
request
(
"http://some.url.1/?x="
+
x
+
"&y="
+
y
);
}
async
function
main
()
{
try
{
var
text
=
await
foo
(
11
,
31
);
console
.
log
(
text
);
}
catch
(
err
)
{
console
.
error
(
err
);
}
}
main
();
As you can see, there’s no run(..)
call (meaning no need for a library
utility!) to invoke and drive main()
—it’s just called as a normal
function. Also, main()
isn’t declared as a generator function anymore;
it’s a new kind of function: async function
. And finally, instead of
yield
ing a Promise, we await
for it to resolve.
The async function
automatically knows what to do if you await
a
Promise—it will pause the function (just like with generators) until
the Promise resolves. We didn’t illustrate it in this snippet, but
calling an async function like main()
automatically returns a promise
that’s resolved whenever the function finishes completely.
Tip
The async
/ await
syntax should look very familiar to
readers with experience in C#, because it’s basically identical.
The proposal essentially codifies support for the pattern we’ve already derived, into a syntactic mechanism: combining Promises with sync-looking flow control code. That’s the best of both worlds combined, to effectively address practically all of the major concerns we outlined with callbacks.
The mere fact that such a ES7-ish proposal already exists and has early support and enthusiasm is a major vote of confidence in the future importance of this async pattern.
Promise Concurrency in Generators
So far, all we’ve demonstrated is a single-step async flow with Promises + generators. But real-world code will often have many async steps.
If you’re not careful, the sync-looking style of generators may lull you into complacency with how you structure your async concurrency, leading to suboptimal performance patterns. So we want to spend a little time exploring the options.
Imagine a scenario where you need to fetch data from two different sources, then combine those responses to make a third request, and finally print out the last response. We explored a similar scenario with Promises in Chapter 3, but let’s reconsider it in the context of generators.
Your first instinct might be something like:
function
*
foo
()
{
var
r1
=
yield
request
(
"http://some.url.1"
);
var
r2
=
yield
request
(
"http://some.url.2"
);
var
r3
=
yield
request
(
"http://some.url.3/?v="
+
r1
+
","
+
r2
);
console
.
log
(
r3
);
}
// use previously defined `run(..)` utility
run
(
foo
);
This code will work, but in the specifics of our scenario, it’s not optimal. Can you spot why?
Because the r1
and r2
requests can—and for performance reasons,
should—run concurrently, but in this code they will run
sequentially; the "http://some.url.2"
URL isn’t Ajax fetched until
after the "http://some.url.1"
request is finished. These two requests are
independent, so the better performance approach would likely be to have
them run at the same time.
But how exactly would you do that with a generator and yield
? We know
that yield
is only a single pause point in the code, so you can’t
really do two pauses at the same time.
The most natural and effective answer is to base the async flow on Promises, specifically on their capability to manage state in a time-independent fashion (see “Future Value” in Chapter 3).
The simplest approach:
function
*
foo
()
{
// make both requests "in parallel"
var
p1
=
request
(
"http://some.url.1"
);
var
p2
=
request
(
"http://some.url.2"
);
// wait until both promises resolve
var
r1
=
yield
p1
;
var
r2
=
yield
p2
;
var
r3
=
yield
request
(
"http://some.url.3/?v="
+
r1
+
","
+
r2
);
console
.
log
(
r3
);
}
// use previously defined `run(..)` utility
run
(
foo
);
Why is this different from the previous snippet? Look at where the
yield
is and is not. p1
and p2
are promises for Ajax requests made
concurrently (aka “in parallel”). It doesn’t matter which one finishes
first, because promises will hold onto their resolved state for as long
as necessary.
Then we use two subsequent yield
statements to wait for and retrieve
the resolutions from the promises (into r1
and r2
, respectively). If
p1
resolves first, the yield p1
resumes first then waits on the
yield p2
to resume. If p2
resolves first, it will just patiently
hold onto that resolution value until asked, but the yield p1
will
hold on first, until p1
resolves.
Either way, both p1
and p2
will run concurrently, and both have to
finish, in either order, before the r3 = yield request..
Ajax request
will be made.
If that flow control processing model sounds familiar, it’s basically
the same as what we identified in Chapter 3 as the gate pattern,
enabled by the Promise.all([ .. ])
utility. So, we could also express
the flow control like this:
function
*
foo
()
{
// make both requests "in parallel," and
// wait until both promises resolve
var
results
=
yield
Promise
.
all
(
[
request
(
"http://some.url.1"
),
request
(
"http://some.url.2"
)
]
);
var
r1
=
results
[
0
];
var
r2
=
results
[
1
];
var
r3
=
yield
request
(
"http://some.url.3/?v="
+
r1
+
","
+
r2
);
console
.
log
(
r3
);
}
// use previously defined `run(..)` utility
run
(
foo
);
Note
As we discussed in Chapter 3, we can even use ES6 destructuring
assignment to simplify the var r1 = .. var r2 = ..
assignments, with
var [r1,r2] = results
.
In other words, all of the concurrency capabilities of Promises are available to us in the generator + Promise approach. So in any place where you need more than sequential this-then-that async flow control steps, Promises are likely your best bet.
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 this 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 a 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 subinstance 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!
Generator Concurrency
As we discussed in both Chapter 1 and earlier in this chapter, two simultaneously running “processes” can cooperatively interleave their operations, and many times this can yield (pun intended) very powerful asynchrony expressions.
Frankly, our earlier examples of concurrency interleaving of multiple generators showed how to make it really confusing. But we hinted that there’s places where this capability is quite useful.
Recall a scenario we looked at in Chapter 1, where two different
simultaneous Ajax response handlers needed to coordinate with each other
to make sure that the data communication was not a race condition. We
slotted the responses into the res
array like this:
function
response
(
data
)
{
if
(
data
.
url
==
"http://some.url.1"
)
{
res
[
0
]
=
data
;
}
else
if
(
data
.
url
==
"http://some.url.2"
)
{
res
[
1
]
=
data
;
}
}
But how can we use multiple generators concurrently for this scenario?
// `request(..)` is a Promise-aware Ajax utility
var
res
=
[];
function
*
reqData
(
url
)
{
res
.
push
(
yield
request
(
url
)
);
}
Note
We’re going to use two instances of the *reqData(..)
generator
here, but there’s no difference to running a single instance of two
different generators; both approaches are reasoned about identically.
We’ll see two different generators coordinating in just a bit.
Instead of having to manually sort out res[0]
and res[1]
assignments, we’ll use coordinated ordering so that res.push(..)
properly slots the values in the expected and predictable order. The
expressed logic thus should feel a bit cleaner.
But how will we actually orchestrate this interaction? First, let’s just do it manually, with Promises:
var
it1
=
reqData
(
"http://some.url.1"
);
var
it2
=
reqData
(
"http://some.url.2"
);
var
p1
=
it1
.
next
();
var
p2
=
it2
.
next
();
p1
.
then
(
function
(
data
){
it1
.
next
(
data
);
return
p2
;
}
)
.
then
(
function
(
data
){
it2
.
next
(
data
);
}
);
*reqData(..)
’s two instances are both started to make their Ajax
requests, then paused with yield
. Then we choose to resume the first
instance when p1
resolves, and then p2
’s resolution will restart the
second instance. In this way, we use Promise orchestration to ensure
that res[0]
will have the first response and res[1]
will have the
second response.
But frankly, this is awfully manual, and it doesn’t really let the generators orchestrate themselves, which is where the true power can lie. Let’s try it a different way:
// `request(..)` is a Promise-aware Ajax utility
var
res
=
[];
function
*
reqData
(
url
)
{
var
data
=
yield
request
(
url
);
// transfer control
yield
;
res
.
push
(
data
);
}
var
it1
=
reqData
(
"http://some.url.1"
);
var
it2
=
reqData
(
"http://some.url.2"
);
var
p1
=
it
.
next
();
var
p2
=
it
.
next
();
p1
.
then
(
function
(
data
){
it1
.
next
(
data
);
}
);
p2
.
then
(
function
(
data
){
it2
.
next
(
data
);
}
);
Promise
.
all
(
[
p1
,
p2
]
)
.
then
(
function
(){
it1
.
next
();
it2
.
next
();
}
);
OK, this is a bit better (though still manual!), because now the two
instances of *reqData(..)
run truly concurrently, and (at least for
the first part) independently.
In the previous snippet, the second instance was not given its data
until after the first instance was totally finished. But here, both
instances receive their data as soon as their respective responses come
back, and then each instance does another yield
for control transfer
purposes. We then choose what order to resume them in the
Promise.all([ .. ])
handler.
What may not be as obvious is that this approach hints at an easier form
for a reusable utility, because of the symmetry. We can do even better.
Let’s imagine using a utility called runAll(..)
:
// `request(..)` is a Promise-aware Ajax utility
var
res
=
[];
runAll
(
function
*
(){
var
p1
=
request
(
"http://some.url.1"
);
// transfer control
yield
;
res
.
push
(
yield
p1
);
},
function
*
(){
var
p2
=
request
(
"http://some.url.2"
);
// transfer control
yield
;
res
.
push
(
yield
p2
);
}
);
Note
We’re not including a code listing for runAll(..)
as it is not
only long enough to bog down the text, but is an extension of the logic
we’ve already implemented in run(..)
earlier. So, as a good
supplementary exercise for the reader, try your hand at evolving the
code from run(..)
to work like the imagined runAll(..)
. Also, my
asynquence library provides a previously mentioned runner(..)
utility with this kind of capability already built in, and will be
discussed in Appendix A of this book.
Here’s how the processing inside runAll(..)
would operate:
-
The first generator gets a promise for the first Ajax response from
"http://some.url.1"
, thenyield
s control back to therunAll(..)
utility. -
The second generator runs and does the same for
"http://some.url.2"
,yield
ing control back to therunAll(..)
utility. -
The first generator resumes, and then
yield
s out its promisep1
. TherunAll(..)
utility does the same in this case as our previousrun(..)
, in that it waits on that promise to resolve, then resumes the same generator (no control transfer!). Whenp1
resolves,runAll(..)
resumes the first generator again with that resolution value, and thenres[0]
is given its value. When the first generator then finishes, that’s an implicit transfer of control. -
The second generator resumes,
yield
s out its promisep2
, and waits for it to resolve. Once it does,runAll(..)
resumes the second generator with that value, andres[1]
is set.
In this running example, we use an outer variable called res
to store
the results of the two different Ajax responses—our
concurrency coordination makes that possible.
But it might be quite helpful to further extend runAll(..)
to provide
an inner variable space for the multiple generator instances to share,
such as an empty object we’ll call data
below. Also, it could take
non-Promise values that are yield
ed and hand them off to the next
generator.
Consider:
// `request(..)` is a Promise-aware Ajax utility
runAll
(
function
*
(
data
){
data
.
res
=
[];
// transfer control (and message pass)
var
url1
=
yield
"http://some.url.2"
;
var
p1
=
request
(
url1
);
// "http://some.url.1"
// transfer control
yield
;
data
.
res
.
push
(
yield
p1
);
},
function
*
(
data
){
// transfer control (and message pass)
var
url2
=
yield
"http://some.url.1"
;
var
p2
=
request
(
url2
);
// "http://some.url.2"
// transfer control
yield
;
data
.
res
.
push
(
yield
p2
);
}
);
In this formulation, the two generators are not just coordinating
control transfer, but actually communicating with each other, both
through data.res
and the yield
ed messages that trade url1
and
url2
values. That’s incredibly powerful!
Such realization also serves as a conceptual base for a more sophisticated asynchrony technique called Communicating Sequential Processes (CSP), which is covered in Appendix B of this book.
Thunks
So far, we’ve made the assumption that yield
ing a Promise from a
generator—and having that Promise resume the generator via a helper
utility like run(..)
—was the best possible way to manage asynchrony
with generators. To be clear, it is.
But we skipped over another pattern that has some mildly widespread adoption, so in the interest of completeness we’ll take a brief look at it.
In general computer science, there’s an old pre-JS concept called a thunk. Without getting bogged down in the historical nature, a narrow expression of a thunk in JS is a function that—without any parameters—is wired to call another function.
In other words, you wrap a function definition around function call—with any parameters it needs—to defer the execution of that call, and that wrapping function is a thunk. When you later execute the thunk, you end up calling the original function.
For example:
function
foo
(
x
,
y
)
{
return
x
+
y
;
}
function
fooThunk
()
{
return
foo
(
3
,
4
);
}
// later
console
.
log
(
fooThunk
()
);
// 7
So, a synchronous thunk is pretty straightforward. But what about an async thunk? We can essentially extend the narrow thunk definition to include it receiving a callback.
Consider:
function
foo
(
x
,
y
,
cb
)
{
setTimeout
(
function
(){
cb
(
x
+
y
);
},
1000
);
}
function
fooThunk
(
cb
)
{
foo
(
3
,
4
,
cb
);
}
// later
fooThunk
(
function
(
sum
){
console
.
log
(
sum
);
// 7
}
);
As you can see, fooThunk(..)
expects only a cb(..)
parameter, as it
already has values 3
and 4
(for x
and y
, respectively)
prespecified and ready to pass to foo(..)
. A thunk is just waiting
around patiently for the last piece it needs to do its job: the
callback.
You don’t want to make thunks manually, though. So, let’s invent a utility that does this wrapping for us.
Consider:
function
thunkify
(
fn
)
{
var
args
=
[].
slice
.
call
(
arguments
,
1
);
return
function
(
cb
)
{
args
.
push
(
cb
);
return
fn
.
apply
(
null
,
args
);
};
}
var
fooThunk
=
thunkify
(
foo
,
3
,
4
);
// later
fooThunk
(
function
(
sum
)
{
console
.
log
(
sum
);
// 7
}
);
Tip
Here we assume that the original (foo(..)
) function signature
expects its callback in the last position, with any other parameters
coming before it. This is a pretty ubiquitous standard for async JS
function standards. You might call it “callback-last style.” If for some
reason you had a need to handle “callback-first style” signatures, you
would just make a utility that used args.unshift(..)
instead of
args.push(..)
.
The preceding formulation of thunkify(..)
takes both the foo(..)
function reference, and any parameters it needs, and returns back the
thunk itself (fooThunk(..)
). However, that’s not the typical approach
you’ll find to thunks in JS.
Instead of thunkify(..)
making the thunk itself, typically—if not
perplexingly—the thunkify(..)
utility would produce a function that
produces thunks.
Uhhhh…yeah.
Consider:
function
thunkify
(
fn
)
{
return
function
()
{
var
args
=
[].
slice
.
call
(
arguments
);
return
function
(
cb
)
{
args
.
push
(
cb
);
return
fn
.
apply
(
null
,
args
);
};
};
}
The main difference here is the extra return function() { .. }
layer.
Here’s how its usage differs:
var
whatIsThis
=
thunkify
(
foo
);
var
fooThunk
=
whatIsThis
(
3
,
4
);
// later
fooThunk
(
function
(
sum
)
{
console
.
log
(
sum
);
// 7
}
);
Obviously, the big question this snippet implies is what is whatIsThis
properly called? It’s not the thunk, it’s the thing that will produce
thunks from foo(..)
calls. It’s kind of like a “factory” for “thunks.”
There doesn’t seem to be any kind of standard agreement for naming such
a thing.
So, my proposal is “thunkory” (“thunk” + “factory”). So, thunkify(..)
produces a thunkory, and a thunkory produces thunks. That reasoning is
symmetric to my proposal for “promisory” in Chapter 3:
var
fooThunkory
=
thunkify
(
foo
);
var
fooThunk1
=
fooThunkory
(
3
,
4
);
var
fooThunk2
=
fooThunkory
(
5
,
6
);
// later
fooThunk1
(
function
(
sum
)
{
console
.
log
(
sum
);
// 7
}
);
fooThunk2
(
function
(
sum
)
{
console
.
log
(
sum
);
// 11
}
);
Note
The running foo(..)
example expects a style of callback that’s
not “error-first style.” Of course, “error-first style” is much more
common. If foo(..)
had some sort of legitimate error-producing
expectation, we could change it to expect and use an error-first
callback. None of the subsequent thunkify(..)
machinery cares what
style of callback is assumed. The only difference in usage would be
fooThunk1(function(err,sum){..
.
Exposing the thunkory method—instead of how the earlier
thunkify(..)
hides this intermediary step—may seem like unnecessary
complication. But in general, it’s quite useful to make thunkories at
the beginning of your program to wrap existing API methods, and then be
able to pass around and call those thunkories when you need thunks. The
two distinct steps preserve a cleaner separation of capability.
To illustrate:
// cleaner:
var
fooThunkory
=
thunkify
(
foo
);
var
fooThunk1
=
fooThunkory
(
3
,
4
);
var
fooThunk2
=
fooThunkory
(
5
,
6
);
// instead of:
var
fooThunk1
=
thunkify
(
foo
,
3
,
4
);
var
fooThunk2
=
thunkify
(
foo
,
5
,
6
);
Regardless of whether you like to deal with the thunkories explicitly, the usage of thunks fooThunk1(..)
and fooThunk2(..)
remains the
same.
s/promise/thunk/
So what’s all this thunk stuff have to do with generators?
Comparing thunks to promises generally: they’re not directly interchangable as they’re not equivalent in behavior. Promises are vastly more capable and trustable than bare thunks.
But in another sense, they both can be seen as a request for a value, which may be async in its answering.
Recall from Chapter 3 that we defined a utility for promisifying a function,
which we called Promise.wrap(..)
—we could have called it
promisify(..)
, too! This Promise-wrapping utility doesn’t produce
Promises; it produces promisories that in turn produce Promises. This
is completely symmetric to the thunkories and thunks presently being
discussed.
To illustrate the symmetry, let’s first alter the running foo(..)
example from earlier to assume an “error-first style” callback:
function
foo
(
x
,
y
,
cb
)
{
setTimeout
(
function
(){
// assume `cb(..)` as "error-first style"
cb
(
null
,
x
+
y
);
},
1000
);
}
Now, we’ll compare using thunkify(..)
and promisify(..)
(aka
Promise.wrap(..)
from Chapter 3):
// symmetrical: constructing the question asker
var
fooThunkory
=
thunkify
(
foo
);
var
fooPromisory
=
promisify
(
foo
);
// symmetrical: asking the question
var
fooThunk
=
fooThunkory
(
3
,
4
);
var
fooPromise
=
fooPromisory
(
3
,
4
);
// get the thunk answer
fooThunk
(
function
(
err
,
sum
){
if
(
err
)
{
console
.
error
(
err
);
}
else
{
console
.
log
(
sum
);
// 7
}
}
);
// get the promise answer
fooPromise
.
then
(
function
(
sum
){
console
.
log
(
sum
);
// 7
},
function
(
err
){
console
.
error
(
err
);
}
);
Both the thunkory and the promisory are essentially asking a question
(for a value), and respectively the thunk fooThunk
and promise
fooPromise
represent the future answers to that question. Presented in
that light, the symmetry is clear.
With that perspective in mind, we can see that generators which yield
Promises for asynchrony could instead yield
thunks for asynchrony. All
we’d need is a smarter run(..)
utility (like from before) that can not
only look for and wire up to a yield
ed Promise but also to provide a
callback to a yield
ed thunk.
Consider:
function
*
foo
()
{
var
val
=
yield
request
(
"http://some.url.1"
);
console
.
log
(
val
);
}
run
(
foo
);
In this example, request(..)
could either be a promisory that returns
a promise, or a thunkory that returns a thunk. From the perspective of
what’s going on inside the generator code logic, we don’t care about
that implementation detail, which is quite powerful!
So, request(..)
could be either:
// promisory `request(..)` (see Chapter 3)
var
request
=
Promise
.
wrap
(
ajax
);
// vs.
// thunkory `request(..)`
var
request
=
thunkify
(
ajax
);
Finally, as a thunk-aware patch to our earlier run(..)
utility, we
would need logic like this:
// ..
// did we receive a thunk back?
else
if
(
typeof
next
.
value
==
"function"
)
{
return
new
Promise
(
function
(
resolve
,
reject
){
// call the thunk with an error-first callback
next
.
value
(
function
(
err
,
msg
)
{
if
(
err
)
{
reject
(
err
);
}
else
{
resolve
(
msg
);
}
}
);
}
)
.
then
(
handleNext
,
function
handleErr
(
err
)
{
return
Promise
.
resolve
(
it
.
throw
(
err
)
)
.
then
(
handleResult
);
}
);
}
Now, our generators can either call promisories to yield
Promises, or
call thunkories to yield
thunks, and in either case, run(..)
would
handle that value and use it to wait for the completion to resume the
generator.
Symmetry-wise, these two approaches look identical. However, we should point out that’s true only from the perspective of Promises or thunks representing the future value continuation of a generator.
From the larger perspective, thunks do not in and of themselves have hardly any of the trustability or composability guarantees that Promises are designed with. Using a thunk as a stand-in for a Promise in this particular generator asynchrony pattern is workable but should be seen as less than ideal when compared to all the benefits that Promises offer (see Chapter 3).
If you have the option, use yield pr
rather than yield th
. But
there’s nothing wrong with having a run(..)
utility which can handle
both value types.
Note
The runner(..)
utility in my asynquence library, which is discussed in Appendix A, handles yield
s of Promises, thunks and
asynquence sequences.
Pre-ES6 Generators
You’re hopefully convinced now that generators are a very important addition to the async programming toolbox. But it’s a new syntax in ES6, which means you can’t just polyfill generators like you can Promises (which are just a new API). So what can we do to bring generators to our browser JS if we don’t have the luxury of ignoring pre-ES6 browsers?
For all new syntax extensions in ES6, there are tools—the most common term for them is transpilers, for trans-compilers—which can take your ES6 syntax and transform it into equivalent (but obviously uglier!) pre-ES6 code. So, generators can be transpiled into code that will have the same behavior but work in ES5 and below.
But how? The “magic” of yield
doesn’t obviously sound like code that’s
easy to transpile. We actually hinted at a solution in our earlier
discussion of closure-based iterators.
Manual Transformation
Before we discuss the transpilers, let’s derive how manual transpilation would work in the case of generators. This isn’t just an academic exercise, because doing so will actually help further reinforce how they work.
Consider:
// `request(..)` is a Promise-aware Ajax utility
function
*
foo
(
url
)
{
try
{
console
.
log
(
"requesting:"
,
url
);
var
val
=
yield
request
(
url
);
console
.
log
(
val
);
}
catch
(
err
)
{
console
.
log
(
"Oops:"
,
err
);
return
false
;
}
}
var
it
=
foo
(
"http://some.url.1"
);
The first thing to observe is that we’ll still need a normal foo()
function that can be called, and it will still need to return an
iterator. So, let’s sketch out the non-generator transformation:
function
foo
(
url
)
{
// ..
// make and return an iterator
return
{
next
:
function
(
v
)
{
// ..
},
throw
:
function
(
e
)
{
// ..
}
};
}
var
it
=
foo
(
"http://some.url.1"
);
The next thing to observe is that a generator does its “magic” by suspending its scope/state, but we can emulate that with function closure (see the Scope & Closures title of this series). To understand how to write such code, we’ll first annotate different parts of our generator with state values:
// `request(..)` is a Promise-aware Ajax utility
function
*
foo
(
url
)
{
// STATE 1
try
{
console
.
log
(
"requesting:"
,
url
);
var
TMP1
=
request
(
url
);
// STATE 2
var
val
=
yield
TMP1
;
console
.
log
(
val
);
}
catch
(
err
)
{
// STATE 3
console
.
log
(
"Oops:"
,
err
);
return
false
;
}
}
Note
For more accurate illustration, we split up the
val = yield request..
statement into two parts, using the temporary
TMP1
variable. request(..)
happens in state 1
, and the
assignment of its completion value to val
happens in state 2
.
We’ll get rid of that intermediate TMP1
when we convert the code to
its non-generator equivalent.
In other words, 1
is the beginning state, 2
is the state if the
request(..)
succeeds, and 3
is the state if the request(..)
fails. You can probably imagine how any extra yield
steps would just
be encoded as extra states.
Going back to our transpiled generator, let’s define a variable state
in the
closure we can use to keep track of the state:
function
foo
(
url
)
{
// manage generator state
var
state
;
// ..
}
Now, let’s define an inner function called process(..)
inside the
closure which handles each state, using a switch
statement:
// `request(..)` is a Promise-aware Ajax utility
function
foo
(
url
)
{
// manage generator state
var
state
;
// generator-wide variable declarations
var
val
;
function
process
(
v
)
{
switch
(
state
)
{
case
1
:
console
.
log
(
"requesting:"
,
url
);
return
request
(
url
);
case
2
:
val
=
v
;
console
.
log
(
val
);
return
;
case
3
:
var
err
=
v
;
console
.
log
(
"Oops:"
,
err
);
return
false
;
}
}
// ..
}
Each state in our generator is represented by its own case
in the
switch
statement. process(..)
will be called each time we need to
process a new state. We’ll come back to how that works in just a moment.
For any generator-wide variable declarations (val
), we move those to a
var
declaration outside of process(..)
so they can survive multiple
calls to process(..)
. But the block scoped err
variable is only
needed for the 3
state, so we leave it in place.
In state 1
, instead of yield resolve(..)
, we did
return resolve(..)
. In terminal state 2
, there was no explicit
return
, so we just do a return;
which is the same as
return undefined
. In terminal state 3
, there was a return false
,
so we preserve that.
Now we need to define the code in the iterator functions so they call
process(..)
appropriately:
function
foo
(
url
)
{
// manage generator state
var
state
;
// generator-wide variable declarations
var
val
;
function
process
(
v
)
{
switch
(
state
)
{
case
1
:
console
.
log
(
"requesting:"
,
url
);
return
request
(
url
);
case
2
:
val
=
v
;
console
.
log
(
val
);
return
;
case
3
:
var
err
=
v
;
console
.
log
(
"Oops:"
,
err
);
return
false
;
}
}
// make and return an iterator
return
{
next
:
function
(
v
)
{
// initial state
if
(
!
state
)
{
state
=
1
;
return
{
done
:
false
,
value
:
process
()
};
}
// yield resumed successfully
else
if
(
state
==
1
)
{
state
=
2
;
return
{
done
:
true
,
value
:
process
(
v
)
};
}
// generator already completed
else
{
return
{
done
:
true
,
value
:
undefined
};
}
},
"throw"
:
function
(
e
)
{
// the only explicit error handling is in
// state 1
if
(
state
==
1
)
{
state
=
3
;
return
{
done
:
true
,
value
:
process
(
e
)
};
}
// otherwise, an error won't be handled,
// so just throw it right back out
else
{
throw
e
;
}
}
};
}
How does this code work?
-
The first call to the iterator’s
next()
call would move the generator from the unitialized state to state1
, and then callprocess()
to handle that state. The return value fromrequest(..)
, which is the promise for the Ajax response, is returned back as thevalue
property from thenext()
call. -
If the Ajax request succeeds, the second call to
next(..)
should send in the Ajax response value, which moves our state to2
.process(..)
is again called (this time with the passed in Ajax response value), and thevalue
property returned fromnext(..)
will beundefined
. -
However, if the Ajax request fails,
throw(..)
should be called with the error, which would move the state from1
to3
(instead of2
). Againprocess(..)
is called, this time with the error value. Thatcase
returnsfalse
, which is set as thevalue
property returned from thethrow(..)
call.
From the outside—that is, interacting only with the iterator—this foo(..)
normal function works pretty much the same as the
*foo(..)
generator would have worked. So we’ve effectively
transpiled our ES6 generator to pre-ES6 compatibility!
We could then manually instantiate our generator and control its
iterator—calling var it = foo("..")
and it.next(..)
and such—or better, we could pass it to our previously defined run(..)
utility
as run(foo,"..")
.
Automatic Transpilation
The preceding exercise of manually deriving a transformation of our ES6 generator to pre-ES6 equivalent teaches us how generators work conceptually. But that transformation was really intricate and very non-portable to other generators in our code. It would be quite impractical to do this work by hand, and would completely obviate all the benefit of generators.
But luckily, several tools already exist that can automatically convert ES6 generators to things like what we derived in the previous section. Not only do they do the heavy lifting work for us, but they also handle several complications that we glossed over.
One such tool is regenerator, from the smart folks at Facebook.
If we use regenerator to transpile our previous generator, here’s the code produced (at the time of this writing):
// `request(..)` is a Promise-aware Ajax utility
var
foo
=
regeneratorRuntime
.
mark
(
function
foo
(
url
)
{
var
val
;
return
regeneratorRuntime
.
wrap
(
function
foo$
(
context$1$0
)
{
while
(
1
)
switch
(
context$1$0
.
prev
=
context$1$0
.
next
)
{
case
0
:
context$1$0
.
prev
=
0
;
console
.
log
(
"requesting:"
,
url
);
context$1$0
.
next
=
4
;
return
request
(
url
);
case
4
:
val
=
context$1$0
.
sent
;
console
.
log
(
val
);
context$1$0
.
next
=
12
;
break
;
case
8
:
context$1$0
.
prev
=
8
;
context$1$0
.
t0
=
context$1$0
.
catch
(
0
);
console
.
log
(
"Oops:"
,
context$1$0
.
t0
);
return
context$1$0
.
abrupt
(
"return"
,
false
);
case
12
:
case
"end"
:
return
context$1$0
.
stop
();
}
},
foo
,
this
,
[[
0
,
8
]]);
});
There’s some obvious similarities here to our manual derivation, such as
the switch
/ case
statements, and we even see val
pulled out of
the closure just as we did.
Of course, one trade-off is that regenerator’s transpilation requires a
helper library regeneratorRuntime
that holds all the reusable logic
for managing a general generator/iterator. A lot of that boilerplate
looks different than our version, but even then, the concepts can be
seen, like with context$1$0.next = 4
keeping track of the next state
for the generator.
The main takeaway is that generators are not restricted to only being useful in ES6+ environments. Once you understand the concepts, you can employ them throughout your code, and use tools to transform the code to be compatible with older environments.
This is more work than just using a Promise
API polyfill for pre-ES6
Promises, but the effort is totally worth it, because generators are so
much better at expressing async flow control in a reason-able, sensible,
synchronous-looking, sequential fashion.
Once you get hooked on generators, you’ll never want to go back to the hell of async spaghetti callbacks!
Review
Generators are a new ES6 function type that does not run-to-completion like normal functions. Instead, the generator can be paused in mid-completion (entirely preserving its state), and it can later be resumed from where it left off.
This pause/resume interchange is cooperative rather than preemptive,
which means that the generator has the sole capability to pause itself,
using the yield
keyword, and yet the iterator that controls the
generator has the sole capability (via next(..)
) to resume the
generator.
The yield
/ next(..)
duality is not just a control mechanism, it’s
actually a two-way message passing mechanism. A yield ..
expression
essentially pauses waiting for a value, and the next next(..)
call
passes a value (or implicit undefined
) back to that paused yield
expression.
The key benefit of generators related to async flow control is that the
code inside a generator expresses a sequence of steps for the task in a
naturally sync/sequential fashion. The trick is that we essentially hide
potential asynchrony behind the yield
keyword—moving the asynchrony
to the code where the generator’s iterator is controlled.
In other words, generators preserve a sequential, synchronous, blocking code pattern for async code, which lets our brains reason about the code much more naturally, addressing one of the two key drawbacks of callback-based async.
Get You Don't Know JS: Async & Performance now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.