JavaScriptでの非同期関数合成

Unserscore.jsや互換ライブラリのLo-Dashを使うと関数合成が出来ます。
複数個の関数があって、関数を呼び出した結果を使って関数を呼び出して…っていうのを1個の関数にします。
ドキュメントの例を見れば分かるかと。


簡略化のために関数合成の対象になる関数を1引数・戻り値ありの関数とします。
これを非同期処理をする関数に当てはめるとcallbackを含む2引数・戻り値なしの関数が当てはまるでしょう。
この場合のcallbackは1引数の関数とします。

まず、logを出力するcallback関数を定義しましょう。

function log(result){
  console.log(result);
}

次にcallbackを含む2引数・戻り値なしの関数を定義します。別に非同期処理はやっていないです。

// 1を足す
function add1(callback,arg){
  callback(arg+1);
}

// 2をかける
function mul2(callback,arg){
  callback(arg*2);
}

実行してみましょう

add1(log,4); // 5
mul2(log,5); // 10

結果を引き回す形で呼んでみましょう。

add1(function(result){
  mul2(log,result);
},4); // 10

呼び出しが深くなるとcallbackのネストが段々鬱陶しくなります。

add1(function(result){
  mul2(function(result){
    add1(log,result);
  },result);
},4);

Function#bindによる部分適用に置き換えます。

add1.bind(null,mul2.bind(null,log))(4); // 10

べた書き部分を汎用にしてみます。

(function(){
  var slice = Array.prototype.slice;
  var funcs = slice.call(arguments);
  return funcs.reduceRight(function(prevResult,current){
    return current.bind(null,prevResult);
  })
})(add1,mul2,log)(4);

今回やりたいことは、callbackを含む2引数・戻り値なしの関数をn個合成してcallbackを含む2引数・戻り値なしの関数にすることなのでこれは違います。
で、こうします。

function noop(){}
function composeAsync(){
  var slice = Array.prototype.slice;
  var funcsOrig = slice.call(arguments);
  return function (callback,arg) {
    var funcs = funcsOrig.slice();
    funcs.push(callback || noop);
    funcs.reduceRight(function(prevResult,current){
      return current.bind(null,prevResult);
    })(arg);
  };
}
実行してみましょう
composeAsync(add1,mul2)(log,4); // 10
composeAsync(add1,mul2,add1)(log,4) //11

「callbackを含む2引数・戻り値なしの関数をn個合成してcallbackを含む2引数・戻り値なしの関数にする」ので合成した関数も合成対象にできます。

var composed = composeAsync(add1,mul2);
composeAsync(composed,composed)(log,4);

この縛りでは合成対象の関数の汎用性に問題があるという場合は、
汎用性のある多引数の関数を定義して部分適用したものを合成すればいいと思います。