JavaScriptは演算子オーバーロードの夢を見るか
JavaScriptでは演算子を定義したりオーバーロードしたりすることはできない。これはだれでも知っているようなことだろう。
だが、過去には演算子オーバーロードのようなことを可能にしたライブラリも存在した。
def.js
このコードを見て欲しい。
//https://github.com/tobeytailor/def.jsより def ("Person") ({ init: function(name){ this.name = name; }, speak: function(text){ alert(text || "Hi, my name is " + this.name); } }); def ("Ninja") << Person ({ init: function(name){ this._super(); }, kick: function(){ this.speak("I kick u!"); } }); var ninjy = new Ninja("JDD"); ninjy.speak(); ninjy.kick();
これはdef.jsという小さなライブラリの例で、まずPerson
というクラスを定義し、次にNinja
というクラスにそれを継承させている。Rubyを知っている人なら、何となく理解できるのではないだろうか。
そしてdef ("Ninja") << Person ( {
という部分はまるで演算子をオーバーロードしているようだ。
これはどういうことなのだろう?
その鍵はvalueOf
が握っている。
valueOf
valueOf
というメソッドは、JavaScriptにおいて特別なメソッドの一つだ。Pythonで言うならば__
で囲まれた特殊メソッドのようなものだと考えてもらえればいい。
ということは、このvalueOf
はあるタイミングで暗黙的に呼ばれることになる。そして、あるタイミングとはオブジェクトをプリミティブな値(数値、文字列、真偽値)に変換するときだ。
これを確かめてみる。
var obj = { valueOf: function () { console.log('call valueOf'); return 10; }, }; console.log(obj + 20);
実行すると'call valueOf'
が表示されたあと30
が表示されるだろう。つまり、obj + 20
というコードの中でvalueOf
が呼ばれ、さらにその返り値の10
がobj
の値となっている。
このvalueOf
を定義しておくとJavaScriptのほとんどの演算子の呼び出し時に任意の関数を実行することができる。そのため、演算子オーバーロードのように見せるかけることができる、というわけだ。
やってみる
では、実際にこのvalueOf
を利用(悪用とも言う)して何か作ってみようと思う。
今回作るのは、関数の合成演算子だ。関数の合成というのは数学と同じで、f
とg
という二つの関数があるときf(g(x))
のように実行する関数を作ることだ。また、数学ではf・gのように点を関数合成の記号として使うので、f * g
のようにして演算子オーバーロードするイメージでいく。
まず初めに、演算子のオーバーロードではない普通の関数として定義してみる。
function compose(f, g) { return function(x) { return f(g(x)); }; } function f(x) { return x + 1; } function g(x) { return x * 2; } console.log(compose(f, g)(20)); //=> 41
当然これはただの関数で演算子には成り得ない。
これをvalueOf
を使って演算子オーバーロードしたように関数を合成できるようにするにはどうしたら良いだろうか?
方法としては、Function
オブジェクトのvalueOf
メソッドを弄ればよいことになる。そうすれば、f
が関数でf * g
のようなコードがあるときにvalueOf
メソッドが呼ばれることになる。
ここで一つ注意しなければいけないのが、valueOf
メソッドがプリミティブな値を返さなければいけないということだ。つまり、f * g
はオブジェクトである関数を直接返すことはできない。
となると積んでしまったように思えるが、これには単に関数で包み込むなどすればそこまで問題ではない。
それでは、実際に演算子オーバーロードのように見せかけるコードを書いてみる。
function id(x) { return x; } Object.defineProperty(global || window ||this, 'compose', { enumerable: false, get: function initCompose() { var fs = [], valueOf_ = Function.prototype.valueOf; Function.prototype.valueOf = function () { fs.push(this); return 1; }; return function () { Function.prototype.valueOf = valueOf_; return fs.reduceRight(function(g, f) { return function (x) { return f(g(x)); }; }, id); }; }, }); function f(x) { return x + 1; } function g(x) { return x * 2; } console.log(compose.call(f * g)(20)); //=> 41
少しコードを解説すると、まずグローバルオブジェクトにcompose
というプロパティを定義している。このcompose
はゲッターを持っていて、そこで演算子オーバーロードのための準備をしている。
その準備とは主に、Function.prototype.valueOf
メソッドを書き換えることだ。関数が数値に変換されようとしたら、その関数を保存して適当な値を返している。
次に、compose
は実際に関数を合成するための関数を値として返している。保存した関数を右畳込みして合成する。また、予めFunction.prototype.valueOf
をバックアップしておいて、ここで戻すことで全体への影響を最小限にしている。
そして、compose.call(f *g)
というのがこのcompose
の用例だ。f * g
という式で関数が合成されているだろう。このとき、compose.call
としてcompose
を呼びださなければいけないのはJavaScriptが引数の方を先に評価するからだ。
この演算子オーバーロードのようなcompose
の便利なところはさっきの関数のと違って二つ以上も簡単に合成できるところだ。
例えばcompose.call(g * f * g)
と書き換えれば返ってくる値は82
になるだろう。
結びに
この演算子オーバーロードのように見せかける方法は、主に言語内DSLを構築するときに役に立つと思う。演算子の直感的な表記方法は魅力的だ。
また、いくつかの説明(valueOfについてなど)を簡略化のために省略したので、一部正確でないところもある。それについてはここなどを参照してほしい。
- 作者: David Flanagan,村上列
- 出版社/メーカー: オライリージャパン
- 発売日: 2012/08/10
- メディア: 大型本
- 購入: 12人 クリック: 252回
- この商品を含むブログ (14件) を見る
パーフェクトJavaScript (PERFECT SERIES 4)
- 作者: 井上誠一郎,土江拓郎,浜辺将太
- 出版社/メーカー: 技術評論社
- 発売日: 2011/09/23
- メディア: 大型本
- 購入: 24人 クリック: 588回
- この商品を含むブログ (12件) を見る