JavaScriptでポイントフリースタイル

積ん読してあった「ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門」を読んだんだけど、
その中にポイントフリースタイルというのが書いてあったので、
同じような事がJavaScriptでもできないか試してみる。


本書によると、ポイントフリースタイルっていうのは関数で関数を
定義しながらプログラミングをするコーディングスタイルの事だそうだ。


要は関数合成や引数の部分適用などにより高階関数を返すような関数を定義し、
それを呼び合いながらプログラムを書こうってこと。
JavaScript高階関数を扱えるので近い事はできそうだ。

引数の部分適用

まずは関数が引数の部分適用をできるようにする。つまりカリー化。


よくある、curry関数に関数と引数を渡す事で部分適用された関数を返すという方法だと
再度、部分適用する時に何度もcurry関数を呼び出す必要がある。
こんな風に

curry(curry(function(x,y,z){}, 1), 2)(3);


これだと冗長なのと呼出側で意識する必要があるので、
関数呼び出し時の引数の数に応じて、関数を返すか評価した値を返すかを、
判断してくれる関数を返す、関数をカリー化に対応させる関数を作成する。


できたのが以下。

function define(func, argc) {
    var argc = argc || func.length;
    return function() {
      var args = Array.prototype.slice.call(arguments);
      if(arguments.length >= argc);
        return func.apply(this, args);
      else
        return define(function() {
          return func.apply(this, args.concat(Array.prototype.slice.call(arguments)));
        }, argc - arguments.length);
    }
}

関数を定義する時はこのようにする。

var myfunc = define(function(x,y,z) {
    return x + y + z;
});

呼び出し時は以下のどの形式でも呼び出せる。

myfunc(1,2,3);
myfunc(1)(2)(3);
myfunc(1,2)(3);
myfunc(1)(2,3);

JavaScriptっぽくメソッドとして呼び出せるように関数のprototypeにも宣言してやる。

Function.prototype.curring = function(argc) { return define(this, argc); };

これで
define(myfunc) も myfunc.curring() のどちらでもカリー化対応した関数を返してくれる。

関数の合成

次に関数の合成。合成といっても単純に関数の戻り値を引数に次の関数を呼び出すのみ。
こちらもメソッドとして呼び出せるようにしておく。

function composite(src, target) {
  return function() {
    return src.apply(this, [].concat(target.apply
                           (this, Array.prototype.slice.call(arguments))));
  };
}
Function.prototype.$ = function(target) { return composite(this, target) };

以下のようにして合成。

function square(n) {
    return n * n;
}

var myfunc = square.$(function(x,y){ return x + y; });
myfunc(2,3); // -> 25

var myfunc2 = myfunc.$(function(m){ return [m, m + 1] });
myfunc2(3); // -> 49

合成された関数のカリー化対応

関数のカリー化の為には仮引数の数を知っておく必要があるので、
それぞれの関数を少し修正する。
関数のプロパティとして__length__を定義し、
そこに引数の数を入れてやるようにする。
修正を加えたものが以下。

function define(func, argc) {
  var f = function() {
    var args = Array.prototype.slice.call(arguments);
      if(arguments.length >= arguments.callee.__length__)
        return func.apply(this, args);
      else
        return define(function() {
          return func.apply(this, args.concat(
                           Array.prototype.slice.call(arguments)))
        }, arguments.callee.__length__ - arguments.length);
  };
  f.__length__ = argc || func.length;
  return f;
}
Function.prototype.curring = function(argc) { return define(this, argc); };

function composite(src, target) {
  if(!src.__length__) src = src.curring();
  if(!target.__length__) target = target.curring();
  return define(function() {
    return src.apply(this, [].concat(target.apply(
                           this, Array.prototype.slice.call(arguments))));
  }, target.__length__);
}
Function.prototype.$ = function(target) { return composite(this, target) };

動作確認

var square = define(function(n) { return n * n });
var add = define(function(x, y) { return x + y });
var add1 = add(1);

var myfunc = square.$(add).$(add1); // 3つの関数を合成
myfunc(5)(4); // まず1引数を部分適用後、もう一個の引数を渡して評価
// ((5 + 1) + 4)^2 という計算が行われる
// 100

おー動いたー。


組み込み関数でも試す。

var add5 = define(function(x,y){ return x + y })(5);
var add_disp = alert.$(add5.$(parseInt));

// 文字列で2進数を与えて10進に変換し5を加えてアラートで表示
add_disp("1010","2"); // 15とアラート表示

これは結構イケるかもしれない。
ただし読み手にもある程度のスキルを要求するので、
一般的には使えない。。。


※20080822 コード中のスペルミスとインデントを修正