ゼロからはじめるスクリプト言語製作: シンボルを「評価」する(8日目)
今回もスクリプト言語の製作を進めていこう。
前回はユーザー入力における1つ目の要素を無視して、常に writeln が指定されたものとみなして暫定的な実装をした。今回はきちんとユーザー入力に応じてこれができるように改良していく。
symbolv 型の評価
説明のために、作業前の Program.cs の EvalLine() を引用しておこう。
expr EvalLine(expr expression)
{
cell? args = expression as cell ?? throw new Exception("invalid argument type");
return Core.writeln(args.next());
}
このコードを見ると Core.writeln() がハードコードされている。expression の1つ目の要素が writeln というシンボルだった場合は、これと同じ処理をしたい。つまり「writeln というシンボルを評価して、Core.writeln() が得られる」ように実装すれば良いのである。
ということで上記を反映した結果、Type.cs の symbolv は↓以下のようになった。
※ 行頭「+」箇所は行追加。行頭「-」箇所は行削除。
- public class symbolv : atomv<string>
+ public class symbolv : atomv<string>, IDisposable
{
public symbolv(string val) : base(val)
{
}
+ public override expr eval() => _scope.GetValueOrDefault(_val) ?? throw new Exception($"unknown symbol [{_val}]");
+
+ public void Dispose() => _disposer();
+
+ public symbolv assign(expr val)
+ {
+ _scope[_val] = val;
+ _disposer += () => _scope.Remove(_val);
+ return this;
+ }
+
+ private static Dictionary<string, expr> _scope = new Dictionary<string, expr>();
+ private Action _disposer = () => {};
}
2つの変更が絡んでいるので、分けて説明しよう。
シンボルの参照
シンボルの参照は symbolv.eval() で行っている。_scope という静的フィールドを参照して、シンボル名から expr 型への変換を行っている。_scope はグローバルな解決可能なシンボルを保持している辞書オブジェクトであり、つまり変数スコープを表現していることになる。
逆に解釈すると、symbolv は解決できないシンボルを表せるということでもある。未知のシンボルがユーザー入力に含まれていた場合には、そのような symbolv インスタンスが生成される。
シンボルの登録と削除
シンボルの登録と削除は対になっていて、それぞれ symbolv.assign() と symbolv.Dispose() で行っている。_disposer はシンボル削除時に実行させたい処理を保持する delegate となっている。
REPL の完成
作業後の EvalLine() は↓以下のようになった。
※ 行頭「+」箇所は行追加。行頭「-」箇所は行削除。
expr EvalLine(expr expression)
{
cell? args = expression as cell ?? throw new Exception("invalid argument type");
- return Core.writeln(args.next());
+ return args.eval();
}
これで REPL に必要なコードがひと通り揃ったことになる。
メインループに入る前に、ユーザー入力として使えるシンボルをいくつかセットアップしておいた。
※ 行頭「+」箇所は行追加。
+ new symbolv("writeln").assign(new functionv(Core.writeln));
+ new symbolv("+").assign(new functionv(Core.add));
string commandLine = "";
while (true)
{
+ try
+ {
expr expression = ReadLine(ref commandLine);
expr result = EvalLine(expression);
PrintLine(result);
+ }
+ catch (Exception err)
+ {
+ Console.WriteLine(err);
+ }
}
コンパイルして実行すると、↓以下の図のとおりになる。
writeln の3つ目の引数として S 式 (+ 1 2) を渡しているが、これが評価されて 3 に置き換わっている部分に注目してほしい。
これから演算子をたくさん定義していけば、より複雑な計算を表現することもできるだろう。
今日はここまでにしよう、おつかれさま。
Program.cs は計 82 行、Type.cs は計 163 行。
小さな発見
public symbolv assign(expr val)
{
_scope[_val] = val;
_disposer += () => _scope.Remove(_val);
return this;
}
上記コードの _disposer は Action 型で宣言されており、Action の定義は↓以下のようになっている。
public delegate void Action();
delegate は演算子 += に対応していて、関数合成による処理の追加が可能になっている。
なお += は可能だが、-= や *= などには対応していない(当然か)。
また Action 型には delegate が取る引数に応じたバリエーションが数多く存在している。
public delegate void Action<in T>(T obj);
public delegate void Action<in T1,in T2>(T1 arg1, T2 arg2);
public delegate void Action<in T1,in T2,in T3>(T1 arg1, T2 arg2, T3 arg3);
public delegate void Action<in T1,in T2,in T3,in T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
:
public delegate void Action<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);
Action に似たものに Func 型というものも存在する。
delegate が戻り値を伴うように宣言したい場合は、Action の代わりに Func を使おう。
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T,out TResult>(T arg);
public delegate TResult Func<in T1,in T2,out TResult>(T1 arg1, T2 arg2);
public delegate TResult Func<in T1,in T2,in T3,out TResult>(T1 arg1, T2 arg2, T3 arg3);
public delegate TResult Func<in T1,in T2,in T3,in T4,out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
:
public delegate TResult Func<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16,out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);
この記事が気に入ったらサポートをしてみませんか?