2015年5月12日
Node.jsフロー制御 Part 2 – FiberとGenerator
(2015-02-16)by Eirik Vullum
本記事は、原著者の許諾のもとに翻訳・掲載しております。
この記事は、 以前投稿したJavaScript/node.jsでの非同期フローに関する記事 の続編です。
今回は以下について取り上げます。
- Fiber(fibrous.js)
- Generator(ES6)
- Generator + co + mz
ここでも私の書いた、Expressフレームワークを使った以下のルート処理(お粗末ですが)を例に見てみましょう。
- ファイルから読み込む
- いくつかのプロセスを実行する(ステップの数は3つ)
プロセスとは、単に拡張データをコールバックする任意の非同期処理を指します - ファイルに結果を書き出す
- リクエストに対して成功またはエラーのメッセージを返す
アプローチ1 – fiberを使う
var fs = require('fs');
var express = require('express');
var fibrous = require('fibrous');
var app = express();
app.get('/', function(req, res) {
fibrous.run(function() {
var inputFile = 'input.txt';
var outputFile = 'output.txt';
try {
var inputData = fs.sync.readFile(inputFile);
var processedData1 = process1.sync(inputData);
var processedData2 = process2.sync(processedData1);
var result = process3.sync(processedData2);
fs.sync.writeFile(outputFile, result);
res.status(200).send('success');
} catch (err) {
res.status(500).send(err);
}
}, function(err) {...});
});
fiberを使用することで、フローは同期的なコードのようになります。非同期関数が値を返すようになるからです。これは、fiberがもたらす”マジック”のような効果の一部です。関数あるいはオブジェクトのメソッドは全て、これを可能にする.syncバージョンを持つことになります。
アプローチ2 – generator(ES6)
generatorはES6で新しく導入された仕組みで、function *()というキーワードを使ってgenerator関数を作り出すことができます。generator関数は(var iter = genFun();)という記述で呼び出され、iteratorを返します。iteratorには、新しく導入されたyieldというキーワードが出現するまでコードの処理を進めることができる特別な機能があります。また、yield式は実行中のアプリケーションを終了させず、戻り値を待つ間だけ一時的に停止させます。
詳細については こちら をご覧ください。
# 部分適用を使用した例
var fs = require('fs');
var express = require('express');
var run = require('../../lib/runner');
var app = express();
app.get('/', function(req, res) {
run(function *() {
var inputFile = 'input.txt';
var outputFile = 'output.txt';
try {
var inputData = yield fs.readFile.bind(fs, inputFile);
var processedData1 = yield process1.bind(null, inputData);
var processedData2 = yield process2.bind(null, processedData1);
var result = yield process3.bind(null, processedData2);
yield fs.writeFile.bind(fs, outputFile, result);
res.status(200).send('success');
} catch (err) {
res.status(500).send(err);
}
});
});
iteratorの実行関数runの部分
function run(fn) {
var gen = fn();
function next(err, res) {
if (err) return gen.throw(err);
var ret = gen.next(res);
if (ret.done) return;
ret.value(next);
}
next();
};
# カリー化を使用した例(lodashで使用した場合)
var fs = require('fs');
var express = require('express');
var run = require('../../lib/runner');
var _ = require('lodash');
var app = express();
app.get('/', function(req, res) {
run(function *() {
var inputFile = 'input.txt';
var outputFile = 'output.txt';
try {
var inputData = yield _.curry(fs.readFile.bind(fs))(inputFile);
var processedData1 = yield _.curry(process1)(inputData);
var processedData2 = yield _.curry(process2)(processedData1);
var result = yield _.curry(process3)(processedData2);
yield _.curry(fs.writeFile.bind(fs))(outputFile, result);
res.status(200).send('success');
} catch (err) {
res.status(500).send(err);
}
});
});
# thunkを使用した例
var fs = require('fs');
var express = require('express');
var run = require('../../lib/runner');
var thunkify = require('../../lib/thunkify');
var app = express();
app.get('/', function(req, res) {
run(function *() {
var inputFile = 'input.txt';
var outputFile = 'output.txt';
try {
var inputData = yield thunkify(fs.readFile.bind(fs))(inputFile);
var processedData1 = yield thunkify(process1)(inputData);
var processedData2 = yield thunkify(process2)(processedData1);
var result = yield thunkify(process3)(processedData2);
yield thunkify(fs.writeFile.bind(fs))(outputFile, result);
res.status(200).send('success');
} catch (err) {
res.status(500).send(err);
}
});
});
thunkifyは基本的にカリー化の2つのステップと同等の処理を行う関数なので、最初の呼び出しで入力データを渡し、2回目でコールバックを渡します。
function thunkify(fn) {
return function() {
var args = Array.prototype.slice.call(arguments, 0, fn.length - 1);
return function(done) {
fn.apply(null, args.concat(done));
};
};
};
# Promiseを使用した例
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));
var express = require('express');
var run = require('../../lib/runner');
var app = express();
app.get('/', function(req, res) {
run(function *() {
var inputFile = 'input.txt';
var outputFile = 'output.txt';
try {
var inputData = fs.readFileAsync(inputFile);
var processedData1 = yield Promise.promisify(process1)(inputData);
var processedData2 = yield Promise.promisify(process2)(processedData1);
var result = yield Promise.promisify(process3)(processedData2);
yield fs.writeFileAsync(outputFile, result);
res.status(200).send('success');
} catch (err) {
res.status(500).send(err);
}
});
});
generator関数にyieldを入れてPromiseを使うには、それをサポートするために、iteratorの実行関数runを修正する必要があります。
function run(fn) {
var gen = fn();
function next(err, res) {
if (err) return gen.throw(err);
var ret = gen.next(res);
if (ret.done) return;
if (typeof ret.value.then === 'function') {
try {
ret.value.then(function(value) {
next(null, value);
}, next);
} catch (e) {
gen.throw(e);
}
} else {
try {
ret.value(next);
} catch (e) {
gen.throw(e);
}
}
}
next();
};
アプローチ3 – generator + co + mz
独自のiteratorの実行関数を構築する代わりに、 T.J Holowaychuck が作成したcoモジュールを使うことができます。このモジュールは、promiseで実行全体をラップしている間、promise、thunk、配列、オブジェクトなどといったいくつかのオプションをyieldするサポート機能があります。
# coを使用した例
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));
var express = require('express');
var co = require('co');
var app = express();
app.get('/', function(req, res) {
co(function *() {
var inputFile = 'input.txt';
var outputFile = 'output.txt';
try {
var inputData = fs.readFileAsync(inputFile);
var processedData1 = yield Promise.promisify(process1)(inputData);
var processedData2 = yield Promise.promisify(process2)(processedData1);
var result = yield Promise.promisify(process3)(processedData2);
yield fs.writeFileAsync(outputFile, result);
res.status(200).send('success');
} catch (err) {
res.status(500).send(err);
}
}).catch(funtion(err) {...});
});
このコードをさらに簡潔にするには、mzと呼ばれるヘルパーライブラリを使うことができます。mzは、Promiseを戻すように全てのネイティブな非同期APIを修正します。
# co + mzを使用した例
var Promise = require('bluebird');
var fs = require('mz/fs');
var express = require('express');
var co = require('co');
var app = express();
app.get('/', function(req, res) {
co(function *() {
var inputFile = 'input.txt';
var outputFile = 'output.txt';
try {
var inputData = yield fs.readFile(inputFile);
var processedData1 = yield Promise.promisify(process1)(inputData);
var processedData2 = yield Promise.promisify(process2)(processedData1);
var result = yield Promise.promisify(process3)(processedData2);
yield fs.writeFile(outputFile, result);
res.status(200).send('success');
} catch (err) {
res.status(500).send(err);
}
}).catch(onError);
});
このシリーズのPart 1は こちら
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa