ゼロからはじめるスクリプト言語製作: 浮動小数の演算を多彩に(10日目)
前回ようやく、スクリプト言語の中で整数の算術演算(四則演算と剰余)に対応することができた。今回も算術演算について取り組むが、今回は整数だけでなく浮動小数にも対応していく。
ということで今回の実装ゴールを以下のように課して、実装を進めていくことにしよう。
<使用例>
(+ 1 2 3) ===> 6
(+ .1 .2 .3) ===> 0.6
(+ 1 2 (+ .1 .2)) ===> 3.3
<使用例>
(- 1. 2 3) ===> -4
(* 1.6 1.6) ===> 2.56
(/ 2.0 2 2) ===> 0.5
(% 5 1.5) ===> 0.5
floatv 同士の算術演算
Type.cs の floatv クラスに floatv 型同士の演算に使うコードを準備しておくことにする。前回 numberv 型に対して行った変更と大差はない。
※ 行頭「+」箇所は行追加。
public class floatv : atomv<double>
{
:
+ public floatv add(floatv other) => new floatv(_val + other._val);
+ public floatv sub(floatv other) => new floatv(_val - other._val);
+ public floatv mul(floatv other) => new floatv(_val * other._val);
+ public floatv div(floatv other) => new floatv(_val / other._val);
+ public floatv mod(floatv other) => new floatv(_val % other._val);
}
引数すべてに対する演算(の見直し)
算術演算に指定できる引数を2つに限定しない仕様としたため、すべての引数を逐次処理していく関数として Core.reduce<T1, TV>() を実装し、そのコードは↓以下のようになっていた。
public delegate void reducer1<T1, TV>(T1 other, ref TV val);
public static expr reduce<T1, TV>(expr args, ref TV value, reducer1<T1, TV> func) where T1 : expr where TV : expr
{
cell? cur = args.astype<cell>();
if (cur == null) return value;
T1 other = cur.element().eval().astype<T1>() ?? throw new Exception("invalid element type");
func(other, ref value);
return reduce(cur.next(), ref value, func);
}
S 式から引数となる要素を取り出し(cur.element() の呼び出し)、引数を評価して(変数 other)、それまでの演算結果に反映する操作を行い(func() の呼び出し)、それを繰り返している(reduce() の再帰呼び出し)。
整数の算術演算においてジェネリック引数 T1 と TV は、共に numberv 型であることを想定していた。
今後浮動小数の演算に対応したり 比較演算を導入するためにと思って、わざわざ Core.reduce() をジェネリック化しておいたりもしたが、結果的に見ればジェネリックを採用する選択は妥当ではなかった。
本来ジェネリックは「コンパイルの段階で、複数の型を同じロジックで扱いたい」ときに活用すべきものではないだろうか。今回はそうではなく、引数を評価した結果が numberv 型と floatv 型のどちらになるかは事前には判然とせず、結果の反映(func() の呼び出し)を Func<numberv, numberv, numberv> 型と Func<floatv, floatv, floatv> 型のどちらかにするのか動的に使い分ける必要があるのだ。
これを踏まえると、Core.reduce() のコードは↓以下のようになった。
public static expr reduce(expr args, expr value, Func<expr, expr, expr> func)
{
cell? cur = args.astype<cell>();
if (cur is null) return value;
expr other = cur.element().eval();
value = func(value, other);
return reduce(cur.next(), value, func);
}
numberv 型と floatv 型を区別せずに扱うために、引数の value は expr 型となり、func は Func<numberv, numberv, numberv> 型でも Func<floatv, floatv, floatv> 型でもなく Func<expr, expr, expr>型となった。
まずは加算
Core.reduce() の第3引数に渡す func は、その渡されてくる引数の型について少し寛容にふるまう必要がある。まずは Core.add() の変更箇所を紹介していこう。
※ 行頭「+」箇所は行追加。行頭「-」箇所は行削除。
public static expr add(expr args)
{
numberv value = new numberv(0);
- return reduce(args, ref value, (numberv other, ref numberv val) => val = val.add(other));
+ return reduce(args, value, (expr left, expr right) => left.add(right));
}
評価結果の反映を行う func は expr.add() を呼び出す形に変更されており、こうすることで変数 left が numberv 型であれば numberv.add() が、floatv 型であれば floatv.add() がそれぞれ呼び出される、ということになった。
expr 型を基底クラス、numberv 型と floatv 型を派生クラスとするポリモーフィズムを実現するために、Type.cs には↓以下のような変更を追加した。
※ 行頭「+」箇所は行追加。
public class expr
{
public T? astype<T>() where T : expr => this as T;
+ public T cast<T>() where T : expr => astype<T>() ?? throw new Exception("invalid element type");
:
+ public virtual expr add(expr other) => throw new Exception("invalid element type");
+ public virtual expr sub(expr other) => throw new Exception("invalid element type");
+ public virtual expr mul(expr other) => throw new Exception("invalid element type");
+ public virtual expr div(expr other) => throw new Exception("invalid element type");
+ public virtual expr mod(expr other) => throw new Exception("invalid element type");
:
}
:
public class numberv : atomv<long>
{
:
+ private expr binaryOps(expr other, Func<numberv, expr> func1, Func<floatv, expr, expr> func2)
+ {
+ numberv? otherv = other.astype<numberv>();
+ return (otherv is null) ? func2(this!, other) : func1(otherv);
+ }
:
+ public override expr add(expr other) => binaryOps(other, add, (floatv val, expr other_) => val.add(other_));
:
}
:
public class floatv : atomv<double>
{
:
+ public static floatv from(expr v) => v.astype<floatv>() ?? (floatv)v.cast<numberv>();
:
+ public override expr add(expr other) => add(floatv.from(other));
:
}
numberv 型や floatv 型以外の型(stringv 型や boolv 型など)には add() の override を含めていないため、それらの型では算術演算はサポートされない(expr.add() がそのまま呼び出され、例外が発生する)ということになる。
同様に減算・乗算・除算・剰余も
Core.add() が expr 型に対応したのと同様の変更を、Core.sub()、Core.mul()、Core.div()、Core.mod() にも反映させていき、コンパイル&実行してみたのが、↓以下の図だ。
今日はここまで。
こっそり何度か作り直しが発生したので、少し時間が掛かってしまった。
Program.cs は計 81 行、Type.cs は計 202 行、Core.cs は 86 行。
小さな発見
シンボル定義は、Program.cs にある REPL の Loop ロジック手前に配置していた。
new symbolv("writeln").assign(new functionv(Core.writeln));
new symbolv("+").assign(new functionv(Core.add));
new symbolv("-").assign(new functionv(Core.sub));
new symbolv("*").assign(new functionv(Core.mul));
new symbolv("/").assign(new functionv(Core.div));
new symbolv("%").assign(new functionv(Core.mod));
これからシンボルが徐々に増えていくため、これらのシンボル定義を Program.cs から Core.cs へ追い出すようにしたのだが、その際に静的コンストラクターを使うことにした。
そのコードを↓以下に示す。
internal class Core
{
static Core()
{
new symbolv("writeln").assign(new functionv(Core.writeln));
new symbolv("+").assign(new functionv(Core.add));
new symbolv("-").assign(new functionv(Core.sub));
new symbolv("*").assign(new functionv(Core.mul));
new symbolv("/").assign(new functionv(Core.div));
new symbolv("%").assign(new functionv(Core.mod));
}
:
クラス名と同名で、戻り値と引数の型を指定せずにメソッドを宣言することで、静的コンストラクターを定義できる。
静的コンストラクターはインスタンスを生成する1回目にだけ実行されることが保証されている。
Core クラスは静的メソッドだけで構成されているため、new でインスタンス生成される想定で設計されていないが、Program.cs の Loop ロジック手前に↓以下のコードが必要となった。
new Core();
これでシンボル定義を追加した場合でも Program.cs を変更せずに済む(少々不恰好なのは否めない。今のところ、静的コンストラクターを使う必然性が無い)。