C#で自作演算子作ると、どうも気持ち悪い

さて、昨日、自作演算子あったらおもしろいなというお話をしました。
それで電車の中でいろいろ考えてたのだが、2つ有益な方法があると思う。
ところで自作演算子を言語レベルで実装していたものにFORTRANがあるらしい。
それで調べると、自作演算子は .dot. のようにピリオドで挟んだものを使うらしい。
まぁこの方法、当然だがC#では使えんな。
僕の考えでは演算子の機能を提供する演算子クラスってのを作るところから始まると思う。


まず、考えた方法の1つは明示的な型変換と二項演算子を組み合わせる方法。
型変換もれっきとした単項演算子ですよ。結合順位ではf(x)などと同じ順位。
明示的な型変換は (double)i のように、変換先の型を書く。
これで自作した演算子クラスの型に変換させる。これとの二項演算子を適当にオーバーロードすると。
今回作ったのはvectorクラス(自作)同士の内積のvdot、外積のvcross、double型同士のべき乗のpowの3つの演算子クラス。
使用例を示そうか。

vector v1=new vector(1, 2, 3);
vector v2=new vector(-4, 5, -6);
Console.WriteLine("{0}・{1}={2}", v1, v2, v1 %(vdot) v2);
Console.WriteLine("{0}×{1}={2}", v1, v2, v1 %(vcross) v2);
Console.WriteLine("10^0.3010^2={0}", 10 %(pow) 0.3010 %(pow) 2);

これでわかってくれたらうれしいけど。
使い方は v1 %(vdot) v2 のように書くだけ。見た目はシンプル。けどやってる内容は結構ひどい。
結合順序は関数が一番始め、その次が型変換や単項演算子、まず(vdot)v2を考える。
型変換の演算子では new vdot(v2) のようなことを書いてある。
このvdotクラスのインスタンスは2つ目の値を収納するわけだ。in2ってメンバ変数に入れてる。
型変換されたものをmyvdotとすると、v1 % myvdotのような計算式に置き換えられたと。
本来%は剰余の計算をするものだけど、そんなの関係ない。%演算子では v1・myvdot.in2 のような計算をしている。
だから結論はv1とv2の内積となると。非常に気持ち悪いやり方だな。
vcrossもpowも同様。powは読みやすいように10 %(pow) 0.3010 %(pow) 2とスペースを入れてるが、
実際は10 % (pow)0.3010 % (pow)2と言った方がいい。けど上のようにスペースを入れるとそれっぽく見える。
ちなみに*/%の乗除の演算子は結合順位は単項演算子の次。二項演算子としては一番上ですね。
あえてそういうのを選んだわけだけど。初めは^(XOR)にしようと思ったんだけどね。
2*v1 %(pow) v2 は (2*v1)%((pow)v2) と解するけど、 v1 %(pow) v2*2 は v1%((pow)v2)*2と解するという違いがある。


powクラスのソースコードを貼る。

class pow {
  public static double operate(double in1, double in2) { return Math.Pow(in1, in2); }
  private double in2;
  private pow(double in2_0) { in2=in2_0; }
  public static explicit operator pow(double in2_0) { return new pow(in2_0); }
  public static double operator%(double in1, pow op) { return operate(in1, op.in2); }
}

省スペースしたぞ。まず静的メソッドoperateを定めてるけど、これは演算子の定義。
なんでこうしたのかというと上みたいな使い方じゃなくて、pow.operate(a,p)のようにも使えるようにするため。余分な機能だ。
in2を登録することしかしないコンストラクタだが、アクセス修飾子を見るとprivateだ。クラス外でインスタンスは作れない。
まぁこれを制限した意味はあまりない気がしてきたけど、僕が考える限りでは必要ないと思うから。
次に静的メソッド、implicit operator powを定義しているが、これは明示的(implicit)なpowへの型変換の演算子。
この中で2つ目の値を渡してインスタンスを動かしています。
それで最後に%で、1つ目の値と、2つ目の値が登録されたpowのインスタンスの計算を定めてあると。
そんなわけで、なるほど確かに動くというわけです。
悪趣味なやり方だがおもしろいのは確かだ。


もう1つの方法は、二項演算子を2回使う方法。
僕が自作演算子と調べて出てきたのはC++でこのやり方をしてる例だったな。
ところで二項演算子はインスタンスとインスタンスの計算をするもの、そりゃそうだ。
だから演算子インスタンスが必要。クラスのインスタンスを作らなければならない。
しかし、それを逆手に取るとおもしろいことができた。即席演算子を作れてしまう。

myop dot=new myop(
  delegate(vector in1, vector in2) {
    return in1.x*in2.x+in1.y*in2.y+in1.z*in2.z;
  });
Console.WriteLine("{0}・{1}={2}", v1, v2, v1 %dot% v2);

気持ち悪いことこの上ないというのはこのことだろう。
Genericを多用してるから見にくい。そのうえC# 2.0の範囲で書いてるので、C# 3.0ほどスマートに書けない。
まず演算子メソッドを作る。<>内には値1の型・値2の型・結果の型の順番で書く。
それで引数に値1の型・値2の型、返り値に結果の型となるデリゲートを登録する。
このデリゲートの型としてmyoperateを用意してるけど、C# 3.0ではFuncを使えばいいでしょう。
ここではその場で作っている。そう、これが即席演算子ということ。
もちろんこうやって作った演算子インスタンスを返す静的プロパティとして用意しておいてもよかったんだけどね。
それでv1 %dot% v2と、演算子インスタンスの変数名を%で囲んで書くだけ。
さて、これは結合順序からわかると思うけど、(v1%dot)%dotと計算される。
さっきも出てきた%だが、剰余の演算子、だけどそんなの関係ない。
%演算子でT1型とmyop型を計算すると、relay型にされる。
このrelay型、登録されたデリゲートと、T1型の値を納めるクラス。
%演算子でrelay型とT2型を計算すると、デリゲートで定めた計算をして、答えのTr型が返ると。
おもしろい使い方だと思ったのが、

myop pow=new myop(Math.Pow);
Console.WriteLine("10^0.3010^2={0}", 10 %pow% 0.3010 %pow% 2);

既存の関数を登録すると。これはなかなか豪快なやり方だ。
確かに答えは出る。こんな風に二項演算子にしたい静的メソッドを登録するというのはありかもしれない。


ただ、クラスが難解で…

public delegate Tr myoperate(T1 in1, T2 in2);
public class myop {
  private myoperate operate;
  public myop(myoperate operate0) { operate=operate0; }
  public static relay operator%(T1 in1, myop op0) {
    return new relay(op0.operate, in1);
  }
}
public class relay {
  private T1 in1;
  private myoperate operate;
  internal relay(myoperate operate0, T1 in1_0) {
    operate=operate0;
    in1=in1_0;
  }
  public static Tr operator%(relay myr, T2 in2) {
    return myr.operate(myr.in1, in2);
  }
}

さっきの例は1つの演算子に対して1つクラスを作ればいい。わりと理解しやすくていいですね。
今回は演算子いくつでもいいけど、デリゲート1つ・クラス2つも必要。演算子の定義は別途と、まぁそういうわけです。
1つ目の%演算子はmyop<>クラスに、2つ目の%演算子はrelay<>クラスにあります。こんな風に分かれてるのはわかりにくい。
relay<>クラスのコンストラクタはinternalになってるから、同じアセンブリ内では作れる。
事実myop<>クラスの%演算子の定義のときに使ってるからね。
まぁinternalはpublicだと思ってもかまわない程度のアクセス権だけどな。C#独特のアクセス権だ。


自作演算子を作ったところで書き方は簡単でも、実際の動作はすごい複雑。
そこが難儀なところだ。
こんな自作演算子がメリットになるのは10 %(pow) 0.3010 %(pow) 2のような計算だろう。
もし、これをPowメソッドで表したならMath.Pow(Math.Pow(10 , 0.3010) , 2)のように書かないといけない。
こんな風に厄介な静的メソッドを、スマートに表すための方法ですよ。