問題は非同期コールバックのネストにあるんじゃない、待ち合わせにあるんだ!

Node.jsでのプログラミングで問題としてよく挙げられるコールバック地獄。


ずっと、非同期コールバックがネストすることが問題なんだと思っていました。でも、そうじゃない。ネストすればクロージャが使える(内側の関数から外側の関数スコープにある変数を参照できる)という便利な側面があるし、ネストが深くなりすぎるのが嫌なら別の場所に定義した関数を引数として渡して簡単に回避できる。


問題の大部分は、非同期処理の待ち合わせ処理が面倒なことなんです。
試しに、「2つのファイルを同時に読んで別のファイルに書き出し、書き出した内容を読み込む」、という処理をPure nodeで書いてみるとこうなります。

var fs = require('fs');

var count = 0;
var results = [];

fs.readFile('path1', 'utf8', function(err, data) {
  if(err) throw err;
  count++;
  results[0] = data;
  writeAndRead();
});

fs.readFile('path2', 'utf8', function(err, data) {
  if(err) throw err;
  count++;
  results[1] = data;
  writeAndRead();
});

function writeAndRead(){
  if(count < 2){
    return;
  }
  fs.writeFile('path3', results[0] + results[1], function(err) {
    if(err) throw err;
    fs.readFile('path3', 'utf8', function(err, data){
      console.log(data);
      console.log('all done');
    });
  });
}


countやresults配列のindexといった変数の管理、両方のファイル読み出しが完了したかどうかのチェックと言った処理が煩雑ですね。さらに、並列処理の数が増えたり、ループなどであらかじめ並列処理の数を固定できなかったりするともっと面倒になってしまいます。


じゃあ、単純にこれらの問題を解決してみよう、と思ってライブラリつくってみました。その名もgate。待ち合わせのためのゲートです。npm install gateでインストールできます。


gateを使うと上記の処理はこう書けます。

var gate = require('gate');
var fs = require('fs');

var latch = gate.latch(); // ラッチを取得 
fs.readFile('path1', 'utf8', latch(1)); // ラッチに取得したい引数のindexを教えて非同期処理を実行。
fs.readFile('path2', 'utf8', latch(1)); // ラッチに取得したい引数のindexを教えて非同期処理を実行。
latch.await(function (err, results) { // 上記非同期処理の待ち合わせ。待ち合わせができたらcallbackを実行。
  if (err) throw err;
  fs.writeFile('path3', results[0] + results[1], function (err) {
    if (err) throw err;
    fs.readFile('path3', 'utf8', function (err, data) {
      if (err) throw err;
      console.log(data);
      console.log('all done');
    });
  });
});


ラッチと並列に実行したい非同期処理を紐づけ、それらが完了するまで待ちます。すべて完了すると、awaitに指定したcallbackが実行されるという仕組みです。


大規模なものはわかりませんが、小さなものやユーティリティ的なものを作る場合に便利なんじゃないかなーと思います。


gateはコード数100行以下の小さなライブラリです。