関数的っポイ!? JavaScript ― 2006年04月29日 19時40分
関数型プログラミング言語を触ったことがないのでこれが関数的かどうかは知らないが、いちいち function とか return とか書くのは面倒くさいと思ったのと「関数の変形」や beyond.js 、boost::bind に影響を受けて、関数の変形を行うライブラリを作った。
まずは引数の束縛、及び入れ替え。「関数の変形」及び boost::bind にならって後々入力される引数を _n
であらわすことにする。
function concat(a, b) {
return "" + a + b;
}
concat.bind(_1, "a")("b"); // "ba"
concat.bind("a", _1)("b"); // "ab"
concat.bind(_2, _1)("a", "b"); // "ba"
concat.bind(_1, _1)("a"); // "aa"
次に関数の合成。boost::bind のように bind()
を入れ子にする。
f.bind(g.bind(_1)); // f(g(x))
f.bind(g.bind(_1), h.bind(_1)); // f(g(x), h(x))
f.bind(g.bind(_1), h.bind(_2)); // f(g(x), h(y))
f.bind(g.bind(_1, _2)); // f(g(x, y))
f.bind(g.bind()); // f(g())
function plus2(x) {return x + 2;}
function mult2(x) {return x * 2;}
mult2.bind(plus2.bind(_1))(3); // 10
beyond.js にならって文字列を関数として扱えるように。これで「関数の変形」の引数束縛パターン表に出ていた「引数の加工」も行える。
"+".call(1, 2, 3); // 6 ("+".toFunction()(1, 2, 3) の省略形)
func.bind(_1, "+".bind(_2, 42), "+".bind(_2, _3));
// 第 1 引数はそのまま、第 2 引数は 42 を足した値、
// 第 3 引数は第 2 引数を足した値を用いる。
さらに以下の関数を導入することでより柔軟な加工が可能に。Array.slice
は Array.prototype.slice.call
と同義。
"get".toFunction() == function (object, property) {
return object[property];
};
"set".toFunction() == function (object, property, value) {
return object[property] = value;
};
"call".toFunction() == function (object, method) {
// 実際は IE の DOM メソッドも呼び出せるようもう少し複雑
return object[method].apply(object, Array.slice(arguments, 2));
};
"invoke".toFunction() == function (func) {
return func.apply(null, Array.slice(arguments, 1));
};
"k".toFunction() == function (k) {
return k;
};
func.bind("call".bind(_1, "method", arg1, arg2, ...));
// 第 1 引数をオブジェクトとみなし、その method メソッドを
// 引数 arg1, arg2, ...で呼び出した結果を第 1 引数として渡す。
func.bind("get".bind(_2, _1));
// 第 2 引数をオブジェクトみなし、第 1 引数で示される名前の
// プロパティの値を第 1 引数として渡す。
これも「関数の変形」から例を引かせてもらうが、print という文字列を出力する関数があったとして、それを基に常に大文字で文字列を出力する関数を作るのには以下のようにする。
print.bind(String.toUpperCase.bind(_1)); // あるいは :
print.bind(Function.call.bind(String.prototype.toUpperCase, _1));
bind()
した関数を引数にするとその関数を実行した結果が引数に渡されるが、そうではなく bind()
した関数そのものを引数として渡したいときは literal()
を使う。
// function abs(x) {return x < 0 ? -x : x;}
var abs = "invoke".bind(
"?:".bind(
"<".bind(_1, 0),
"-".bind(_1).literal(),
"k".bind(_1).literal()
),
_1
);
ついでに target.addEventListener(type, func, false)
の代わりに func.listen(target, type)
と書けるように。これで beyond.js のサンプルは以下のように書きなおせる。
"invoke".bind(
"?:".bind(
"get".bind($("doStar"), "checked"),
"void".bind("set".bind(_1, "left", "+".bind(_2, "px")),
"set".bind(_1, "top", "+".bind(_3, "px")))
.delay(1000)
.bind($("star").style,
"+".bind(_1, "*".bind(_3, Math.sin.bind(_4))),
"+".bind(_2, "*".bind(_3, Math.cos.bind(_4))))
.bind("get".bind(_1, "pageX"), "get".bind(_1, "pageY"),
30, "/".bind(Date.now.bind(), 50))
.literal(),
"void".toFunction()
), _1
).listen(document, "mousemove");
やっぱり普通に書くより長い。というか元のサンプルより長くなった。
"call"
が示す関数では JavaScript の関数でなくても呼び出せるように細工してあるので「JavaScriptを関数的っぽくしたらモテるかもな」で示されていた例は以下のように書ける。
var addNewElement =
"call".bind("get".bind(document, "body"), "appendChild", _1)
.bind(_1, "set".bind(_1, "innerHTML", "Shibuya.js"))
.bind("call".bind(document, "createElement", _1));
フィボナッチ数を求める関数もかける。Memoize したら普通に書くよりかなり長めになったが。
// function fib(n)
// {
// return (n in fib) ? fib[n] :
// (n <= 1) ? fib[n] = 1 : fib[n] = fib(n - 1) + fib(n - 2);
// }
window.fib = "invoke".bind(
"?:".bind(
"in".bind(_1, "get".bind(window, "fib")),
"get".bind("get".bind(window, "fib"), _1).literal(),
"?:".bind(
"<=".bind(_1, 1),
"set".bind("get".bind(window, "fib"), _1, 1).literal(),
"set".bind("get".bind(window, "fib"), _1, "+".bind(
"call".bind(window, "fib", "-".bind(_1, 1)),
"call".bind(window, "fib", "-".bind(_1, 2))
)).literal()
)
), _1
);
とまあいろいろやったが何だかんだ言って beyond.js 及び「関数の合成」の劣化コピーに過ぎないわけで。結局のところ何が言いたいかといえば「beyond.js はすごい!」「『関数の合成』は一読しておくべし」といったところ。
コメント
トラックバック
このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2006/04/29/346605/tb
コメントをどうぞ
※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。
※投稿には管理者が設定した質問に答える必要があります。