演算子オーバーライド
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。ライブラリ… ではないですが、ライブラリ作成で便利な機能。
今回は演算子オーバーライドです。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
Ruby の何でもオブジェクト方針は一貫した思想という意味で美しいとも思うのだが、1+1
の意味を変えるのは百害あって一利無しと思います。できても良いとは思うけど。ただし、クラス・オブジェクトに対しての演算子オーバーライドは有益です。
ということで、Kinx では クラス・オブジェクトに対してのみ演算子のオーバーライドを明示的にサポート します。String.+
とかも定義して使えますが、というか標準ライブラリの中で既に使ってますが、オーバーライドしたときの動作保証は いたしません。標準ライブラリで使っている=標準ライブラリの動作が変わる、なので本当に保証できませんので悪しからず...。
演算子オーバーライド
演算子オーバーライドとは
オブジェクトに対する演算子の挙動を上書きすること。演算子がクラスに属しているメソッドと考えれば「オーバーライド」となり、クラスに属さないと考えると「オーバーロード」となるイメージですが、ここでは Ruby っぽく演算子はクラス・オブジェクトへのメッセージでありクラスに属しているイメージで、そのクラス・メソッドを上書きする形を表現して「オーバーライド」で統一しておきます。
尚、C++ の演算子オーバーロードは演算子の多重定義です。クラス・メソッドではなく、同じ名前の関数(や演算子)でも、その引数の違いによって呼び出される関数が区別される機能のことです。
基本形
オーバーライド可能な演算子の種類は以下の通り。
-
==
,!=
,>
,>=
,<
,<=
,<=>
,<<
,>>
,+
,-
,*
,/
,%
,[]
,()
.
例として、+
演算子をオーバーライドしてみましょう。関数名を演算子名の +
とするだけです。他の演算子でも同じ。
class Sample(value_) {
@isSample = true;
@value = value_;
public +(rhs) {
if (rhs.isSample) {
return new Sample(value_ + rhs.value);
}
return new Sample(value_ + rhs);
}
}
rhs
として渡されるものは、適宜想定するコンテキストに合わせて場合分けして実装する必要があります。上記のように実装すると、以下のように使えます。
var s1 = new Sample(10);
var s2 = s1 + 100;
s1 += 1100;
System.println(s1.value); // => 1110
System.println(s2.value); // => 110
a += b
も内部的には a = a + b
に展開されるので正しく動作します。
尚、オブジェクトに対するメソッド呼び出しなので、以下のようにも書けます。
var s1 = new Sample(10);
var s2 = s1.+(100);
System.println(s2.value); // => 110
基本的に、[]
演算子と ()
演算子以外の右辺値を取る演算子は、同様の動作をします。
[]
演算子
[]
はインデックス要素的なアクセスを許可します。ただし、インデックスには整数(Integer)かオブジェクト、配列しか使えません。実数(Double)は動作しますが引数には整数(Integer)で渡ってきます。文字列は使えません(プロパティ・アクセスと同じであり、無限ループする可能性があるため)。
実際に、例えば Range
には実装されており、以下のようなアクセスが可能です。
System.println((2..10)[1]); // => 3
System.println(('b'..'z')[1]); // => 'c'
ただし内部で toArray() されるので、イテレーションは最後まで行われた後に応答されます。具体的には以下のように実装されています。
class Range(start_, end_, excludeEnd_) {
...
public [](rhs) {
if (!@array) {
@array = @toArray();
}
return @array[rhs];
}
}
[]
演算子もメソッド呼び出し風に書くと以下のようになります。
System.println((2..10).[](1)); // => 3
System.println(('b'..'z').[](1)); // => 'c'
()
演算子
()
演算子はオブジェクトに直接作用します。C++ のファンクタ(operator()
を定義したクラス)みたいなものです。例えば以下のようにクラス・インスタンスを関数のように見立てて直接 ()
演算子を適用できます。
class Functor {
public ()(...a) {
return System.println(a);
}
}
var f = new Functor();
f(1, 2, 3, 4, 5, 6, 7); // => [1, 2, 3, 4, 5, 6, 7]
メソッド呼び出し風に書くと以下と同じです。
var f = new Functor();
f.()(1, 2, 3, 4, 5, 6, 7); // => [1, 2, 3, 4, 5, 6, 7]
サンプル
スタック
スタック操作を <<
で行えるクラス Stack
を作ってみましょう。<<
で Push します。>>
でポップさせたいですが、引数に左辺値を渡せないので、無理矢理ですが ()
演算子で行きます。ちょっと中途半端ですが仕方ない。配列を Push すると末尾に全部追加するようにしておきます。
class Stack {
var stack_ = [];
public <<(rhs) {
if (rhs.isArray) {
stack_ += rhs;
} else {
stack_.push(rhs);
}
}
public ()() {
return stack_.pop();
}
public toString() {
return stack_.toString();
}
}
var s = new Stack();
s << 1;
s << 2;
s << 3;
s << 4;
s << [5, 6, 7, 8, 9, 10];
System.println(s);
var r = s();
System.println(s);
System.println(r);
実行してみましょう。
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
10
期待通りですね。
有理数クラス
別のサンプルとして四則演算のみをサポートした有理数クラスを作ってみましょう。符号処理は今回は省略します。基本形は以下の通り。
class Rational(n, d) {
@isRational = true;
}
まず初期化です。Rational オブジェクトのコピーも作れるようにしておきます。また、有理数の演算では最大公約数を求める機会も多いのでそのための private メソッドを用意します。また、確認しやすいように toString()
メソッドも用意しておきます。
class Rational(n, d) {
@isRational = true;
private gcd(a, b) {
if (a < b) {
[a, b] = [b, a];
}
var r;
while ((r = a % b) != 0) {
[a, b] = [b, r];
}
return b;
}
private initialize() {
if (d.isUndefined && n.isRatioal) {
d = n.denominator;
n = n.numerator;
}
var g = gcd(n, d);
@numerator = Integer.parseInt(n / g);
@denominator = Integer.parseInt(d / g);
}
public toString() {
return "%{@numerator}/%{@denominator}";
}
}
var r = new Rational(5, 10);
System.println("r = ", r); // => r = 1/2
では、早速四則演算を定義していきます。
ここではまず +
演算子の定義です。ただし、r1 + r2
で r1
が破壊されるのは直感的ではないので、新しいオブジェクトを返すようにします。また、直接破壊的に操作する別のメソッドを用意してきます。ついでにオブジェクトのクローンをつくる clone()
メソッドを作って活用しましょう。
class Rational(n, d) {
@isRational = true;
private gcd(a, b) {
if (a < b) {
[a, b] = [b, a];
}
var r;
while ((r = a % b) != 0) {
[a, b] = [b, r];
}
return b;
}
private initialize() {
if (d.isUndefined && n.isRational) {
d = n.denominator;
n = n.numerator;
}
var g = gcd(n, d);
@numerator = Integer.parseInt(n / g);
@denominator = Integer.parseInt(d / g);
}
public toString() {
return "%{@numerator}/%{@denominator}";
}
public clone() {
return new Rational(this);
}
public add(rhs) {
if (rhs.isInteger) {
return this + new Rational(rhs, 1);
} else if (rhs.isRational) {
var n = @numerator * rhs.denominator + @denominator * rhs.numerator;
var d = @denominator * rhs.denominator;
var g = gcd(n, d);
@numerator = Integer.parseInt(n / g);
@denominator = Integer.parseInt(d / g);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
return this;
}
public +(rhs) {
return @clone().add(rhs);
}
}
var r1 = new Rational(5, 10);
var r2 = new Rational(2, 6);
var r3 = r1 + r2;
var r4 = r1 + 2;
System.println("r1 = ", r1);
System.println("r2 = ", r2);
System.println("r1 + r2 = ", r3);
System.println("r1 + 2 = ", r4);
rhs
が Integer の場合、こんなこと(this + new Rational(rhs, 1)
のことね)する必要はないのですが、こんなこともできます、という意味での単なる例です。新たに Rational オブジェクトを作って再度 .+()
演算子が呼ばれて正しく計算されるというイメージです。
結果は以下のように表示されます。
r1 = 1/2
r2 = 1/3
r1 + r2 = 5/6
r1 + 2 = 5/2
では、四則演算全て定義してみましょう。先ほどの無駄っぽいところ(this + new Rational(rhs, 1)
のことね)も今回は変えておきます。
class Rational(n, d) {
@isRational = true;
private gcd(a, b) {
if (a < b) {
[a, b] = [b, a];
}
var r;
while ((r = a % b) != 0) {
[a, b] = [b, r];
}
return b;
}
private makeValue(n, d) {
var g = gcd(n, d);
@numerator = Integer.parseInt(n / g);
@denominator = Integer.parseInt(d / g);
return this;
}
private initialize() {
if (d.isUndefined && n.isRational) {
d = n.denominator;
n = n.numerator;
}
makeValue(n, d);
}
public toString() {
return "%{@numerator}/%{@denominator}";
}
public clone() {
return new Rational(this);
}
public add(rhs) {
if (rhs.isInteger) {
return makeValue(@numerator + @denominator * rhs, @denominator);
} else if (rhs.isRational) {
return makeValue(@numerator * rhs.denominator + @denominator * rhs.numerator,
@denominator * rhs.denominator);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
}
public sub(rhs) {
if (rhs.isInteger) {
return makeValue(@numerator - @denominator * rhs, @denominator);
} else if (rhs.isRational) {
return makeValue(@numerator * rhs.denominator - @denominator * rhs.numerator,
@denominator * rhs.denominator);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
}
public mul(rhs) {
if (rhs.isInteger) {
return makeValue(@numerator * rhs, @denominator);
} else if (rhs.isRational) {
return makeValue(@numerator * rhs.numerator,
@denominator * rhs.denominator);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
}
public div(rhs) {
if (rhs.isInteger) {
return makeValue(@numerator, @denominator * rhs);
} else if (rhs.isRational) {
return makeValue(@numerator * rhs.denominator,
@denominator * rhs.numerator);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
}
public +(rhs) {
return @clone().add(rhs);
}
public -(rhs) {
return @clone().sub(rhs);
}
public *(rhs) {
return @clone().mul(rhs);
}
public /(rhs) {
return @clone().div(rhs);
}
}
var r1 = new Rational(5, 10);
var r2 = new Rational(2, 6);
var r3 = r1 + r2;
var r4 = r1 - r2;
var r5 = r1 * r2;
var r6 = r1 / r2;
System.println("r1 = ", r1);
System.println("r2 = ", r2);
System.println("r1 + r2 = ", r3);
System.println("r1 - r2 = ", r4);
System.println("r1 * r2 = ", r5);
System.println("r1 / r2 = ", r6);
結果。
r1 = 1/2
r2 = 1/3
r1 + r2 = 5/6
r1 - r2 = 1/6
r1 * r2 = 1/6
r1 / r2 = 3/2
おわりに
上記有理数クラスに符号処理はありませんが、簡単なので省略します。もしかしたらどこかで正式に有理数クラスをサポートするかもしれません。その時は本気出して色々メソッドを定義してみます(以下が参考)。
ではまた次回。
clone()
についての補足
clone()
は通常、上記のように new 自分自身のクラス(this)
で定義することが多いですが、以下のようにすると新たに作ったオブジェクトが過去のオブジェクトへの参照を持ち続けてしまうので、新たに作成したオブジェクトが死なない限りその元オブジェクトも GC で解放されないといったことになり、リークする可能性があります。
class A(arg_) {
@isA = true;
var a_;
private initialize() {
a_ = arg_.isA ? arg_.get() : 0;
// arg_ = null が無いと参照を持ち続けてしまう
}
public get() {
return a_;
}
public clone() {
return new A(this);
}
/* ... */
}
上記コメントのように初期化後に arg_ = null
とすれば OK ですが、それ以外にも、arg_
と a_
を共用させる方法もあります(上記 Rational クラスはそれに近い方法)。例えば以下のような感じ。
class A(a_) {
@isA = true;
private initialize() {
a_ = a_.isA ? a_.get() : 0;
}
public get() {
return a_;
}
public clone() {
return new A(this);
}
/* ... */
}
こうすることで、新たなオブジェクトから過去のオブジェクトへの参照が切れるので、しかるべき時にきちんと GC が働くようになります。
では、また。