qsonaのブログ

プログラマーです。

async.waterfallとneo-async.angelFallの話

今回登場するモジュールはこちらです。

github.com

github.com

簡単に説明すると、Node.jsにおいて、callback地獄とif (err) return callback(err);の多用を軽減して読みやすくするためのモジュールです。

async.waterfallについて

async.waterfallは、非同期な処理を逐次行っていく時に、メインに使うメソッドです。

使い方

var async = require('async'); // or 'neo-async'
async.waterfall([
  function(next) {
    next(null, 1);
  },
  function(result, next) {
    // result に1が入る。
    next();
  }
], function(err) {
  // 途中でnext(err)すると、ここに飛んでerrが入る。
}

問題点

次のように、第2引数を返したり返さなかったりするメソッドがあったときに、その次の関数の受け方が定まらなくて、よくバグの原因になる。

function anAsyncMethod(foo, callback) {
  if (foo) {
    callback(null, 1); // [A]
  } else {
    callback(null); // [B]
  }
}

async.waterfall([
  function(next) {
    var foo = false;
    anAsyncMethod(foo, next);
  },
  function(result, next) {
    // [A] の場合はresultに値(1)が入るが、
    // [B] の場合は入らない。(すなわち、resultにnextと呼ぶべき関数が入っている)
    next(); // TypeError: next is not a function
  }
], function(err) {
});

回避方法はある(後述)ものの、面倒だし、
「値を返していない状態から、返すように変更する」ことがbreaking changeになるのも辛い。
同期関数だったり、非同期コールバックスタイルでも普通の使い方であれば、そうはならないのに。

回避方法

anAsyncMethodがコールバックへ引数渡してきたり渡さなかったりするから困るなー、でもanAsyncMethod自体は書き換えられないな、という時は、次のように回避する。

async.waterfall([
  function(next) {
    var foo = false;
    anAsyncMethod(foo, function(err, result) {
      next(err, result); // 1つ渡すことを強制している
    });
  },
  function(result, next) {
    // ...

neo-async.angelFall

waterfallと似ているが、上述の問題が解決されたもの。

常に最後の引数にnext(と呼ぶべき関数)を渡す。

すなわち、関数の仮引数を、単に(next)と書けば、前の関数が何を渡そうが無視されて、nextに関数が入る。
もし(result, next)と書けば、仮に前の関数が何も渡してなければresultにundefinedを入れ、nextに関数が入る。

var async = require('neo-async');
async.angelFall([
  function(next) {
    var foo = false;
    anAsyncMethod(foo, next);
  },
  function(result, next) {
    // 定義してる引数の個数が見られ、
    // 2つなので、1つめに前の関数から渡される結果、2つめにnextが入る。
    next();
  }
], function(err) {
});

前回の記事で紹介したFunction.lengthを利用して実現されている。

注意点

async.angelFall([
  function(next) {
    var foo = false;
    anAsyncMethod(foo, next);
  },
  function(result) {
    // nextを呼ばないから仮引数としても定義したくないよ、というとき。(実際あり得る)
    // 仮引数を1つしか定義していないので、ここにnextと呼ぶべき関数が入ってしまい、
    // 期待する値(result)が入ってこない。

他の解決案

github.com

waterfallのタスク関数はfunction(foo, next)ではなく、function(next, foo)の順で取るように仕様変更するという話。

これをすれば、fooがこようがこまいが、nextの位置はずれないので上のような問題は起きない。

実際自分もこれは1年半前くらいに思ったのだけれど、Node.jsの「コールバック関数は最後にとる」という規約に反するので少し気持ち悪い。でも黒魔術しないですむし、async.waterfallに限って言えばこれもありだなぁと思う。

まとめ

Node.jsがcallbackを最後にとるというルールが、思わぬ形で影響を及ぼしてしまっている話でした。