引数固定、関数合成

id:reinyannyan:20070110:p1 の Scheme 風関数群を用いて、"cut" と "compose" という、関数操作のための便利な関数を実装します。また、その応用例として、関数的な DOM 操作の手法についても見ていきたいと思います。

引数固定

Scheme の拡張仕様として "cut" という関数があります (仕様: http://srfi.schemers.org/srfi-26/srfi-26.html)

これは、関数の引数リストの任意の箇所を固定 (予め指定) しておくための関数です。いわゆる currying:

function curry(f) {
  var x = list_tail(arguments, 1);
  return function() {
    return apply(f, append(x, list_tail(arguments, 0)));
  };
}

は、引数リストの左側しか固定できないという点が cut とは異なります。

cut の実装です:

function cut() {
  var x = list_tail(arguments, 0); // argument list with slots (holes to be filled in later)
  return function() {
    var y = list_tail(arguments, 0); // arguments that fill the holes
    var z = map(function(v) {
                  if (eqp(v, "<>")) {
                    v = car(y);
                    y = cdr(y);
                  }
                  return v;
                },
                x);
    return apply(car(z), cdr(z));
  };
}

引数リストの穴は "<>" という文字列で表す仕様にしてみました (実際に "<>" という引数を渡したい時はどうするのか、という問題は敢えて無視しています)。

使い方はこんな感じです:

var mul2 = cut(function(a,b){return a*b}, "<>", 2);
mul2(2);
// -> 4

map(mul2,
    list(1,2,3,4,5));
// -> [2,4,6,8,10]

この場合は curry で左側の引数を固定しても同じことですね。

また、「関数への引数」ではなく「関数」そのものを空欄にしておくことも出来ます:

map(
  cut("<>", 1,2,3,4,5),
  list(min, max)
);
// -> [1,5]
/* where
 *  min = function(){ return Math.min.apply(null, arguments) }
 *  max = function(){ return Math.max.apply(null, arguments) }
 */

関数合成

関数 f と g を合成して f(g(x)) を行う関数を作る、というものです:

function compose(f, g) {
  if (not(g))
    return f;
  var more = list_tail(arguments, 2);
  return nullp(more)
      ? function(){ return f(apply(g, arguments)) }
      : compose(f, apply(compose, cons(g, more)));
}

再帰処理により、3つ以上の関数も合成可能になっています。

使用例:

var ls = list(1,2,3,4,5);
var cadr = compose(car, cdr);
cadr(ls);
// -> 2

compose された関数群は最後から順番に実行される、というのがポイントです。つまり、リストの2番目の要素を取り出す、という操作の場合

car(cdr(ls))

とするのと同じ順番で compose すれば良いことになります。

また、既に compose された関数に基づいて新たな関数を compose することも自在です:

var caddr = compose(cadr, cdr);
caddr(ls);
// -> 3

応用: 関数的な DOM 操作

以下、JavaScriptを関数的っぽくしたらモテるかもな より例題を引かせて頂きます。

まず、ドキュメントに "Shibuya.js" という文字列を出力する、というケースを考えてみましょう。

これは、

document.createElement で要素を生成 -> その innerHTML に文字列を設定 -> ドキュメントに追加

という流れを作ってやれば良いわけです。compose が使えますね:

var addNewElement = compose(
  document.body.appendChild,
  function(x){x.innerHTML = "Shibuya.js"; return x},
  document.createElement
);

addNewElement("div");
// -> <DIV>Shibuya.js</DIV>

上述の様に、compose された関数達は最後のものから実行されますので、addNewElement は document.createElement への引数を待っている関数、ということになります。そして、最初に与えられた引数は、Unix のパイプの様に順次加工されつつ次へ次へと渡されていきます。

さて、このままでは出力できる文字列が限定されているため、あまり使い道がありません。そこで cut の出番です。

addNewElement = cut(
  compose,
  document.body.appendChild,
  "<>",
  document.createElement
);

innerHTML を設定する部分を空欄にして、任意の関数を指定できるようにしたわけです。

さらに便利のため、そのための専門の関数を作っておきましょう:

function addNewText(text) {
  return function(el){el.innerHTML = text; return el};
}

そうすると、"Shibuya.js" を出力する関数はこのように書くことが出来ます:

var addShibuyaJS = addNewElement(addNewText("Shibuya.js"));
addShibuyaJS("div");
// -> <DIV>Shibuya.js</DIV>

// Or,
var addShibuyaJS = curry(addNewElement(addNewText("Shibuya.js")), "div");
addShibuyaJS();
// -> <DIV>Shibuya.js</DIV>

また、改行を出力する関数は、

var br = curry(addNewElement(identity), "br");
br(); // -> <BR>
/* where
 *  identity = function(x){ return x }
 */

と書けます。

最後に、リスト操作の例を。

リストの文字列を加工しつつドキュメントに加える、というものです。

for_each(
  compose(
    cut(apply, "<>", list("div")),
    addNewElement,
    addNewText,
    cut(add, "<>", ".js")
  ),
  list("Shibuya", "Shinjuku", "Akihabara")
);
// -> <DIV>Shibuya.js</DIV> <DIV>Shinjuku.js</DIV> <DIV>Akihabara.js</DIV>
/* where
 *  add = function(a,b){ return a+b }
 */

compose の第一引数がちょっとややこしい部分かも知れません。addNewElement が返してくるのはまだ適用待ちの関数ですので、apply によって適用してやる必要がある、ということです。