Generators are coming to nodejs via ES6 promising to simplify async code
Node 0.11
Some features of JavaScript ES6 (Harmony) are available right now under nodejs 0.10.24. To see all available Harmony features, run:
:::sh
$ node --v8-options | grep harmony
--harmony_typeof (enable harmony semantics for typeof)
--harmony_scoping (enable harmony block scoping)
--harmony_modules (enable harmony modules (implies block scoping))
--harmony_proxies (enable harmony proxies)
--harmony_collections (enable harmony collections (sets, maps, and weak maps))
--harmony (enable all harmony features (except typeof))
I prefer running with all Harmony features turned on node --harmony
Generators are NOT available in node 0.10, so you need to run latest unstable version of node to get them. The best is to install multiple versions (0.10 and 0.11) in parallel using Node Version Manager
Install nvm using brew
brew install nvm
Load nvm script in your profile file, for example .bash_profile
# Load Node version manager
source $(brew --prefix nvm)/nvm.sh
Install two versions of Node using nvm
nvm install v0.11
nvm install v0.10
nvm ls
Now you can run a particular ES6 source file using
nvm run v0.11.10 --harmony <source file.js>
Simple generator example
Lets create an example that generates n*n
sequence.
In current EcmaScript5 it would be something like this:
function square() {
var k = 0;
return function genNextSquare() {
return (++k)*k;
};
}
var squares = square();
console.log(squares());
console.log(squares());
console.log(squares());
Note the use of closure to keep private instance of variable k. Same program using the new Generators feature in ES6:
function *square() {
var k = 0;
while (true) {
yield (++k)*k;
}
}
var squares = square();
console.log(squares.next().value);
console.log(squares.next().value);
console.log(squares.next().value);
Notice the parallels: square
returns a generator function in both cases,
variable k
is private. The difference are less code, calling next
and value
,
and having an infinite loop inside the generator function.
yield
returns a value
Outside code calling next()
can communicate with the generator via yield
return value
function *square() {
var k = 0;
while (true) {
k = yield (++k)*k;
console.log('k =', k);
}
}
var squares = square();
console.log(squares.next(10).value);
// prints 1, k = 20
console.log(squares.next(20).value);
// prints 441
Notice that everytime we called squares.next()
we passed a value that was assigned
to the variable k
, and the generator function continued from yield
statement.
The continuation with outside value (provided by the caller when calling next()
) is
the key insight into how to use the generator functions.
returned value
Value returned from a generator is treated the same way as yielded.
function *square() {
console.log('yielding 1');
yield 1;
console.log('returning 2');
return 2;
}
var squares = square();
function logValues(generator) {
console.log(generator.next());
console.log(generator.next());
}
logValues(squares);
// prints
yielding 1
{ value: 1, done: false }
returning 2
{ value: 2, done: true }
next()
throws an Error if generator has finished
If you try to call next()
on a completed generator, it will
throw an Error
function *square() {
console.log('returning 2');
return 2;
}
var squares = square();
function logValues(generator) {
console.log(generator.next());
console.log(generator.next());
}
logValues(squares);
// prints
returning 2
{ value: 2, done: true }
/Users/gbahmutov/git/training/node/es6/test2/index.js:8
console.log(generator.next());
^
Error: Generator has already finished
Generators and promises
Generator function returns an object with two properties: value
and done
.
The later is boolean flag showing if the generator function has exited, for
example by calling return
. This is the key feature of generators
allowing to simplify ES6 asynchronous code via promises.
An external function can call the generator function repeatedly, until done
is true.
If the generator function returns a promise, then external function can call the generator
again with the resolved value, once the promise is resolved.
This makes the code inside the generator look and behave like synchronous code. Q.spawn is one such implementation. Lets compare code required to call async function and print result using promises vs generators
// common code
var Q = require('q');
function asyncSquare(n) {
var defer = Q.defer();
process.nextTick(function () {
defer.resolve(n*n);
});
return defer.promise;
}
// ES5 using promises
asyncSquare(5)
.then(console.log)
.then(function () {
return asyncSquare(6);
})
.then(console.log)
.done(function () {
console.log('all done');
});
// ES6 using generator that yields promises to Q.spawn
Q.spawn(function *() {
console.log(yield asyncSquare(5));
console.log(yield asyncSquare(6));
console.log('all done');
});
// logical flow
// Q grabs promise, waits until it is resolved, then calls next(25)
// 25 is passed to console.log
// Q grabs promise, waits until it is resolved, then calls next(36)
// 36 is passed to console.log
Same code, but looks much, much cleaner!
Future
For more examples look at Promises and Generators: an utopia slides. For real world example using generators to simplify async code, see Experiments with Koa and JavaScript generators