見出し画像

ゼロからはじめるスクリプト言語製作: 基本型を「評価」する(7日目)

今回もスクリプト言語の製作を進めていこう。
例えばユーザー入力として (writeln 1 2) を指定したとき、インタープリターはコンソールに 1 と 2 と改行を出力するものとしたい。

コンソール出力したところ

前回までで、スクリプト言語の挙動に必要な「REPL」の R と P と L を実装してきた。
いよいよ今回から、残る E の実装を進めていこう。ここからだんだんと楽しい部分に踏み込んでいく。

REPL の E は Eval の E

Eval は「評価」と訳される。では評価とはどういうことだろう。

Clojure の S 式の説明を思い返しながら あらためて技術的に考え直してみると、Lisp 系言語における評価とは「S 式をルールに従って変形させること」といえるのではないだろうか。

Clojure コードの構造的な側面と文脈的な側面
(引用元: Clojure 公式ページ)

ユーザー入力 (+ 3 4) は、「+」というスクリプト関数に引数 3 と 4 を渡してコールすることを意味していて、出力として 7 を得る(まだ実装していないけど、そうふるまうべきである)。したがって (writeln (+ 3 4)) と入力したときに、1つ目の引数 (+ 3 4) を評価することで (writeln 7) を得る、この過程が(1つ目の引数にとっての)「評価」ということになる。

もう1つ別の例を挙げよう。
ユーザーがすでに a というシンボルを宣言していたとして、ユーザー入力 (+ a 1) を評価するときはどうなるだろうか。
先に a を「評価」して、その評価結果を引数として「+」関数に渡すことになる。a の評価結果が仮に 2 であれば、(+ a 1) という全体の評価結果は 3 になる。

このように引数となる要素を評価していく部分を実装してみようと思う。「どういう順番で評価を行うか」については一考の余地があるけれど、今後改めて検討していくことにして、まずは先頭から順番に評価していくものと仮定しよう。

準備として Program.cs の PrintLine() を少し手直ししておいた。

void PrintLine(expr expression)
{
	Console.WriteLine($"===> {expression}");
}

expr 型の評価

atomv 型と cell 型のどちらも評価される/評価できる必要がある。まずはそれぞれの基本クラスに当たる expr 型に抽象メソッドを宣言しよう。
※ 行頭「+」箇所は行追加

		public abstract class expr
		{
+			public abstract expr eval();
		}

stringv 型、boolv 型、numberv 型、floatv 型、nil 型の評価

これらの基本型が評価されるときは、保持している値をそのまま返すことになる。コードは↓以下のようになった。
※ 行頭「+」箇所は行追加

		public class atomv<T> : expr
		{
			public atomv(T val) => _val = val;
			public override string ToString() => _val.ToString();
+			public override expr eval() => this;
			public readonly T _val;
		}

cell 型の評価

cell 型が評価されるときは、先頭の要素(_car メンバー)から functionv インスタンスを取り出し、続く要素(_cdr メンバー)をスクリプト関数に渡して好きに評価してもらう。
※ 行頭「+」箇所は行追加

		public class cell : expr
		{
<中略>

+			public override expr eval()
+			{
+				functionv? func = _car.eval() as functionv ?? throw new Exception("eval against non-function");
+				return func.eval(_cdr);
+			}

functionv 型の評価

functionv 型(スクリプト関数を保持する基本型)には2つの eval() メソッドを実装する。1つ目は先に触れた atomv.eval() であり、自身のインスタンスをそのまま返す。2つ目は引数に expr 型を伴う特殊な eval() メソッドだ。
※ 行頭「+」箇所は行追加

		public delegate expr evaluator(expr args);
		public class functionv : atomv<evaluator>
		{
			public functionv(evaluator val) : base(val)
			{
			}

+			public expr eval(expr args) => _val(args);
		}

実装を見ると evaluator という delegate を宣言しているのが分かる。これが、先に述べた「expr を expr に変換する」部分に該当する。

writeln 関数の実装

ここまでのお膳立てがあれば、新たなスクリプト関数を実装し、インタープリターに実行させることができる。
今回のところは暫定的に、ユーザー入力における1つ目の要素を無視して、常に writeln が指定されたものとみなすよう実装をしてみた。Program.cs の EvalLine() は↓以下のようになった。

expr EvalLine(expr expression)
{
	cell? args = expression as cell ?? throw new Exception("invalid argument type");
	return Core.writeln(args.next());
}

スクリプト関数の実装は、新たにプロジェクトに追加した Core.cs へ集約することにした。Core.writeln() のコードは↓以下のようになった。

using ToyLisp.Type;

namespace ToyLisp
{
	internal class Core
	{
		public static expr join0(expr args, string separator)
		{
			string val = "", sep = "";
			for (cell? cur = args as cell; cur != null; cur = cur.next() as cell)
			{
				string elem = cur.element().eval().ToString() ?? throw new Exception("invalid element type");
				(val, sep) = (val + sep + elem, separator);
			}
			return new stringv(val);
		}

		public static expr writeln(expr args)
		{
			string val = "";
			expr str = join0(args, " ");
			Console.WriteLine(str);
			return new nil();
		}
	}
}

コンパイルして実行すると、冒頭の図のとおりになる。
コンソールへの出力に加えて nil が表示されているが、これは writeln 関数の戻り値を示している。writeln() の return 文に指定しているとおりである。

今日はここまで、おつかれさま。
Program.cs  は計 73 行、Type.cs  は計 149 行。いい調子だと思う。

小さな発見

演算子 as を使えば、オブジェクトが指定クラスにキャストできるかどうかを確認することができる。その戻り値は null 許容型になる。
そして演算子 ?? を使えば、null 許容型の変数に null が保持されているかどうかを確認することができる。

それらを組み合わせて使用したのが↓以下の例だ。

	cell? args = expression as cell ?? throw new Exception("invalid argument type");

変数 expression が cell 型にキャストできた場合は、args にはその参照が代入されるが、キャストできなかった場合は例外を投げるようになっている。

同じ処理を↓以下のようにもう少し短く書くこともできるが、そうした場合、キャストできなかった時に投げられる例外は System.InvalidCastException に固定されてしまう。

	cell args = (cell)expression;

なお単純に「キャストできるかどうか」「互換性があるか」だけが知りたい場合は、↓以下のように as の代わりに演算子 is を使う方が良い。

	expression is cell

似たような演算子に typeof というものもある。これも変数の型のチェックに使用できる。
変数 expression が cell 型かどうかをチェックするには、↓以下のように記述する。

	expression.GetType() == typeof(cell)

このように変数の型を調べるには GetType() を呼ぶ必要があり、typeof(expression) というような記述はできないので注意しておきたい。

また expression is cell と expression.GetType() == typeof(cell) とでは意味が異なるので、これも注意しておきたい。


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