js lover's

女子小学生が好きなわけじゃないよ…

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が呼ばれ、さらにその返り値の10objの値となっている。

このvalueOfを定義しておくとJavaScriptのほとんどの演算子の呼び出し時に任意の関数を実行することができる。そのため、演算子オーバーロードのように見せるかけることができる、というわけだ。

やってみる

では、実際にこのvalueOfを利用(悪用とも言う)して何か作ってみようと思う。

今回作るのは、関数の合成演算子だ。関数の合成というのは数学と同じで、fgという二つの関数があるとき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についてなど)を簡略化のために省略したので、一部正確でないところもある。それについてはここなどを参照してほしい。

JavaScript 第6版

JavaScript 第6版

パーフェクトJavaScript (PERFECT SERIES 4)

パーフェクトJavaScript (PERFECT SERIES 4)