ゼロからはじめるスクリプト言語製作: 分岐とループを実装する(15日目)
前回の実装によって、ユーザーはコードブロックとローカル変数を駆使して 自由に計算ができるようになった。今回も、いくつかの代表的な制御構文に取り組んでいく。
ということで今回の実装ゴールを以下のように課して、実装を進めていくことにしよう。
> (if True 1 2)
===> 1
> (if False 1 2)
===> 2
> (if False 1)
===> nil
> ($ '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
> (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_));
}
関数定義 lambda と分岐処理 if を組み合わせれば、フィボナッチ数列を計算することができる。フィボナッチ数列 F(n) は F(0) = F(1) = 1、F(n) = F(nー1)+F(n-2) で定義される数列だ(初期値の与え方にはいくつかのバリエーションが存在するかもしれない)。
そのままスクリプトにしてみると、↓以下のようになる。繰り返し処理 while を使って、i=0,1,…,9 におけるフィボナッチ数を表示している。
ループを扱うための仕込み
次に要件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
…
翻訳品質が最悪ですね…
分かりにくい場合があります…w
↓こちらのページはどうだろう?
foreach ループでの拡張機能 GetEnumerator | Microsoft Learn
…
こちらも翻訳品質が最悪ですね…
↓こちらのページはどうだろう?
反復ステートメント (C# リファレンス) | 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
翻訳品質は問題ないように見えるけど…
メソッドを追加できる…インスタンスメソッドのように呼び出せる…
どういうことなの…? まるで分からない。
予想だけど、このあたりは C# の発展の歴史(黒歴史?)とかを把握していないと、解読が困難なんじゃないかと思う。不本意だけど、公式リファレンス以外の情報をあれこれ検索してみる必要がありそうだ。
この記事が気に入ったらサポートをしてみませんか?