見出し画像

ゼロからはじめるスクリプト言語製作: 整数の演算を多彩に(9日目)

前回までで、S 式の基本的な評価が完成している。
今回はユーザー入力で呼び出せるスクリプト関数を増やして、整数演算のバリエーションを豊かにしていこうと思う。

まずは加算

前回「+」というシンボルを追加した。そのコードを提示していなかったので、Core.cs にある add() を↓以下に提示しておこう。

		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);
		}

		public static expr add(expr args)
		{
			numberv value = new numberv(0);
			return reduce(args, ref value, (numberv other, ref numberv val) => val = new numberv(val._val + other._val));
		}

多くのプログラミング言語では、+ は二項演算子として定義されている。そのため、例えば a と b を加算するときは a + b、a と b と c を加算するときは a + b + c と記述することになる。
その一方で「+ a」というように、変数やリテラルに対して単独で適用することもでき、単項演算子のようにふるまうことも多い。

今回製作するスクリプト言語においてはその両方を包含する意味で、+ を多項演算子として定義することにした。Core.add() に S 式を渡すと、その先頭から順番に numberv を取り出せるだけ取り出して、それらすべてを合算する、というふうにふるまう。

+ を多項演算子として実装

さて、ユーザー入力が (+) だけのときはどうしたらよいだろうか。
引数が無いときはエラーにすることもできるが、今回は↓以下の法則性を考慮して、これは 0 として扱うことにしている。

引数が 0 個のときの加算結果

四則演算の準備

今回は四則演算を実装していくので、Type.cs の numberv 型に numberv 型との演算に使うコードを準備しておくことにする。
※ 行頭「+」箇所は行追加。

		public class numberv : atomv<long>
		{
			:

+			public numberv add(numberv other) => new numberv(_val + other._val);
+			public numberv sub(numberv other) => new numberv(_val - other._val);
+			public numberv mul(numberv other) => new numberv(_val * other._val);
+			public numberv div(numberv other) => new numberv(_val / other._val);
+			public numberv mod(numberv other) => new numberv(_val % other._val);
		}

加算の次は減算

Core.add() は多項演算子としてふるまうために、内部で Core.reduce() をコールしていた。このとき Core.add() は引数が無いときの合計値 0 を value に指定し、合計値と引数を演算するための func を指定していた。

同じように減算も実装できるのだろうか。
↓以下は Core.reduce() に渡す func のみを改変して、同じように実装してみた例である。
※ 行頭「+」箇所は行追加。

		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));
		}

+		public static expr sub(expr args)
+		{
+			numberv value = new numberv(0);
+			return reduce(args, ref value, (numberv other, ref numberv val) => val = val.sub(other));
+		}

この Core.sub() のコードは一見してバグがあるようには見えないが、実際に実行して使ってみるとすぐにちょっとした不都合に気付く。
例えばユーザー入力が (- 1 2 3) のときに評価結果は -6 となり、(- 1 2) のときに評価結果は -3 になってしまう。
一般的な表記で a - b という演算を行おうとして、スクリプト言語的に (- a b) と入力するのは誤りで、代わりに (+ a (- b)) と入力すればよいということになるのだが、これでは少々苦痛かもしれない。

ここの使い勝手を良くするため、減算の実装においては引数の個数に応じて処理内容を変更する、という定義に落ち着いた。

引数の個数に応じた減算処理

ということで上記を反映した結果、Core.sub() のコードは↓以下のようになった。
※ 行頭「+」箇所は行追加。

+		public static expr sub(expr args)
+		{
+			cell? arg0 = args.astype<cell>();
+			cell? arg1 = arg0?.next().astype<cell>();
+			numberv value = (arg1 == null) ? new numberv(0) : arg0?.element().eval().astype<numberv>() ?? throw new Exception("invalid element type");
+			return reduce(arg1 ?? args, ref value, (numberv other, ref numberv val) => val = val.sub(other));
+		}
引数の個数に応じた減算処理結果

乗算と除算

加算と減算のコードを流用すると、乗算と除算の実装が完成する。
初期値は 0 ではなく 1 に置き代わっている点に注意すると良い。

+		public static expr mul(expr args)
+		{
+			numberv value = new numberv(1);
+			return reduce(args, ref value, (numberv other, ref numberv val) => val = val.mul(other));
+		}

+		public static expr div(expr args)
+		{
+			cell? arg0 = args.astype<cell>();
+			cell? arg1 = arg0?.next().astype<cell>();
+			numberv value = (arg1 == null) ? new numberv(1) : arg0?.element().eval().astype<numberv>() ?? throw new Exception("invalid element type");
+			return reduce(arg1 ?? args, ref value, (numberv other, ref numberv val) => val = val.div(other));
+		}
乗算と整数除算
(※ 本来 2 ÷ 2 ÷ 2 は 0.5 となるが、整数演算であるため 0 に丸められている)

なおスクリプト言語で除算をサポートするときは、ゼロ除算が発生する可能性に配慮が必要だ。Core.div() にはゼロ除算発生時の処理が無いため、標準的な算術例外が発生する。
いまのところこのままで不都合はないため、これはこれで置いておくことにしよう。

除算の次は剰余

除算ができるようになったら、次は剰余だ。
だがここで問題が発生してしまった。↓以下の図を見てほしい。

剰余の引数が足りない時のふるまいは?

剰余においては引数が2つ未満の場合に、どのような演算結果となるべきか、法則性を見出して定義することができなかった。この場合は引数が足りないエラーとして定義することにした。

+		public static expr mod(expr args)
+		{
+			cell? arg0 = args.astype<cell>();
+			cell arg1 = arg0?.next().astype<cell>() ?? throw new Exception("wrong number of args");
+			numberv value = arg0?.element().eval().astype<numberv>() ?? throw new Exception("invalid element type");
+			return reduce(arg1, ref value, (numberv other, ref numberv val) => val = val.mod(other));
+		}

なおスクリプト言語で剰余をサポートするときも、被除数と除数の符号の扱いに配慮が必要だ。特別なコードは追加していないため、↓以下のように C# 標準の剰余の定義にしたがった評価結果が得られる。

負数の剰余

検算してみる

ここまでで主要な整数演算の実装が一段落した。
最後に確認として、これらの演算を組み合わせて、1から9までの数字を1回ずつ使う小町算をやってみた。

小町算を計算!

式を2つ実行させてみた結果、それぞれのコンソール出力が無事 100 になった。大丈夫なようだ。
今日はここまでにしよう、おつかれさま。
Program.cs  は計 86 行、Type.cs  は計 169 行、Core.cs は 89 行。

小さな発見

		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);
		}

上記コードでは、型パラメータ T1 と TV に対して where キーワードを指定している。これで T1 や TV に対する制約を設けることができる。
今回の where キーワードの使い方的には、expr という制約は T1 と TV がどちらも expr を継承した型でなければならないことを意味している。

仮に T1 に対する制約が無かった場合、コードブロック2行目の return 文はコンパイルエラーとなる。value の型である T1 が、関数の戻り値型である expr と互換が無い可能性があるからだ。

同様に TV に対する制約が無かった場合、コードブロック3行目の astype() メソッドはコンパイルエラーとなる。astype() の型パラメータには、すでに以下の制約が指定されているからだ。

		public abstract class expr
		{
			public T? astype<T>() where T : expr => this as T;
			public abstract expr eval();
		}


この記事が気に入ったらサポートをしてみませんか?