A generator is a combination of two things - an Iterator
and an Observer
.
An iterator is something when invoked returns an iterable
. An iterable
is something you can iterate upon. From ES6/ES2015 onwards, all collections (Array, Map, Set, WeakMap, WeakSet) conform to the Iterable contract.
A generator(iterator) is a producer. In iteration the consumer
PULL
s the value from the producer.
Example:
function *gen() { yield 5; yield 6; }
let a = gen();
Whenever you call a.next()
, you're essentially pull
-ing value from the Iterator and pause
the execution at yield
. The next time you call a.next()
, the execution resumes from the previously paused state.
A generator is also an observer using which you can send some values back into the generator.
function *gen() {
document.write('<br>observer:', yield 1);
}
var a = gen();
var i = a.next();
while(!i.done) {
document.write('<br>iterator:', i.value);
i = a.next(100);
}
Here you can see that yield 1
is used like an expression which evaluates to some value. The value it evaluates to is the value sent as an argument to the a.next
function call.
So, for the first time i.value
will be the first value yielded (1
), and when continuing the iteration to the next state, we send a value back to the generator using a.next(100)
.
Generators are widely used with spawn
(from taskJS or co) function, where the function takes in a generator and allows us to write asynchronous code in a synchronous fashion. This does NOT mean that async code is converted to sync code / executed synchronously. It means that we can write code that looks like sync
but internally it is still async
.
Sync is BLOCKING; Async is WAITING. Writing code that blocks is easy. When PULLing, value appears in the assignment position. When PUSHing, value appears in the argument position of the callback.
When you use iterators, you PULL
the value from the producer. When you use callbacks, the producer PUSH
es the value to the argument position of the callback.
var i = a.next() // PULL
dosomething(..., v => {...}) // PUSH
Here, you pull the value from a.next()
and in the second, v => {...}
is the callback and a value is PUSH
ed into the argument position v
of the callback function.
Using this pull-push mechanism, we can write async programming like this,
let delay = t => new Promise(r => setTimeout(r, t));
spawn(function*() {
// wait for 100 ms and send 1
let x = yield delay(100).then(() => 1);
console.log(x); // 1
// wait for 100 ms and send 2
let y = yield delay(100).then(() => 2);
console.log(y); // 2
});
So, looking at the above code, we are writing async code that looks like it's blocking
(the yield statements wait for 100ms and then continue execution), but it's actually waiting
. The pause
and resume
property of generator allows us to do this amazing trick.
The spawn function uses yield promise
to PULL the promise state from the generator, waits till the promise is resolved, and PUSHes the resolved value back to the generator so it can consume it.
So, with generators and spawn function, you can clean up all your async code in NodeJS to look and feel like it's synchronous. This will make debugging easy. Also the code will look neat.
This feature is coming to future versions of JavaScript - as async...await
. But you can use them today in ES2015/ES6 using the spawn function defined in the libraries - taskjs, co, or bluebird