ES6 Generators for the Nosy
JavaScript ES6 adds a new construct called a generator. They appear function-like, but don't be fooled, there are a lot of differences. In fact, the more you know the less like a "function" they seem.
Notes
- Links prefaced by the "experiment" icon indicate "less frequently asked questions" and are covered in the Appendix.
- In the "printouts", in many cases newlines have been replaced by spaces for brevity.
Syntax
Declaration
The declaration line for a generator has a *, which can be either
1) function *foo() { ... }
2) function* foo() { ... }
3) function * foo() { ... }
4) function*foo() { ... }
Kyle Simpson and others make a good case to version 1 over version 2, so I'll use that. Versions 3 and 4 are just silly.
yield
Within a generator, ( how about in a normal function?) one can use a new keyword, yield
. This allows for two way communication (more later). A "typical" syntax is:
var incoming = yield outgoing;
As in a return
statement, outgoing is optional, but for a generator it really should be present almost all the time.
The left side, var incoming
, is truly optional and will often be absent.
IMO, it's unfortunate that yield gets used for two purposes: outgoing and incoming communication. I would have preferred something like
yield outgoing
var incoming = resume
or even
return outgoing
var incoming = resume
which might costs one more reserved word but provide a ton of clarity. But the ES6 usage of yield
is consistent with several other languages. "Clever" programmers can actually make the yield line very messy with more operations. IMO it's confusing enough already, and the operator precedence is wonky, so don't do that!
( Can I use return
in a generator?)
What the Heck is going on?
Even though *foo()
looks like a function, it isn't. Proof:
function *foo(x) {
console.log('starting foo');
}
var it=foo(0);
Prints nothing. It looks like you are "calling" foo(), but something else is happening. "Calling" a generator does not run the code in the generator. It produces an iterator (a.k.a. "generator iterator") that you then use to control the generator and execute it's code. ( Can I use new foo()
to get the iterator?)
What does yield outgoing
do?
Each "yield" returns the result of one iteration.
What am I iterating?
In the example above, not much. However, a real generator iterates by yielding a result to it.next()
. And resumes the next time you call it.next()
.
Here's a minimal generator, returning the value you passed in:
function *foo(x) {
yield x;
}
var it=foo('42');
console.dir(it.next());
console.dir(it.next());
The results are:
{ value: '42', done: false }
{ value: undefined, done: true }
Here's a slightly more useful generator, iterating over a range of integers. We use the new-fangled ES6 for of
loop to access the values.
function *foo(min, max) {
while (min < max)
yield min++;
}
for (var t of foo(0, 5))
console.dir(t);
Which prints 0 1 2 3 4
as expected.
Can I "call" another generator?
Yes, including yourself. yield *
(with optional spaces in the 4 versions from above) delegates to another iterable. (do they have to be a generator?) Let's "enhance" our range iterator so it recurs with max-1:
function *range_r(min, max) {
while (min < max)
yield min++;
if (max > 0) // important!
yield *range_r(0, max-1);
}
for (var v of range_r(0, 4))
console.log(v);
prints: 0 1 2 3 0 1 2 0 1 0
If you forget that if (max > 0) // important!
line, it will print the same results, because it is no longer yielding anything to print, but generators cannot defy laws of programming: the code still goes into an infinite loop and a stack overflow.
What funky stuff can I do?
You can provide an infinite iterator. For example, for a lot of squares starting at x:
function *foo(x) {
while (true) {
yield x*x;
x++;
}
}
This means that the caller has to know when to stop:
var it=foo(2);
var t = it.next();
while (t.value<=25) { // stop myself
console.dir(t);
t = it.next();
}
result :
{ value: 4, done: false }
{ value: 9, done: false }
{ value: 16, done: false }
{ value: 25, done: false }
In theory one can also stop the iterator by calling return()
but that didn't work for me.
What about passing data back to the Generator?
Here's a variation on the above, where one can (optionally) pass back in x.
function *foo(x) {
while (true) {
var newX = yield x*x;
x = newX || x; // switch x to y if one was passed in
x++;
}
}
var it=foo(2);
console.dir(it.next());
console.dir(it.next());
console.dir(it.next(5));
result:
{ value: 4, done: false }
{ value: 9, done: false }
{ value: 36, done: false }
All these examples are standalone functions. Can I use them in an object or class?
Generators within a Javascript object
The version 1 syntax works great with the new ES6 concise methods. If you prefer, you can still use old fashioned function syntax - note the placement of the "*".
var foo = {
max : 5,
*generator() { // new concise syntax
var x = this.max;
while (x > 0)
yield x--;
},
traditional: function*() {
yield *this.generator();
}
};
foo.max = 3;
for (var v of foo.generator())
console.log(v);
for (var v of foo.traditional())
console.log(v);
prints 3 2 1
and 3 2 1
.
Generators within a pre-ES6 "class"
Again note the placement of the "*".
function Foo(x) {
this.x = x;
}
Foo.prototype.generator = function*(){
var x = this.x;
while (--x > 0)
yield x;
};
Foo.prototype.anotherGenerator = function*() {
yield *this.generator();
};
var foo = new Foo(5);
for (var v of foo.generator())
console.log(v);
for (var v of foo.anotherGenerator())
console.log(v);
prints 4 3 2 1
then 4 3 2 1
.
Are Generators Useful?
The examples here range from useless to very slightly useful. Surely they didn't add all this syntax just to do ranges of squares?
Generators are powerful tools and I'll discuss more uses in a later post. In the meantime, to the laboratory!
Appendix: Experiments
Can I use `yield` in a normal function?
No.
function notAGenerator() {
yield 42;
}
var z = notAGenerator();
Will complain:
morgan@morgan-UL80Jt ~/Work/ES6 $ node test1.js
/home/morgan/Work/ES6/test1.js:13
yield 42;
^^
SyntaxError: Unexpected number
Can I use `return` in a generator?
Yes, but...
function *foo(x) {
yield x;
return -1;
}
var it = foo(4);
console.dir(it.next());
console.dir(it.next());
prints:
{ value: 4, done: false }
{ value: -1, done: true }
but, return
doesn't work with for..of
loops!
for(var v of foo(4))
console.dir(v);
Only prints "4". Use return with extreme caution!
Can I use `new` to create the generator iterator?
Some blogs that imply that you can't but it works for me:
function *foo(x) {
yield x;
}
var it = new foo(4);
console.dir(it.next());
console.dir(it.next());
prints what you expect:
{ value: 4, done: false }
{ value: undefined, done: true }
Can I yield * to a non-generator?
Yes, you can delegate to any iterable, such as an array, string, or Map. (But not object or null!)
function *array_string_map(array, string, map) {
yield * array;
yield * string;
yield * map;
}
for (var v of array_string(
[1,2,3,4],
'abcd',
new Map([ [{id: 'foo'}, 'bar'] ])
))
console.log(v);
generates:
1 2 3 4 a b c d [ { id: 'foo' }, 'bar' ]