0%

Koa入門 - Part3: ジェネレータなどKoaの特徴

前回までにKoaのアプリを開発するためのio.jsのDockerイメージを作成したり、Babel
でES6で書いたJavaScriptのコンパイルを試しました。今回はジェネレータなどKoaで開発する場合に重要な特徴を見ていきます。

リソース

KoaのREADMEにGetting startedとして以下のサイトが紹介されていました。

やはり最初のジェネレーター関数を理解するところです。ハンガリーのRisingStackというNode.jsの開発会社のブログを写経していきます。とてもわかりやすい英語で大変勉強になりました。強みにしているのがJavaScript/DevOps/IoTというのも親近感があります。

ジェネレータ関数

ジェネレータ関数はfunctionキーワードの後に*のアスタリスクを付けて定義します。

function *foo() {}

この関数をコールするとイテレータオブジェクトが返ります。通常の関数と異なりコールしても実行されません。返されたイテレータに対して処理を実行します。

> function *foo(arg) {}
> var bar = foo(123);

イテレータオブジェクトbarnext()メソッドをコールしてイテレートを開始します。next()をコールすると関数は開始するか、中断していた場合は次のポイントまで実行されます。この処理ではジェネレータの状態オブジェクトを返します。valueプロパティは現在のイテレーションの値で、この場所でジェネレータは中断しています。もう一つはブール値のdoneプロパティです。こちらはジェネレータが終了したかを示します。

> function *foo(arg) { return arg }
> var bar = foo(123);
> bar.next()
{ value: 123, done: true }

この例では処理の中断はしていないのでdonetrueのオブジェクトがすぐに返ります。もしジェネレータの中でreturnがあれば、最後のイテレータオブジェクト(doneがtrue)が返ります。
次にジェネレータを中断してみます。関数をイテレーションする毎にyieldが処理を中断したときの値を返します。つまりyieldキーワードのところで処理は中断しています。

yield

next()をコールするとジェネレータは開始してyieldのところまで進みます。その場所でvaluedoneを含むオブジェクトを返します。valueは式(expression)の値です、数値や文字列など結果として値を返すものならなんでも良いです。

function *foo() {
var index = 0;
while (index < 2) {
yield index++;
}
}

var bar = foo();
bar.next()
//{ value: 0, done: false }
bar.next()
//{ value: 1, done: false }
bar.next()
//{ value: undefined, done: true }

next()を再び実行するとyieldの結果が返り処理が再開します。イテレータオブジェクトからジェネレータの中で値を受け取ることもできます。next(val))のようにすると、処理が再開したときにジェネレータに値が返ります。

> function *foo() {
... var val = yield 'A';
... console.log(val);
... }
> var bar = foo();
> console.log(bar.next());
{ value: 'A', done: false }
> console.log(bar.next('B'))
B
{ value: undefined, done: true }

エラー処理

間違いを見つけた時はイテレータオブジェクトのthrow()メソッドをコールするとジェネレータの中でエラーをcatchしてくれます。こうするとジェネレータの中のエラーハンドリングがとても良く書けます。

function *foo() {
try {
x = yield 'asd B';
} catch (err) {
throw err;
}
}

var bar = foo();
if (bar.next().value == 'B') {
bar.throw(new Error("its' B!"));
}

for…of

ES6のループにはジェネレータをイテレートするきにつかうfoo...ofループがあります。イテレーションはdonefalseになるまで続きます。注意が必要なのはこのループを使う時にはnext()のコールに値を渡すことができません。ループは渡された値を無視します。

function *foo() {
yield 1;
yield 2;
yield 3;
}

for (v of foo()) {
console.log(v);
}

yield*

上記のようにyieldでは何でも扱えるため、ジェネレータも可能ですがその場合はyield *を使う必要があります。この処理をデリゲーションといいいます。別のジェネレータをデリゲートすることができるので、複数のネストされたジェネレータのイテレーションを、1つのイテレータオブジェクトを通して操作することができます。

function *bar() {
yield 'b';
}

function *foo () {
yield 'a';
yield *bar();
yield 'c';
}

for (v of foo()) {
console.log(v);
}
//a
//b
//c

Thunks

Thunksはまた別のコンセプトですが、Koaを深く知るためには理解が必要です。主に別の関数のコールしやすくするために使われます。サンクは遅延評価とも呼ばれます。重要なことはNode.jsのコールバックを引数から関数のコールとして外に出せるということです。

var read = function(file) {
return function(cb) {
require('fs').readFile(file, cb);
}
}

read('package.json')(function (err, str) {});
}

thunkifyという小さなモジュールは、普通のNode.jsの関数をthunkに変換してくれます。どう使うか疑問に思うかもしれませんが、コールバックをジェネレータの中に放り込むときに便利です。

var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);

function *bar () {
try {
var x = yield read('input.txt');
} catch (err) {
throw err;
}
console.log(x);
}

var gen = bar();
gen.next().value(function (err, data) {
if(err) gen.throw(err);
gen.next(data.toString());
});

十分な時間をとってこの例をパーツ毎に理解することが必要なのは、Koaを使うときにとても重要なことがあるからです。ジェネレータの使い方が特にクールです。同期的なコードのシンプルさがありながら、エラー処理もしっかりしているのに、これは非同期で動いています。