引数固定、関数合成
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 によって適用してやる必要がある、ということです。