Generators and Suspend

31 May 2013 | By Alex Young | Tags node modules es6

ECMAScript 6 generators are at the draft stage, and available in Node 0.11 when node is run with --harmony or --harmony-generators. Generators are “first-class coroutines” – think functions that can be postponed and resumed.

Generators are denoted with function*, and return values by calling yield. The value isn’t really returned: yield could be placed inside a loop, and then generator.next() is called to fetch the yielded value. The generator is said to be an iterator – it could be provided as the expression to an iteration statement like for:

function* generator() {
  for (;;) {
    yield someValue;
  }
}

for (var value of generator()) {
  // Do something with `value`,
  // then `break` when enough values have been yielded
}

The ECMAScript 6 wiki has a Fibonacci sequence example, but generators don’t really hit their conceptual stride until you start hooking generators up to other generators. The classic example of this is consumer-producer relationships: generators that produce values, and then consumers that use them. The two generators are said to be symmetric – a continuous evaluation where coroutines yield to each other, rather than two functions that call each other.

Jeremy Martin sent in a small but novel module based on generators called suspend (GitHub: jmar777 / suspend, License: MIT, npm: suspend). As it needs Node 0.11 and for Node to be run with --harmony, let’s just say it’s academically interesting for now.

You can think of suspend as an early example of generators that feature an idiomatic Node API:

// async without suspend
async.map(['file1','file2','file3'], fs.stat, function(err, results) {
  // results is now an array of stats for each file
});

// async with suspend
var res = yield async.map(['file1','file2','file3'], fs.stat, resume);

Here the async module has been modified to use suspend, resulting in more concise code.

suspend is “red light, green light” for asynchronous code execution. yield means stop, and resume means go.

If this sounds familiar, that’s because it’s not semantically too different to node-fibers. The node-fibers documentation includes a comparison between the ES6 generators example and its own syntax.

This is the entire source to suspend:

var suspend = module.exports = function suspend(generator, opts) {
  opts || (opts = {});

  return function start() {
    Array.prototype.unshift.call(arguments, function resume(err) {
      if (opts.throw) {
        if (err) return iterator.throw(err);
        iterator.send(Array.prototype.slice.call(arguments, 1));
      } else {
        iterator.send(Array.prototype.slice.call(arguments));
      }
    });
    var iterator = generator.apply(this, arguments);
    iterator.next();
  };
};

The suspend function accepts a generator and returns a function. The callback supplied to suspend will be passed the resume function, which accepts an error argument to fit Node’s callback API style. The user-supplied callback can then call yield on an asynchronous function that accepts resume as its callback, allowing Node’s core modules (or any other asynchronous methods) to be used in a synchronous style:

suspend(function* (resume) {
  var data = yield fs.readFile(__filename, resume);
})();

I liked this twist on generators, and I think modules like this will start to become more important in the JavaScript community over the next few years.


blog comments powered by Disqus