A second taste of nodejs generators

Another set of examples using generators.

See "part 1: A taste of nodejs generators"

I played with generator functions to learn more about their properties.

Logging intermediate values

Function generators can be used to add logging to functions without tight coupling. For example, the generator can simply yield a value to be processed by the outside caller, and the outside caller might log it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// generator function
function *square() {
yield 1;
yield 2;
yield 4;
yield 9;
}
var squares = square(); // generator
function logValues(generator) {
var v;
while(!(v = generator.next()).done) {
console.log('generated value', v.value);
}
}
// the generator function knows nothing about the logging
logValues(squares);
generated value 1
generated value 2
generated value 4
generated value 9

yield is synchronous

When a generator yields a value, it happens synchronously, you can test this by adding a couple of functions that place other messages on the event queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// node --harmony index.js
function *square() {
yield 1;
yield 2;
yield 4;
yield 9;
}
var squares = square();
process.nextTick(function () {
console.log('trying to print between yield and return');
});
process.nextTick(function () {
console.log('trying to print between yield and return again');
});

function logValues(generator) {
var v;
while(!(v = generator.next()).done) {
console.log('generated value', v.value);
}
}
logValues(squares);
generated value 1
generated value 2
generated value 4
generated value 9
trying to print between yield and return
trying to print between yield and return again

generator and caller work in parallel

You can think of the generator and its caller as executing in turns at the same time. The generator surrenders to the caller using yield and the caller resumes the generator by calling next(). You can see by inserting print statements into both generator and caller functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function *square() {
console.log('yielding 1');
yield 1;
console.log('yielding 2');
yield 2;
console.log('all done');
}
var squares = square();
function logValues(generator) {
do {
console.log('starting generator');
var v = generator.next();
console.log('got back', v);
} while (!v.done);
console.log('caller done');
}
logValues(squares);
starting generator
yielding 1
got back { value: 1, done: false }
starting generator
yielding 2
got back { value: 2, done: false }
starting generator
all done
got back { value: undefined, done: true }
caller done

caller can transform yielded values

yield returns a value, which can be transformed by the caller and sent back to the generator via next() call. For example, lets double the numbers before printing

1
2
3
4
5
6
7
8
9
10
11
12
13
function *square() {
console.log(yield 1);
console.log(yield 2);
console.log(yield 4);
console.log(yield 9);
}
var squares = square();
function doubleValues(generator) {
do {
var v = generator.next(v && v.value * 2);
} while (!v.done);
}
doubleValues(squares);
2
4
8
18

yield without generator

If you try to use yield keyword outside a generator function, Node throws an error

1
2
3
4
function square() {
yield;
}
square();
// prints
ReferenceError: yield is not defined
    at square (/Users/gbahmutov/git/training/node/es6/test2/index.js:2:5)

Broken generator cannot be mended

If the generator throws an Error, it cannot be restarted

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function *square() {
console.log('yielding 1');
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
console.log('all done');
}
var squares = square();
function logValues(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('got back', v);
} catch (err) {
console.log('fixing generator', v);
}

try {
v = generator.next();
console.log('got back', v);
} catch (err) {
console.log('fixing generator', v);
}

try {
v = generator.next();
console.log('got back', v);
} catch (err) {
console.log('fixing generator', v);
}

console.log('caller done');
}
logValues(squares);
starting generator
yielding 1
got back { value: 1, done: false }
throwing an exception
fixing generator { value: 1, done: false }
fixing generator { value: 1, done: false }
caller done

Bonus: co module

See callbacks vs generators by TJ Holowaychuk that shows simplifying callback hell using generators. He recommends his NPM module called co that wraps around promises, callbacks etc yielded from the generator to drastically simplify asynchronous processing. For example, here is a code sample that downloads a page, then downloads all links, then collects all content type fields, all executed asynchronously, but in a very clean function.

1
2
3
4
5
6
7
8
9
function showTypes(fn) {
// co - function from 'co' module
co(function *(){
// get returns a promise / thunk
var res = yield get(‘http://cloudup.com’)
var responses = yield links(res.text).map(get)
return responses.map(header(‘content-type’))
})(fn)
}