見出し画像

ゼロからはじめるスクリプト言語製作: 分岐とループを実装する(15日目)

前回の実装によって、ユーザーはコードブロックとローカル変数を駆使して 自由に計算ができるようになった。今回も、いくつかの代表的な制御構文に取り組んでいく。

ということで今回の実装ゴールを以下のように課して、実装を進めていくことにしよう。

<要件1>
1) シンボル if は2つか3つの引数を取り、1つめは boolv 型でなければならない
2) if の1つめの引数を評価して、True のときは2つめの expr を、False のときは3つめの expr があれば評価し、その評価結果を返す(3つめの expr が無かったときは nil を返す)

> (if True 1 2)
===> 1
> (if False 1 2)
===> 2
> (if False 1)
===> nil

<要件2>
3) シンボル while は1つ以上の引数を取り、1つめは boolv 型でなければならない
4) while はまずローカルスコープを確保して、変数 while:value を nil で、while:break を False で初期化する
5) while は1つめの引数を評価して、True のときは2つめ以降の expr をすべて評価し、再び1つめの引数の評価からやり直す(評価結果が False になるまで繰り返す)
6) while は while:value に設定された値を返す

> ($ 'v 0)
===> v
> (while (< v 10)
>>> (writeln v)
>>> ($= 'v (+ v 1))
>>> )
0
1
2
3
4
5
6
7
8
9
===> nil
> ($ 'v 0)
===> v
> ($ 'sum 0)
===> sum
> (while (< v 10)
>>> ($= 'sum (+ sum v))
>>> (writeln v sum)
>>> ($= 'while:break (>= sum 30))
>>> ($= 'while:value v)
>>> ($= 'v (+ v 1))
>>> )
0 0
1 1
2 3
3 6
4 10
5 15
6 21
7 28
8 36
===> 8

<要件3>
7) シンボル for は2つ以上の引数を取り、1つめは symbolv 型、2つめは cell 型(リスト)でなければならない
8) for はまずローカルスコープを確保して、変数 for:value を nil で、for:break を False で初期化する
9) for は2つめの引数を評価して、要素を列挙できたときは1つめのシンボルに代入した上で、3つめ以降の expr をすべて評価し、再び2つめの引数の列挙からやり直す(評価した結果列挙されなくなるまで繰り返す)
10) for は for:value に設定された値を返す

> (for 'i (' 1 3 5 7 9)
>>> (writeln i)
>>> )
1
3
5
7
9
===> nil

条件分岐と繰り返し

要件1~2は難しいところは無く、落ち着いて素直に実装していくだけで良い。
シンボル if の実体となる Core.if_() のコードを↓以下に示す。

		public static expr if_(expr args)
		{
			cell? arg0 = args.astype<cell>();
			cell? arg1 = arg0?.next().astype<cell>();
			cell? arg2 = arg1?.next().astype<cell>();
			cell? arg3 = arg2?.next().astype<cell>();
			if (arg0 is null || arg1 is null || arg3 is not null) throw new Exception("wrong number of args");
			boolv cond0 = arg0.element().eval().cast<boolv>();
			return cond0._val ? arg1.element().eval() : arg2?.element().eval() ?? new nil();
		}

シンボル while の実体となる Core.while_() のコードを↓以下に示す。

		public static expr while_(expr args)
		{
			cell? arg0 = args.astype<cell>();
			cell? arg1 = arg0?.next().astype<cell>();
			if (arg0 is null) throw new Exception("wrong number of args");
			using var symbols = new symbolv.scope();
			symbolv value = new symbolv("while:value").assign(new nil(), true);
			symbolv isbreak = new symbolv("while:break").assign(new boolv(false), true);
			boolv cond0 = arg0.element().eval().cast<boolv>();
			while (cond0)
			{
				reduce(arg1, new nil(), (expr left, expr right) => right, fetcher);
				if (isbreak.eval().astype<boolv>() ?? true) break;
				cond0 = arg0.element().eval().cast<boolv>();
			}
			return value.eval();
		}

functionv のシンボル定義を追加して、コンパイル&デバッグしたのが下図である。
※ 行頭「+」箇所は行追加。

		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("if").assign(new functionv(Core.if_));
+			new symbolv("while").assign(new functionv(Core.while_));
		}
if と while に対応できた!

関数定義 lambda と分岐処理 if を組み合わせれば、フィボナッチ数列を計算することができる。フィボナッチ数列 F(n) は F(0) = F(1) = 1、F(n) = F(nー1)+F(n-2) で定義される数列だ(初期値の与え方にはいくつかのバリエーションが存在するかもしれない)。
そのままスクリプトにしてみると、↓以下のようになる。繰り返し処理 while を使って、i=0,1,…,9 におけるフィボナッチ数を表示している。

if を使って、フィボナッチ数列を計算してみた!
出力の3行目以降は、1行上と2行上の数値を足したものだ

ループを扱うための仕込み

次に要件3を見ていこう。シンボル for の定義のほとんどは、while の定義と一致する。違うのは与えるのが論理式か、ループ変数と列挙可能な式の組か、というだけだ。

ということで Core.for_() の実装の手始めとして Core.while_() をコピペしている。それに対して要件3を反映させた結果、コードの差異は↓以下の通りになった。
※ 行頭「-」箇所は行削除。行頭「+」箇所は行追加。

-		public static expr while_(expr args)
+		public static expr for_(expr args)
		{
			cell? arg0 = args.astype<cell>();
			cell? arg1 = arg0?.next().astype<cell>();
+			cell? arg2 = arg1?.next().astype<cell>();
-			if (arg0 is null) throw new Exception("wrong number of args");
+			if (arg0 is null || arg1 is null) throw new Exception("wrong number of args");
-			boolv cond0 = arg0.element().eval().cast<boolv>();
+			symbolv key0 = arg0.element().eval().cast<symbolv>();
+			expr values = arg1.element().eval();
			using var symbols = new symbolv.scope();
-			symbolv value = new symbolv("while:value").assign(new nil(), true);
-			symbolv isbreak = new symbolv("while:break").assign(new boolv(false), true);
+			symbolv value = new symbolv("for:value").assign(new nil(), true);
+			symbolv isbreak = new symbolv("for:break").assign(new boolv(false), true);
-			while (cond0)
+			foreach (var item in values)
			{
+				key0.assign(item, true);
-				reduce(arg1, new nil(), (expr left, expr right) => right, fetcher);
+				reduce(arg2, new nil(), (expr left, expr right) => right, fetcher);
				if (isbreak.eval().astype<boolv>() ?? true) break;
-				cond0 = arg0.element().eval().cast<boolv>();
			}
			return value.eval();
		}

Core.while_() のときは変数 cond0 が繰り返し要否を制御していて、while の手前と while の中で要素を(再)評価していた。Core.for_() では foreach-in に与える変数 values が繰り返し要否を制御している。

2つめの引数として与えられる cell 型が foreach-in で列挙されるよう、Type.expr 型と Type.cell 型に↓以下の変更を加えた。
※ 行頭「+」箇所は行追加。

		public abstract class expr : IEquatable<expr>, IComparable<expr>
		{
			:
+			public virtual IEnumerator<expr> GetEnumerator() => throw new Exception("invalid element type");
		}

:

		public class cell : expr
		{
			:
+			public override IEnumerator<expr> GetEnumerator()
+			{
+				cell cur = this;
+				yield return cur._car;
+				while (cur._cdr is cell)
+				{
+					cur = (cell)cur._cdr;
+					yield return cur._car;
+				}
+			}

			:
		}

functionv のシンボル定義を追加して、コンパイル&デバッグしたのが下図である。
※ 行頭「+」箇所は行追加。

		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("for").assign(new functionv(Core.for_));
		}
フィボナッチ数列を再計算してみた!

今日はここまで、おつかれさま。
Program.cs は計 81 行、Type.cs は計 278 行、Core.cs は 254 行。コード量は 67 行も増加してしまった。増加行のうち Core.if_() と Core.while_() と Core.for_() で 50 行を占めている。Core モジュールにコードのコピペ箇所が増えてきたので、引数の型変換と解釈をもう少し一般化しておく余地があるのかもしれない。

小さな発見

C# の foreach-in 構文で何かを列挙するための必要要件について、少し調べてみた。

foreach ステートメントで Visual C# クラスを使用できるようにする | Microsoft Learn

Microsoft Learn からの引用 その①


翻訳品質が最悪ですね…
分かりにくい場合があります…w

↓こちらのページはどうだろう?

foreach ループでの拡張機能 GetEnumerator | Microsoft Learn

Microsoft Learn からの引用 その②


こちらも翻訳品質が最悪ですね…

↓こちらのページはどうだろう?

反復ステートメント (C# リファレンス) | Microsoft Learn

Microsoft Learn からの引用 その③

どうやらこのページを参考にするのが正解だったようだ。
配列であれば そのまま foreach-in 構文に対応でき、もしくは GetEnumerator() を public に実装することで そのクラスは foreach-in 構文に対応できる模様。
なるほど。
ということで今回は、基本型 expr 型と派生型 cell 型に GetEnumerator() を実装している。

		public abstract class expr : IEquatable<expr>, IComparable<expr>
		{
			:
+			public virtual IEnumerator<expr> GetEnumerator() => throw new Exception("invalid element type");
		}

:

		public class cell : expr
		{
			:
+			public override IEnumerator<expr> GetEnumerator()
+			{
+				cell cur = this;
+				yield return cur._car;
+				while (cur._cdr is cell)
+				{
+					cur = (cell)cur._cdr;
+					yield return cur._car;
+				}
+			}

			:
		}

どうやら IEnumerable インターフェースを cell 型の宣言に追加する必要は無いらしい。
そして GetEnumerator() というのは拡張メソッドというものに該当するらしい。
拡張メソッドについて調べたくなって、↓以下のページを参照してみたが…

拡張メソッド (C# プログラミング ガイド) | Microsoft Learn

Microsoft Learn からの引用 その④

翻訳品質は問題ないように見えるけど…
メソッドを追加できる…インスタンスメソッドのように呼び出せる…
どういうことなの…? まるで分からない。

予想だけど、このあたりは C# の発展の歴史(黒歴史?)とかを把握していないと、解読が困難なんじゃないかと思う。不本意だけど、公式リファレンス以外の情報をあれこれ検索してみる必要がありそうだ。


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