1. Qiita
  2. 投稿
  3. JavaScript

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

  • 3
    いいね
  • 0
    コメント

動機

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/