ゼロからはじめるスクリプト言語製作: 枝に枝を継ぐ(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 の中身が 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 であったときに、代わりの右辺値を生成してくれる。
これはスマートで良い。ぜひ使いこなせるようになりたい。
この記事が気に入ったらサポートをしてみませんか?