最近、お仕事でNode.jsを使っている。Node.jsは、いろいろな処理が非同期処理を前提とした作りとなっており、「ある処理を行った後にする処理」を、「ある処理」を行う関数のコールバック関数として書く必要がある。例えば、ファイルに何か書く場合は、こう。
require('fs'); // ファイルのIOを行うfsモジュール読み込み。
fs.readFile('foo.txt', 'utf8', function(error, data) {
// エラーの場合は、コールバックの第1引数にエラーを表すオブジェクトが入る
if(error) {
console.log("error!");
return;
}
console.log(data); // ファイルの内容
});
わお! ファイルを読むだけで1つコールバック関数が出てきた。
例に挙げたような単純な物ならば特に問題にはならないだろうが、大体のプログラムは、こんなに簡単ではない。ファイルを読んで、DBのAとBとCテーブル読んで、それらでゴニョゴニョした結果を別のファイルに書き込んで、そんな処理が2つも3つも在って……とやっていくと、出来上がるのは深い深ーいコールバック関数の「谷」である。人はコレを「コールバック地獄」と呼ぶとか呼ばないとか。
コレは拙いよね、どうにかしないとね。という訳で使われるのが、async.jsというライブラリ。というか、その中のwaterfall
やseries
か。
この内、waterfall
関数の効果的な使い方が、イマイチよく分からない。
waterfall
には、その第1引数に関数のリストとなる配列、第2引数に最終的な処理を行う関数を指定する。第1引数の関数を順に実行し、何れかの処理に「失敗」した場合、或いは全ての関数の処理に「成功」した場合に、第2引数の関数が呼び出される。
var async = require('async'); // async.jsのロード。
async.waterfall(
[ // 関数の配列。ここに挙げられた関数が順に実行される。
process1,
process2,
process3,
],
// waterfallの第1引数の配列の関数が全て成功する、
// 或いは何れかが失敗すると実行される。
final
);
function process1(next) { // 最後の引数は、次に呼ぶ関数を呼び出す関数。
// 略。いろいろ処理する。
// 成功の場合は、第1引数に「真偽判定した時に偽になる」値を指定
// (大体nullが使われる模様)、
// 第2引数以降に、次の処理に渡したいデータを指定する。
next(null, 'result1-1', 'result1-2');
}
function process2(arg1, arg2, arg3, next) {
// 最後の引数より前の引数には、1つ前の処理から渡されたデータになる。
var value1 = arg1; // ← 'result1-1'
var value2 = arg2; // ← 'result1-2'
// 略
// 失敗の場合は、第1引数にエラーを表す値を、
// 第2引数移行には、最終的な処理(この場合final)に渡すデータを指定。
next({ value: 123 }, 'result2-1');
}
function process3(next) {
// (この例の場合は、このコードは実行されない。)
// waterfallの第1引数にリストアップされた最後の関数のnextは、
// 最終的な処理(この場合final)を呼び出す。
next(null, 'result3-1');
}
function final(error, arg) {
// 第1引数には、直前の処理のコールバック関数(process1~process3で
// nextと名付けた引数)の第1引数が、
// それ以降の引数には、直前の処理のコールバック関数の第2引数以降が
// 渡される。
var e = error
var value = arg; // ← 'result2-1'
}
ここで挙げたコードは、あくまで例なので、「process1
」〜「process3
」なんて意味を持たない名前になっている。しかし、それぞれの関数は実際には「何をするのか」がハッキリしている筈だ。例えば「(ファイルから)設定を読み込む」「(DBから)条件に合うデータを検索する」「結果を(ファイルに)書き込む」……
さて、waterfall
を使ったコードを改修する事になった、とする。例えばprocess1
とprocess2
の間にprocess1_5
が入る事になった。或いはprocess2
とprocess3
を逆にする必要が生じたのかもしれない。
ここで、waterfall
の第1引数の配列にprocess1_5
を追加したり、process2
とprocess3
を並べ直したりすれば解決するかというと、恐らく、そうはならない。各関数の仮引数、及びコールバック関数(next
関数)の実引数が、「直前の処理」「直後の処理」に依存するからだ。「process1
とprocess2
の間にprocess1_5
を差し挟む」場合、「process1
が呼ぶコールバック関数の引数列」を変更する必要がある。改修とは直接関係ない筈のprocess1
とprocess2
の中身にも注意を払う必要が出てきてしまう。これは嬉しくない。特にJavaScriptは動的型付け言語なので、ついウッカリ修正を忘れた場合、それと分かりづらいエラーが発生する事になる(「TypeError: object is not a function」とか「TypeError: undefined is not a function」とか。理想的なエラーメッセージは「(対象が)関数ではない」事ではなく「引数の型が想定外」の筈だ)。
どうすれば、こういう危険性を排除してwaterfall
を使う事が出来るのだろう……?