見出し画像

ゼロからはじめるスクリプト言語製作: 枝に枝を継ぐ(4日目)

前回までで、ユーザー入力から1ステップ分の入力を切り出すことができるようになった。
今日は、ユーザー入力を構造化するための対応を進めていこう。

木構造の枝と葉を表すには

「S 式には S 式やリテラル等を含めることができる」という再起構造なので、デザインパターンでいう Composite パターンを使っていくべきだろう。

新たに Type.cs をプロジェクトに追加して、↓以下のようなコードを追加した。

namespace ToyLisp
{
	namespace Type
	{
		public abstract class expr
		{
		}

		public class nil : expr
		{
		}

		public class atomv : expr
		{
			public atomv(string val) => _val = val;
			private string _val;
		}

		public class cell : expr
		{
			public cell(expr? expression)
			{
				_car = expression ?? new nil();
				_cdr = new nil();
			}

			public cell(IEnumerable<expr> expressions)
			{
				_car = (expressions.Count() > 0) ? expressions.First() : new nil();
				_cdr = (expressions.Skip(1).Count() > 0) ? new cell(expressions.Skip(1)) : new nil();
			}

			public cell append(expr expression)
			{
				cell cur = this;
				while (cur._cdr is cell)
				{
					cur = (cell)cur._cdr;
				}
				cur._cdr = expression;
				return this;
			}

			private expr _car, _cdr;
		}
	}
}

基本となる抽象型 expr を定義して、これを基底クラスとしてさらに、葉となる型 atomv と、枝となる型 cell の2つを定義してみた。atomv 型は S 式の要素のひとつひとつを表現し、cell 型は expr 型への参照をひとつ格納しつつ、次の要素への参照をひとつ格納する。

cell 型の2つ目のコンストラクターは、少し注意が必要かもしれない。コンストラクターの中で new cell(~~) としているので、コンストラクターを再帰呼び出ししているのが分かる。これによってリスト状になった cell インスタンスを _cdr フィールドにつないでいる。

ざっくりとではあるが、S 式を構成する枝と葉を準備することができた。

コードの読み込み処理をいじって、内部表現を構築させる

Program.cs の ReadLine() の戻り値は、これまではstring 型だったが、これからは expr 型になる。
戻り値を構築するための expression 変数も、併せて変更になる。

expr ReadLine(ref string line)
{
	expr? expression = null;
	:

それから ReadLine() 内で expression 変数を更新している処理を、それぞれ置き換えていく。

一部実装の変更前と変更後は、↓こうなった。
※ 行頭「-」が変更前、行頭「+」が変更後

@@ -24,26 +29,35 @@
                        }
                        else
                        {       // read out before open paren.
-                               expression += line.Substring(0, parenAt);
+                               var tokens = tokenize(line.Substring(0, parenAt));
+                               if (tokens?.Count() > 0)
+                               {
+                                       append(ref expression, new cell(tokens));
+                               }
                                line = line.Substring(parenAt);
                                // read out from open paren to close paren.
-                               expression += ReadLine(ref line);
+                               append(ref expression, new cell(ReadLine(ref line)));
                        }
                        parenAt = line.IndexOfAny(parens);
                }
-               // read out to close paren.
-               expression += line.Substring(0, parenAt + 1);
-               line = line.Substring(parenAt + 1);
+               {       // read out to close paren.
+                       var tokens = tokenize(line.Substring(0, parenAt));
+                       if (tokens?.Count() > 0)
+                       {
+                               append(ref expression, new cell(tokens));
+                       }
+                       line = line.Substring(parenAt + 1);
+               }
        }
-       return expression;
+       return expression!;
 }

変更前に「expression += ~~」だった箇所が、tokenize() と append() と new cell(~~) に置き換わっている。

1つ目・3つ目の new cell() には、line.Substring() で切り出した文字列を、tokenize() で要素ごとに切り刻んだものを渡している。
2つ目の new cell() には、ReadLine() で構造化したものを渡している。

本日完成したプログラムを Visual Studio でデバッグ実行して確認してみよう。

expression 変数が expr で構造化できた

ローカル変数 expression の中身が cell や atomv の連鎖で表現されていることが確認できる。
今日はここまで、おつかれさま。

_cdr フィールドは cell? 型で宣言すべきでは?

実装を振り返ってみて、1つの疑問にぶつかった。
expr 型のインスタンスをリスト状につなげるために cell 型を定義したのであれば、_cdr は cell 型でないとおかしいことになる。
そもそもリンクリスト構造を自前定義しないで、System.Collections.Generic.List<T> を使った方がラクなのではないか。

それに今の実装では、cell 型において後続の要素が存在しないことを表すために、わざわざ nil 型を定義して使っている。
C# や多くの上級言語では null 値が使えるけど、型にしておく動機は何なのか。

これらの疑問については、今のところうまく説明できない。今の選択が最良かどうか自信が無い…のが正直なところだ。
今後、内部表現を評価していく段階を迎えたら、また考えてみようかと思う。

小さな発見

処理がほとんどないメソッドを、短く書く方法があるようだ。
ラムダ式と同じく、「=>」に続けてステートメントを書くことができる。

		public class atomv : expr
		{
			public atomv(string val) => _val = val;
			private string _val;
		}

こうしておくと、処理の単純さがより際立って良い。

それと、null 許容値型からの値の取得に便利な演算子の話。

			public cell(expr? expression)
			{
				_car = expression ?? new nil();
				_cdr = new nil();
			}

?? 演算子は、null 許容値型の変数が null であったときに、代わりの右辺値を生成してくれる。
これはスマートで良い。ぜひ使いこなせるようになりたい。

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