In all the discussions about ES6 one thing is bugging me. I'm picking one random comment here from this io.js issue but it's something that comes up over and over again:
There's sentiment from one group that Node should have full support for Promises. While at the same time another group wants generator syntax support (e.g.
var f = yield fs.stat(...)
).
People keep putting generators, callbacks, co, thunks, control flow libraries, and promises into one bucket. If you read that list and you think "well, they are all kind of doing the same thing", then this is to you.
There are three distinct categories that they fall into:
- Models/Abstractions of async behavior
- Control flow management
- Data structures
Generators are category 3. Genators are like arrays. Don't believe me? Here's some code:
function *doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
What does it do? Not much actually. It just creates a list of functions that take a callback. We can even iterate over it:
for (task of doStuff()) {
// task is a function that takes a good ol' callback
}
Written down using boring, ES5 code:
function doStuff() {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}
Ready to get your mind blown? We can use the exact same for-loop snippet to iterate over this.
Now, of course generators aren't just a slightly more verbose array syntax. They allow you to dynamically alter the content of the array based on stuff being passed in or to return lazy (read: infinite) sequences. All this can be done in ES5 already (regenerator is proof of that), but generators do offer a nicer syntax.
But they aren't async. And they don't manage control flow.
Would you say that an array is "control flow management"
because you can pass it into async.series
?
async.series(doStuff());
Then why would you call generators "control flow management"
because you can pass one into co
?
co(doStuff());
Sure, generators are a more powerful "data structure" than arrays. But they are still closer to arrays than they are to promises, caolan/async, or callbacks.
If you want to do something async, you need category 1 (an abstraction of async behavior). To make it nicer, category 2 (control flow management) can be helpful. And more often than not category 2 will require you to use known data structures from category 3. You can pick your poison for category 1 and 2 freely. But you won't be able to replace a promise with a fancier array.
- Models/Abstractions for async behavior: thunks+callbacks, promises
- Control flow management: co, async, spawn/ES7 async functions, Promise.all
- Data structures: arrays, generators, objects/maps
P.S.: I hope this post can usher in an era of JS developers using all sorts of different, slightly weird analogies to explain an often misunderstood language feature. Then we finally got our own little "Monads are like X".
P.P.S: The right choice is obviously Promises + async functions.
Totally agree generators are not, in and of themselves, async. Generators are async-capable, is how I would describe it. But totally disagree that they aren't capable of expressing flow-control -- they absolutely are!
Let me illustrate both points separately:
From the perspective of what's going on inside of
*main()
, the code does in fact express a primitive sort of flow control. In fact, it's impossible to tell from looking only at that code whether it's sync or async, but what we do know is that theyield
provides a hint where asynchronicity can occur. This is flow control. Also, thetry..catch
is flow control, since either synchronously or asynchronously an external influence could change the path of code execution inside*main(..)
.Now, if
foo()
andbaz()
are in fact sync and return immediate values, and the*main(..)
above is run like this:...then of course
*main(..)
is essentially just a synchronous function.But, if you make no changes whatsoever to the internals of
*main()
, and instead fiddle with whatfoo()
andbaz()
do (making them asynchronous, promise-returning), and you run*main()
not with a loop as shown but with a more sophisticated utility (as many promise libs have, likeQ.spawn(..)
, etc), now all of a sudden the same*main(..)
function has become an asynchronously completing function, and theyield
andtry..catch
can kick in to handle the pause/resume/error flow control.yield
andtry..catch
allow the generator to be async-capable, if you so decide. In fact, I think that's the genius of generators, is that they separate the flow control logic (which is expressed in a nice, natural, synchronous way) from the implementation details of how the generator is run to completion (which can either be synchronous or asynchronous). Generators let you think about each of those two pieces independently.When you're thinking about flow control, you think and author code in a naturally sequential sync-looking way. When you want to control the implementation details of how the generator is run to completion, you ignore its actual code and worry only about what mechanisms are used (sync immediate values or async promises) to flow through the flow-control.
Now, there's no question that
yield
andtry..catch
are a limited form of flow control. They are not, in and of themselves, nearly as capable as promises are at flow control.But that's why putting promises together with generators (plus the runner utility) is so great, because it lets you get the best of both worlds. Promises solve all the pesky IoC issues that callbacks-only code suffers from, AND they have more expressive flow control, such as abstractions like
Promise.race(..)
orPromise.all(..)
. Generators provide a capability to separate out the flow control expression from the implementation of the completion.For example:
That "magical" combination where promises improve the flow-control expressivity of generators... is, I think, why we already see the ES7
async
/await
on such a solid track even before ES6 is fully ratified. We've already realized that both promises and generators offer parts of the "solution to JS async coding", and putting them together is our best path forward.