Edited at

【Javascript】演算子をオーバーロードしたい話

More than 3 years have passed since last update.


動機

python にはnumpyというライブラリがあります。numpyでは行列の演算を以下のコードのように処理できます。

a = numpy.array([1,2,3,4,5])

b = numpy.array([1,2,3,4,5])
a * 3
=> [ 3 6 9 12 15]
a - b
=> [0 0 0 0 0]
a * b
=> [ 1 4 9 16 25]

でこんな感じの演算をJavascriptで実現したいなぁと思いました。


結論

いきなりですが結論から言うと無理に等しいです。そもそもJavascriptは演算子のオーバーロードを許していません。

ただvalueOfを継承することでイイ線まではいきます。

参考: http://www.2ality.com/2011/12/fake-operator-overloading.html


valueOfでいいところまで実装してみる

valueOfはJavascriptのObjectに実装されてるメソッドです。つまり全てのオブジェクトがvalueOfを継承しています。プリミティブには無い。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf

こんな事が出来ます。

var exampleOne = {valueOf: function(){return 1;}};

var exampleTwo = {valueOf: function(){return 2;}};

exampleOne + exampleTwo
=> 3

なんか行けそう!!なきがしますが、結局valueOfはプリミティブな値を返さないといけません。

ということは結局振り出しに戻ります。

var exampleOne = {

valueOf: function(){
return {
add: function ・・・   ← このように出来ない
};
}
};

なかなかうまく行きません。。。そこで次の方法を考えます


式全体をラップする

式全体をラップすることで 部分的 に扱うことが出来ます。

参考: https://github.com/rauschma/op_overload

var operands = []

fake_numpy.prototype = funciton(){
operands.push(this);
return 3;
}
function A(value){
 //__minus などの定義省略
var operator;
var ops = operands;
if(value === 0 && ops.length === 2){
operator = __minus
}else if (ops.length === 2 && value === 1) {
operator = __divide;
} else if (ops.length >= 2 && (value === 3 * ops.length)) {
operator = __add;
} else if (ops.length >= 2 && (value === Math.pow(3, ops.length))) {
operator = __multiply;
}else {
throw Error("失敗");
}
operands = [];
return operator.apply(this, ops);
}

valueOfが評価されるときに一旦オブジェクトを保存して、ラップした関数内で初めての計算を行います。

例えばvalue が式の項目の3の倍数の数であれば、元の式は恐らくすべて+なんだろうなぁと推測できます。

それらの数値から元の式の形を判定しそれら全てを特定の関数(例: __add など)に突っ込みます。

しかしこれでは複雑な式は扱えません。。。 複数の演算子があるともはや検知不可能だからです。。。。

var f1 = fake_numpy([1,2,3,4]);

var f2 = fake_numpy([1,2,3,4]);
var res = A(f1 + f2)
res.array
=> [2, 4, 6, 8]
var res = A(f1 + f2 - f1) ← これだけで破綻する
var res = A(f1 + f2 + f1) ← これならOK

ではどうするか・・・


構文解析器を使用する

こちらの方法はもはや純javascriptではありません。javascriptを抽象構文木に分解して、新たにjavascriptのコードを作ります。JSのコードを動的に変換するイメージ。

ちなみにPaper.jsではこの手法が取られています。

githubからソースを抽出します。

    var binaryOperators = {

// The hidden math methods are to be injected specifically, see below.
'+': '__add',
'-': '__subtract',
'*': '__multiply',
'/': '__divide',
'%': '__modulo',
// Use the real equals.
'==': 'equals',
'!=': 'equals'
};
//省略
function __$__(left, operator, right) {
var handler = binaryOperators[operator];
if (left && left[handler]) {
var res = left[handler](right);
return operator === '!=' ? !res : res;
}
switch (operator) {
case '+': return left + right;
case '-': return left - right;
case '*': return left * right;
case '/': return left / right;
case '%': return left % right;
case '==': return left == right;
case '!=': return left != right;
}
}

上記の様な関数を用意して、全演算子を__$()の関数に置き換えています。出力されるJSは

 1 + 2  __$__(1, 2)

の様な形になります。

ここまでしてしまえばなんとか出来そうです。


まとめ

純粋なJavascriptでは演算子をオーバーロード出来ません。

素直にnew numpy([1,2,3,4]).add(new numpy([1,2,3,4]))と書くしかないです。

特殊なケースを除いてJavascriptでnumpyの様な演算をすることを諦めましょう!!

参考:http://tkengo.github.io/blog/2015/06/30/operator-overload-in-javascript/