いぇーい yield と co と koa
Posted: Updated:
いぇーい
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、並列実行用です。
- promises
- thunks (functions)
- array (parallel execution)
- objects (parallel execution)
- generators (delegation)
- generator functions (delegation) https://github.com/visionmedia/co#yieldables
戻りが Promise であれば、co 自身で使いやすい形にノーマライズしてくれるので、あまり意識せずに yield
で戻してやることができます。
co(function *() {
// mongoose Model
yield Model.find({}).exec(); // return
});
このように、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 周りの処理も co
と koa-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
このような出力が得られます。ね、簡単でしょう?
あわせて読みたい
- harmony:generators Delegating yield [ES Wiki]
- koa入門(ミドルウェアの書き方) - from scratch
- Node.js 0.12 では yield が使えるのでコールバック地獄にサヨナラできる話 - てっく煮ブログ
yield周りの語彙が結構自信ないので、気になるところは教えてもらえたら嬉しいです。
ノシ