Node.jsにおけるPromise
Node.jsのAPIの多くは、
fs.readFile(fileName, function(err, data) {
if (err) {
// onError
}else{
// onSuccess
}
})
のような形式なので、逐次的に処理を行うとその分だけネストの階層が増えてしまいます。これを解消するために使われるのがPromiseです。上記に対応するコードは以下のようになります。
new Promise(function(onFulfilled, onRejected){
fs.readFile(fileName, function(err, data) {
if (err) {
onRejected(err);
}else{
onFulfilled(data);
}
})
})
.then(function(data){
// onSuccess
}, function(err){
// onError
});
thenが返す戻り値もPromiseなので、さらにthenをつなげることで逐次処理を実現できます。ここで、thenが返すPromiseの動作は2つの方法で決定されます一つは、onSuccessハンドラ内で、別のPromiseをreturnすることです。この場合、その結果が次のthenに渡されます。もう一つは、onErrorハンドラを省略することで、こうすると、エラー時には次のthenに処理が引き継がれます。
これらの性質を使うことで、逐次的な処理を、ネスト階層を上げずに実現することができます。
denodeify
ただ、この方法、コード量的には短くなるどころかかえって長くなってしまっています。Node.jsではAPIのコールバックの形式がほとんど同じなので、Promiseの生成処理も毎回同じになるわけですが、それを毎回書くのは冗長です。そこで、Promise.denodeifyの出番です。上記のコードは、
Promise.denodeify(fs.readFile)(fileName)
.then(function(data){
// onSuccess
}, function(err){
// onError
});
と書き換えられます。denodeifyは関数を戻す関数で、戻り値となる関数は元の関数(fs.readFile)と同じ形式の引数を取って、Promiseを返します。
ただ、これはすべての関数に使えるわけではありません。たとえば、dbがデータベースへの接続を表すオブジェクトだとして、
Promise.denodeify(db.open)()
は動作しません。関数呼び出し時にthisが正しく設定されないからです。この場合は、
Promise.denodeify(db.open).call(db)
とthisを明示的に指定してあげる必要があります。実は先ほどのfs.readFileの場合も、
Promise.denodeify(fs.readFile).call(fs, fileName)
としても動きます。というか、その方が無難です。fs.readFileが内部でthisを使っているかどうかは、コードの中身を見ないと分からないからです。fs.readFileは現状の実装では内部でthisを使っていないので、最初の記述でも動くだけです。denodeifyを使うときは、callを一緒に使うというのを基本パターンとして覚えておくと良いでしょう。
これは通常問題になりませんが、以下のような場合、
var m = collection.find();
return Promise.denodeify(m.toArray).call(m);
中間の変数mを置かないといけないのでコードの見通しが悪くなります。これが嫌であれば、以下のようなラッパーを使うのが良いかもしれません。
function promisize(obj, funcName){
var func = Promise.denodeify(obj[funcName]),
slice = Array.prototype.slice;
return function(){
func.apply(obj, slice.call(arguments));
}
}
return promisize(db, 'toArray')()
互換性
こんな便利なdenodeifyですが、大きな注意点があります。それは、Node.jsのライブラリpromise.jsに固有の機能だということです。他の処理系のPromiseでは使えません。ただ、心配はいりません。というのも、denodeifyという名前の通り、この関数が対応しているのは、Node.js形式のコールバックを引数として取る関数だけだからです。なので、予期しない互換性の問題は、それほど頻繁には起きないのではないかと思います。