見出し画像

ゼロからはじめるスクリプト言語製作: エレガントにさばく(3日目)

前回製作した読み込み処理では、ユーザー入力に複数の丸カッコがあったときに、正しいところでコードを切り出すことができない問題があった。
今日はこの部分の改善を進めていこう。

入れ子構造の丸カッコに対応するには

ここは再帰呼び出しを使っていくべきだろう。
丸カッコの対応を見つけるだけなら他の方法でも良いのだが、このあと構文解析も必要になるし、前回までで1対の丸カッコを切り出す処理はそろっているので、これを活用できる。

Program.cs の ReadLine() の前半ブロックは、開き丸カッコの位置を探す処理になってて、ここの変更は必要ない。
後半ブロックは閉じ丸カッコの位置を探す処理になってて、ここに if 文を追加する。閉じ丸カッコよりも先に開き丸カッコを見つけたならば、ReadLine() を呼び直すことにしよう。

string ReadLine(ref string line)
{
	string expression = "";
	{	// skip chars to open paren.
		int openedAt = line.IndexOf('(');
		while (openedAt < 0)
		{
			Console.Write("> ");
			line += Console.ReadLine();
			openedAt = line.IndexOf('(');
		}
		expression += line.Substring(openedAt, 1);
		line = line.Substring(openedAt + 1);
	}
	{	// find nearest paren.
		char[] parens = { '(', ')', };
		int parenAt = line.IndexOfAny(parens);
		while (parenAt < 0 || line[parenAt].Equals(parens[0]))
		{
			if (parenAt < 0)
			{	// read new from input stream.
				Console.Write(">>> ");
				line += ' ' + Console.ReadLine();
			}
			else
			{	// read out before open paren.
				expression += line.Substring(0, parenAt);
				line = line.Substring(parenAt);
				// read out from open paren to close paren.
				expression += ReadLine(ref line);
			}
			parenAt = line.IndexOfAny(parens);
		}
		// read out to close paren.
		expression += line.Substring(0, parenAt + 1);
		line = line.Substring(parenAt + 1);
	}
	return expression;
}

if 文の else 節にご注目。これで入れ子にも対応できた。
プログラムを実行して確認してみよう。

入れ子構造になった丸カッコを解釈する

よしよし、順調だ。どんな入力に対しても、開き丸カッコと閉じ丸カッコの数は一致している。
今日はここまで、おつかれさま。

内部表現について想いを巡らせる

Lisp といえば S 式が有名だ。
でもそれが何なのかを説明できなかったので、少し調べてみた。たどり着いた Clojure 公式ページの説明が比較的腹落ちできたので、自分の言葉でまとめ直してみた。

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

S 式は、シンボルやリテラル(数値など)の列を空白で区切り、丸カッコで括ることで、先頭から末尾までの直列構造を表すものである。そして S 式の要素に S 式を含めることもできる。
それと同時に、リスト先頭に位置する要素を「呼び出したいサブルーチン」、先頭に続くすべての要素を「サブルーチンに与える引数」という風にみなし、スクリプト言語の文脈や評価手順を表すものでもある。

これを扱うには S 式を表現するための構造を考えないといけない。
次回からはこの辺りを取り組んでいこうと思う。

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