いぇーい yield と co と koa

いぇーい

express の後継だけあって期待が高まってる Koa ですが、あの珍妙な yield による同期処理っぽい記述がどのようにして支えられているかメモってみます。

年末年始を経てヤル気が高まってきたので、久々にnodeの話。

visionmedia/co

さて本題。

早速ですが、koa の middleware における、あの特徴的な yield 天国は、koa ではなく co というモジュールによるものです。サンプルを見るのが早いです。

本件は yield を使うので、現時点では node v0.11.x を --harmony オプション付き実行が必要なことに注意してください

下記は co を単品で利用した場合のサンプルです。

/**
 * GETリクエストを非同期処理するモジュールを想定
 * @example get('http://example.com')(function() { /* callback! */ });
 */
var get = require('get');
var co  = require('co');

/**
 * @example 非同期処理を直列で逐次実行する
 */
co(function *(){
  var a = yield get('http://google.com');
  var b = yield get('http://yahoo.com');
  var c = yield get('http://cloudup.com');
  console.log(a.status);
  console.log(b.status);
  console.log(c.status);
})()

/**
 * @example 非同期処理を並列実行して待ち合わせる
 */
co(function *(){
  var a = get('http://google.com');
  var b = get('http://yahoo.com');
  var c = get('http://cloudup.com');
  var res = yield [a, b, c];
  console.log(res);
})()

非同期な処理を、まるで同期処理かのように平たく記述することができます。この手のコールバック地獄向けのソリューションは色々ありますが、なかなかクールなほうに見えます。

大雑把には以下のような流れで内部処理されています。yieldableという言葉が出てきますが、これは後述します。

  • generator function を co でラップする
  • ラップした関数を実行する
  • generator 内で yield のとき yieldable なオブジェクトを返す
  • co が返ってきた yieldable オブジェクトの完了を判断して gen.next() する
  • 以降 done とか StopIteration まで繰り返し

generator の yield で止めて、gen.next() で止めた場所に戻れる特徴を利用しています。

yieldable

yieldableというのは、co 内で yield を利用した同期処理っぽい文法に対応できるオブジェクトたちです。たぶんcoの造語であって一般的な語彙ではない…気がします。Array や Object は parallel execution、並列実行用です。

戻りが Promise であれば、co 自身で使いやすい形にノーマライズしてくれるので、あまり意識せずに yield で戻してやることができます。

co(function *() {
  // mongoose Model
  yield Model.find({}).exec(); // return <Promise>
});

このように、mongoose の exec などは Promise を返してくれるので簡単に取り扱うことができます。

visionmedia/node-thunkify

前項の2番目に thunks とありますが、これは node-thunkify モジュールで関数をラップすることで生成することができます。

下記のようなExampleを見ると分かりやすいです。

var thunkify = require('thunkify');
var fs = require('fs');
fs.readFile = thunkify(fs.readFile);
fs.readFile('package.json', 'utf8')(function(err, str){
  // callback!
});

伝統的なコールバックを受け取るインターフェースをもった関数(地獄の元)を、ラップしてyield でコールバック処理するためのワンクッションを作っています。

このように、多くの関数は thunkify でラップすることで co に対応できますが、最初から co に対応しているモジュール類が Home · visionmedia/co Wiki にまとめられています。koa も一覧のうち HTTP Server に分類されて名前が入っています。

koaのmiddleware

If you're a front-end developer you can think any code before yield next; as the "capture" phase, while any code after is the "bubble" phase. koa/docs/guide.md at master · koajs/koa

引用で説明されているような特徴をもつ koa の middleware 周りの処理も cokoa-compose における存外にシンプルコードで、実現されています。

上記のコードから抽出して要約すると下記のようになります。

var co = require('co');
 
var stack = [
  function *first(next) { // second generator が渡る
    console.log('1 prev');
    yield next; // second を返す
    console.log('1 next');
  },
  function *second(next) { // third generator が渡る
    console.log('2 prev');
    yield next; // third を返す
    console.log('2 next');
  },
  function *third(next) { // noop generator が渡る
    console.log('3 prev');
    yield next; // noop を返す
    console.log('3 next'); // noop から折り返し実行が開始(バブリング)
  }
];
 
co(function *() {
  console.log('start');
  var prev = function *() {}; // noop generator
  var i = stack.length;
 
  // 逆順からジェネレーターをセットしている
  while (i--) {
    prev = stack[i].call(this, prev)
    console.log('#' + (i+1) + '#');
  }
 
  yield *prev; // first generator に delegate して開始(キャプチャリング)
})();

これを実行すると

start
#3#
#2#
#1#
1 prev
2 prev
3 prev
3 next
2 next
1 next

このような出力が得られます。ね、簡単でしょう?

あわせて読みたい

yield周りの語彙が結構自信ないので、気になるところは教えてもらえたら嬉しいです。

ノシ