08.102015

Generatoren zur Programmflusssteuerung in NodeJS

Callbacks und Promises sind euch nicht elegant genug? Ihr wollt einfach nur in Reihe verschiedene Funktionen aufrufen, aber die an sich genialen Callbacks zwingen euch dazu mehr Code als nötig zu schreiben?

Vor kurzem habe ich Promises als eine Lösung für die sequentielle Verarbeitung asynchronen Funktionen in Javascript (vor allem auch NodeJS) vorgestellt. Nun haben mit Node4 ES6 Generator-Funktionen Einzug in den Sprachkern gefunden (bereits seit 0.11.2 sind Generatoren via Flag zuschaltbar). Generatoren bieten eine weitere interessante Möglichkeit Code zu strukturieren und deshalb starten wir heute auch direkt mit einem Beispiel:

function fetchVersion(callback) {
 setTimeout(function() {
  var version = '1.0.0';
  console.log('upstream version:', version);
  callback(null, version);
 }, 500);
}

function download(version, callback) {
 setTimeout(function() {
  console.log('finished downloading', version);
  callback();
 }, 500);
}

function install(version, callback) {
 setTimeout(function() {
  console.log('finished installing', version);
  callback();
 }, 500);
}

run(function*(resume) {
  try {
    console.log('fetching upstream version number');
    var version = yield fetchVersion(resume);

    console.log('downloading', version);
    yield download(version, resume);

    console.log('installing', version);
    yield install(version, resume);

    console.log('update to', version, 'complete');
  } catch (err) {
    console.log(err.stack);
  }
});

Genial oder? Doch jetzt fragt ihr euch sicherlich, was ist denn diese ominöse run-Funktion, die ich hier verwende. An sich benötigen wir einen Wrapper, der unseren Generator initialisiert und die next()-Funktion auf diesen aufruft, bis entweder ein Fehler auftritt, oder kein weitere Wert mehr geliefert werden kann.

Eine schöne Implementierung lieferte Tim Caswell (creationix - Author von nvm) bereits 2013 hier

function run(generator) {
  var iterator = generator(resume);
  var data = null, yielded = false;

  iterator.next();
  yielded = true;
  check();

  function check() {
    while (data && yielded) {
      var err = data[0], item = data[1];
      data = null;
      yielded = false;

      if (err) return iterator.throw(err);
      iterator.next(item);
      yielded = true;
    }
  }

  function resume() {
    data = arguments;
    check();
  }
}

Wer eine fertige Library bevorzugt kann sich suspend oder das von suspend inspirierte genny anschauen. Beide unterstützen neben einfacher sequentieller Abarbeitung auch parallele Verarbeitung in einer an fork und join angelehnten Syntax.

Einziges Manko ist, dass Stacktraces zu Fehlern nicht mehr sonderlich durchsichtig sind. Abhilfe verschaffen die genannten Libs oder ein manuelles Wrappen des Error in der hier vorgestellten Run-Funktion.

Sicherlich findet Ihr noch viele weitere tolle Anwendungsfälle für Generatoren!