けんごのお屋敷

2015-06-30

果たして JavaScript で演算子オーバーロードは可能なのか

果たして僕の他に誰が JavaScript で演算子オーバーロードをやりたいと思っている人がいるだろうか。

世の中のニーズは置いといて、少なくとも自分の中では欲しいと思うことがあった。JavaScript で自作のオブジェクトを作った際に、それらのオブジェクト同士の演算を定義したいことがある。Ruby や C# のもつ演算子オーバーロードの言語機能を羨ましく思いながら JavaScript でもそんなことができないのか調べて試行錯誤した結果をまとめてみようと思う。

はじまり

完全な趣味で JavaScript で行列やベクトルの演算ができる mx.js というものを作っている。mx.js では MatrixVector というオブジェクトが出てくるが、それら同士の足し算や引き算、掛け算を演算子を使ってやりたい、というところがモチベーションとなる。

JavaScript は言語機能として演算子オーバーロードを持っていないので、行列やベクトルのオブジェクト同士の演算を定義しようとすると普通はメソッドを定義することになる。そう、こういう風に。

var m1 = Matrix.create([ ... ]);
var m2 = Matrix.create([ ... ]);

// m1とm2の各要素を足し合わせる
m1.add(m2);

これは一般的な、誰にでも思いつく実装でしょう。そうではなくて、もっと直感的に、まるで数値を扱うかのようにこんな風に書ければとても嬉しい。

// m1とm2の各要素を足し合わせる
var m3 = m1 + m2;

これを目指して試行錯誤を繰り返していく。

オブジェクトからプリミティブへの変換

とはいえ、演算子オーバーロードがない JavaScript において、そんなことどうやって実現すればいいのか。実は JavaScript ではオブジェクトに対して +- のような二項演算子を適用した場合、左オペランドから右オペランドの順に valueOf というオブジェクトをプリミティブ型に変換するためのメソッドが暗黙的に呼び出される。つまりはこういうこと。

Matrix.prototype.valueOf = function() {
  return this.rows + " x " + this.cols + "のMatrix。";
};

var m1 = Matrix.eye(3); // 3x3の単位行列
var m2 = Matrix.eye(4); // 4x4の単位行列

var m3 = m1 + m2;

これを実行すると m3 には以下のテキストが入っている。

"3 x 3のMatrix。4 x 4のMatrix。"

m1 と m2 が valueOf によって一旦文字列に変換された後に + 演算子が適用されて、各文字列が連結されたものが返るようになっている。なるほど、これを応用すればなんとなく演算子オーバーロードのようなことができそうな気がする。ちょっと考えてみよう。

つまり、演算子の左側にあるオペランドの valueOf が最初に呼ばれて、その次に右側にあるオペランドの valueOf が呼ばれるのだった。ということは

  1. 1 回目に呼ばれる valueOf で this の値をどこか外部の変数に保存する。
  2. 2 回目に呼ばれた valueOf で this と、前に保存していた値とで足し算をしてその結果を返す。

という風にすれば少なくとも足し算においてはなんとなく実現できそうな気がする。valueOf の実装をこんな風にしてみたらどうだろう?

var tmp;
Matrix.prototype.valueOf = function() {
  if (tmp === undefined) {
    // 左側オペランドの呼び出し。
    // 後で参照するために外部の変数に値を保存して、戻り値は適当な値を返す。
    tmp = this;
    return 0;
  } else {
    // 右側オペランドの呼び出し。
    // thisの値と外部変数の値を足し算してそれを返す。
    var result = this.add(tmp);
    tmp = undefined;
    return result;
  }
};

なんとなくイケそうな気がするけど、実際はこの実装ではやりたいことを実現できない。たとえばこの実装で m1 + m2 を計算するということは、以下のような式を計算するということになる。

var m3 = 0 + m2.add(m1);

m1 での valueOf が 0 を、m2 での valueOf が this.add(tmp) を返しているので、こういう式になる。この式だけみると結局また + 演算子が登場しているので、うまく計算できるはずがない。さらに、valueOf はプリミティブ型への変換のためのメソッドなので、戻り値はプリミティブしか返せないことになっている。オブジェクトを返してはいけない。先の実装では右側のオペランドで Matrix オブジェクトを返すようにしているけど、そもそも Matrix オブジェクトは返ってこない。

ここまできて、盛大なちゃぶ台返しだ。

関数でラップしてみる

しかし valueOf を使って、外部変数にオペランドの値を保存しておくというアイデアは捨てがたい。もう少しなにか工夫すればできるようになる気がする。そこで、関数を使って実現できないかどうかを試してみる。どういうことかというと

var tmp = [];
Matrix.prototype.valueOf = function() {
  tmp[tmp.length] = this;
  return 0;
};

function add(v) {
  var result = tmp[0].add(tmp[1]);
  tmp = [];
  return result;
}

var m3 = add(m1 + m2);

やった!これだとうまくいく!

少し掘り下げて説明すると、JavaScript での関数呼び出しはまず最初に引数の部分から評価される。つまり add(m1 + m2) という式は m1 + m2 から評価されることになり、まずは valueOf が呼び出される。valueOf では Matrix のオブジェクトを外部の変数に保存して、戻り値は使われることがないので適当な値を返している。左オペランドと右オペランドでそれぞれ呼び出されるので結果的には tmp には 2 つの Matrix オブジェクトが入っていることになる。その後に add メソッドが呼び出され tmp の中身を参照して足し算の結果を返す、というやり方になっている。

うーん、でもこんなものでは到底満足できない。

  1. そもそも書き方を簡素化したいために演算子オーバーロードを実装しているのに、これだととても冗長である。足し算をするのに add+ という同じような意味のものが 2 回も出てくる。
  2. 足し算以外のものが実装できていない。引き算、掛け算(掛け算の場合は要素同士の掛け算と行列の積の 2 種類)、割り算も実装したい。

もう少し工夫してみよう。

1 の問題についてはとりあえず関数を 1 文字にすれば短くできそうだ。2 の問題については、さっきは捨てていた valueOf の戻り値を使ってやってみる。0 を返すのではなくて、演算子の種類を特定できるような何かの値を返してあげればよいということになる。たとえば 3 を返してみるとどうなるだろう。

Matrix.prototype.valueOf = function() {
  tmp[tmp.length] = this;
  return 3;
};

var m3 = A(m1 + m2); // A(3 + 3)
var m4 = A(m1 - m2); // A(3 - 3)
var m5 = A(m1 * m2); // A(3 * 3)
var m6 = A(m1 / m2); // A(3 / 3)

関数 A に渡される引数部分は上記のコメントのように、それぞれ

  • 足し算の場合は 3 + 3 となり 6 が渡される
  • 足し算の場合は 3 - 3 となり 0 が渡される
  • 足し算の場合は 3 * 3 となり 9 が渡される
  • 足し算の場合は 3 / 3 となり 1 が渡される

となるので、これを元に A を以下のように実装すればよい。

function A(v) {
  var result;
  switch (v) {
    case: 6 // 足し算
      result = tmp[0].add(tmp[1]); break;
    case: 0 // 引き算
      result = tmp[0].sub(tmp[1]); break;
    case: 9 // 掛け算
      result = tmp[0].mul(tmp[1]); break;
    case: 1 // 割り算
      result = tmp[0].div(tmp[1]); break;
  }
  tmp = [];
  return result;
}

これで、足し算、引き算、掛け算、割り算、を A というラップ関数の力を借りて演算子を定義することができた!

構文解析する

でも、理想を言うとまだ足りない点がある。あ、行列の積の演算子が定義できてない?残念、演算子の種類の問題ではない。問題なのは A を使っての演算子による計算は最大 2 つしか項を取れないことにある。つまり、こういうことができない。

var mx = A(m1 * (m2 + m3) - m4);

項が 2 つ以上に増えるだけではなく、カッコによる優先順位の変更まである。こうなってくると A に渡ってくる引数の値から演算子の種類を推測することはもはや不可能になる。ここまで来ると JavaScript 標準の機能だけだとどうにもできない気がする。じゃぁどうすればいいんだろう。

実は最終手段として 構文解析する という方法が残っている。

少し話がそれるが paper.js というものを知っているだろうか。これは HTML5 の Canvas 上で動作する JavaScript で書かれたベクターグラフィックスライブラリである。この paper.js では、オリジナルの PaperScript という JavaScript ライクな言語を使って書いていくことになるが、その中で完全な演算子のオーバーロードを実現している。どうやっているのだろうと中身を覗いてみれば、なんと JavaScript の構文解析器を使ってソースコードを構文解析して AST を生成し、その中でカッコの優先順位や演算子の種類を特定して、計算を実行している。

なるほど、そこまですればもう何でもできそうだ。話を戻して m1 * (m2 + m3) - m4 という式も、いったん JavaScript の構文解析器に通して AST を取得できれば、なんとかできそうな気がする。ではさっそくやってみよう。と、思ったものの、構文解析をするためには m1 * (m2 + m3) - m4 という部分を文字列として取得できている必要がある。さて、困った。どうやってこの式を 文字列として 取得したものか。

少し考えてみよう。JavaScript では関数オブジェクトに対して toString メソッドを呼び出した場合、その関数の中身を文字列として返してくれる。たとえば

function X() {
  return m1 * (m2 + m3) - m4;
}
var string = X.toString();

とすると、上記の string 変数には以下のような 文字列 が返ってくる。ちなみに無名関数であっても同じように文字列として取り出せる。

"function X() {
  return m1 * (m2 + m3) - m4;
}"

なんとなくゴールに近づいてきている。つまり A に無名関数を渡してその中に式を記述すれば A 内で文字列表現として取得できる。これでどうだろう?

var mx = A(function() { m1 * (m2 + m3) - m4 });

function A(v) {
  if (typeof v === 'function') {
    var string = v.toString();

    // 構文解析してASTを取得後に適切な計算をして値を返す
    ...
  } else {
    // vの値によって足し算、引き算、掛け算、割り算を実装
    ...
  }
}

JavaScript で書かれた JavaScript の構文解析器に acorn というものがあるので、構文解析にはそれを使うと良い。詳細な実装を書くと長くなるのでここでは割愛するが、このように実装することで 3 つ以上の項があっても、カッコで優先順位が変更されていても、正しく計算結果を戻してあげることができる。すごい!できた!

まとめ

本当に理想を求めるのであれば、もちろん単純にこんな風に書けるのが一番いい。

var mx = m1 * (m2 + m3) - m4;

でもこんなことはできないので、なんとか頑張って以下のように記述すれば演算子のオーバーロードのようなことができるようになった。

// 項が2つの場合
var mx = A(m1 + m2);

// 項が3つ以上の場合
var mx = A(function() { m1 * (m2 + m3) - m4 });

それでも A という余計なワードがついてしまうのは避けられないし、おまけに 3 項以上の計算に関しては function という長いキーワードまでついてくるし、構文解析を間に挟んでいるのですごく重い。この手法を使うかどうかはケースバイケースで、処理速度が問題になるような箇所では使えないと思う。しかしそうじゃない場合は、全部メソッドで記述するよりかはいくらか書きやすくなってるのではないかな。全部メソッドで書くとするとこんな感じだろうか。

var mx = m1.add(m2);
var mx = m1.mul(m2.add(m3)).sub(m4);

なんだ、やっぱり演算子を使って書けたほうが断然わかりやすいじゃん!

というわけで、自分が試行錯誤してきた JavaScript による演算子のオーバーロード手法をまとめてみた。理想にはたどり着けなかったけど、それに近いところまでは辿りつけたので、ヨシとしよう。

  • このエントリーをはてなブックマークに追加