こんにちは。Rubyを作りながらRubyを学ぼうという連載企画、第6回です。
前回は、Rubyで変数付き四則演算インタプリタを作りました。 今回は、これを拡張して分岐を実装します。
現状の確認
まず、変数付き四則演算インタプリタの現状のソースコードを省略せずに載せておきます。 第4回の練習問題にあった、剰余や比較式の実装についても加えてあります。
require "minruby"
def evaluate(tree, env)
case tree[0]
when "lit"
tree[1]
when "+"
evaluate(tree[1], env) + evaluate(tree[2], env)
when "-"
evaluate(tree[1], env) - evaluate(tree[2], env)
when "*"
evaluate(tree[1], env) * evaluate(tree[2], env)
when "/"
evaluate(tree[1], env) / evaluate(tree[2], env)
when "%"
evaluate(tree[1], env) % evaluate(tree[2], env)
when "**"
evaluate(tree[1], env) ** evaluate(tree[2], env)
when "<"
evaluate(tree[1], env) < evaluate(tree[2], env)
when "<="
evaluate(tree[1], env) <= evaluate(tree[2], env)
when "=="
evaluate(tree[1], env) == evaluate(tree[2], env)
when ">="
evaluate(tree[1], env) >= evaluate(tree[2], env)
when ">"
evaluate(tree[1], env) > evaluate(tree[2], env)
when "func_call" # 仮の実装
p(evaluate(tree[2], env))
when "stmts"
i = 1
last = nil
while tree[i]
last = evaluate(tree[i], env)
i = i + 1
end
last
when "var_assign"
env[tree[1]] = evaluate(tree[2], env)
when "var_ref"
env[tree[1]]
end
end
# ① 計算式の文字列を読み込む
str = minruby_load()
# ② 計算式の文字列を計算の木に変換する
tree = minruby_parse(str)
# ③ 計算の木を実行(計算)する
env = {}
evaluate(tree, env)
今回はこれをさらに拡張して、if
文とwhile
文を実装していきます。
でもその前に、ここまでのおさらいをしておきましょう。 上記のソースコード、すなわち「変数付き四則演算インタプリタ」は何をしているのでしょうか? 連載の第3回から第5回までの話を思い返してみましょう。
- 「木」という構造があった。「関数」を使うと木をたどることができた(第3回)
- 四則演算の式を「木」として表現できた。その木をたどって式の答えを返す「関数」を作った(第4回)
- 式を並べた複文も「木」として表現できることを見た。プログラムは複文なので、プログラムもまた「木」として表現できる。その木(抽象構文木)をたどって結果を返す「関数」を作った(第5回の前半)
- プログラムでは「変数」(値を覚えておいてくれるもの)を使いたい。どの変数にどんな値を覚えさせたかを管理する「環境」というものを用意した。変数への代入があったら、変数名と値の対応を環境に格納していく。環境は刻々と変化するので、抽象構文木をたどる関数では次々に持ちまわることにした(第5回の後半)
これらの話が現状のインタプリタの姿にどう対応しているかというと、
- 最初に定義している
evaluate
関数が、木をたどる関数 - 変数
tree
に代入したminruby_parse
の結果が、抽象構文木 - 変数
env
に代入した空のハッシュが、環境の初期状態 - 最後の
evaluate(tree, env)
で、2の抽象構文木と3の環境を指定して実行開始
そしてevaluate
関数は、見かけは長いですが、見てのとおり1つの大きなcase
文になっています (case
文を忘れた人は第2回を読み直しましょう)。 木の節(tree[0]
)に応じた条件分岐のかたまりにすぎません。
たとえば、節が"+"
だったら、その先に2つあるはずの部分木を足し合わせます。 部分木がすぐに葉(しかもリテラル)ならいいのですが、そうとは限らないので、そこで2つの部分木に対して再帰的にevaluate
を使っています (再帰を忘れた人は第3回を、 リテラルについて忘れた人は第4回を読み直しましょう)。 そんなふうに再帰的に木をたどっていくと、どの部分木もいつかはリテラルな葉(lit
)に行きつくので、そのときは葉の値(tree[0]
)を単純に返します。
いかがでしょうか。現状のインタプリタのどの部分で何をやっているのか思いだしていただけたでしょうか? 今回の記事では、いま作っているインタプリタに対して、抽象構文木にif
やwhile
に相当する節が出てきたときに何をすればいいかを教えてあげます。 言い方を変えると、このインタプリタにif
文とwhile
文の実装を組み込みます。
if
文とwhile
文を実装するということは、このインタプリタで条件分岐があるプログラムを実行できるようになるということです。 ここまでくれば、もういっぱしのプログラミング言語のインタプリタを実装しているぞ、と公言してもいいでしょう。 そこで以降では「四則演算インタプリタ」という呼び方をやめて、このインタプリタを正式に「MinRubyインタプリタ」と呼ぶことにします。
if
文を実装する
それではMinRubyインタプリタにif
文を実装していきます。目標は次のMinRubyプログラムを動かせるようにすることです。
if 0 == 0
p(42)
else
p(43)
end
まずは、このMinRubyプログラムの抽象構文木がどのような姿になるか、前回同様にminruby_parse
を使って確かめてみましょう。 次のプログラムをファイルに書いて、experiment4.rb
という名前で保存してください。
require "minruby"
pp(minruby_parse("
if 0 == 0
p(42)
else
p(43)
end
"))
これをRubyで実行すれば抽象構文木のようすを確認できます。
C:\Ruby > ruby experiment4.rb ⏎
["if",
["==", ["lit", 0], ["lit", 0]],
["func_call", "p", ["lit", 42]],
["func_call", "p", ["lit", 43]]]
C:\Ruby >
図に描くと次のような木です。"if
"という新しいラベルが増えていますね。
一見すると難しくなってきたように見えるかもしれませんが、if
文の意味を思い出しながら眺めれば単純です。 if
文の意味は、
- まず
tree[1]
の条件式を評価する、 - trueだったら
tree[2]
を評価する、 - falseだったら
tree[3]
を評価する
というものでしたね。 そう思って先ほどの抽象構文木を見れば、要は次のような構造をしているのだとわかるはずです。
["if",
条件式,
条件式がtrueの場合に実行するコード,
条件式がfalseの場合に実行するコード]
では、このような構造の木をどうやってRubyで処理し、MinRubyのif
文を実装すればいいでしょうか?
当たり前ですが、これは分岐です。そして、Rubyで分岐をするにはif
文を使うのでした。 Rubyのif
文を使って、この構造をプログラムに落としこんでみましょう。
部分木の評価はevaluate(tree[1], env)
のようにすればいいので、次のように素直に書けます。
def evaluate(tree, env)
case tree[0]
when "if"
if evaluate(tree[1], env)
evaluate(tree[2], env)
else
evaluate(tree[3], env)
end
...(略)...
end
end
きつねにつままれたような気分かもしれませんが、これだけでちゃんと動きます。 最初に目標にした次のプログラムを実際に動かしてみてください。
if 0 == 0
p(42)
else
p(43)
end
なお、このif
文の実装では、特に環境をいじっていないことに注目してください。変数env
を単にevaluate
の引数として横流ししているだけで、環境の中身を読み出したり変更したりはしていません。 if
文で分岐の方向を変えるのには変数の値を(通常は)使うので、これはちょっと不思議に思うかもしれません。
しかしよく考えてみると、変数を参照するのは条件式(比較式)であり、if
文そのものは、比較式の評価結果を見て分岐の方向を決定しているだけです。 実際、上記のプログラムでも0 == 0
という(当たり前な)比較式を使っています。if
文と変数は独立しているのです。
ちょっと寄り道:インタプリタとは
「if
を実装するのにif
文を使うのってインチキなのでは?」と思うかもしれません。でも、これこそがインタプリタの本質です。
インタプリタとは、言語Xで書かれたプログラムを実行する、言語Yで書かれたプログラムです。たとえば(本物の)Rubyインタプリタは、Ruby言語で書かれたプログラムを実行する、機械語で書かれたプログラムです。(本物の)Rubyインタプリタでは、Rubyプログラムのif
文を機械語のif
文(に相当する言語機能)で実装しています。
同様に、MinRubyインタプリタは、MinRuby言語で書かれたプログラムを実行する、Rubyで書かれたプログラムです。なので、MinRubyプログラムのif
文をRubyのif
文で実装したわけです。
インタプリタの本質は、高レベルの言語機能を低レベルの言語機能に丸投げすることです。if
文の実装では、非常に綺麗に丸投げできました。しかし、すべての言語機能が常に丸投げできるわけでもありません。低レベルの言語には存在しない言語機能を高レベルの言語に持たせる場合は、低レベルの言語機能を組み合わせて実装する必要があります。また、前回実装した変数のように、単純な丸投げが難しい場合もあります。
ついでにいうと、この連載はMinRubyの数値やtrue/false/nilなどの値を、そのままRubyの対応する値として流用しています。しかし、そうしなければならないわけではありません。たとえば、MinRubyの5
という数字を、Rubyの-5
を使って表すことにしてもかまいません。あるいは、["num", 5]
という配列で表すということにしてもかまいません。それでは話がむやみに複雑になるので、Rubyの対応する値を使っているだけです。
while
文を実装する
MinRubyの分岐はif
文だけではありませんでした。次はwhile
文を実装しましょう。
と言っても、実はif
文とほとんど同じ要領で実装できます。 while
文の実装をまるまる練習問題にしようかと迷ったくらいなので、自信のある人は続きを読む前にちょっと考えてみてください。
実装を考えるときの目標にするのは次のサンプルプログラムです。
i = 0
while i < 10
p(i)
i = i + 1
end
このプログラムの抽象構文木をいつものように求めると次のような姿をしていることがわかります。
["stmts",
["var_assign", "i", ["lit", 0]],
["while",
["<", ["var_ref", "i"], ["lit", 10]],
["stmts",
["func_call", "p", ["var_ref", "i"]],
["var_assign", "i", ["+", ["var_ref", "i"], ["lit", 1]]]]]]
新たに登場したラベルは"while"
です。
while
文の抽象構文木も、意味を考えながら眺めれば見た目ほど難しくはありません。
["while",
条件式,
ループ内のコード本体]
では、MinRubyのwhile
文をRubyで実装していきましょう。 if
文と同様、while
文も「Rubyのwhile
文」を使って実装できます。
def evaluate(tree, env)
case tree[0]
when "while"
while evaluate(tree[1], env)
evaluate(tree[2], env)
end
...(略)...
end
end
これでおしまいです。目標として使ったサンプルプログラムを動かしてみてください。
case
文は?
もうひとつ、MinRubyにはcase
文という分岐もありました。次のサンプルプログラムを目標にしながらcase
文も実装してみましょう。
case 42
when 0
p(0)
when 1
p(1)
else
p(2)
end
このプログラムの抽象構文木はこうなります。
["if",
["==", ["lit", 42], ["lit", 0]],
["func_call", "p", ["lit", 0]],
["if",
["==", ["lit", 42], ["lit", 1]],
["func_call", "p", ["lit", 1]],
["func_call", "p", ["lit", 2]]]]
あれ、新しいラベルがありませんね? この抽象構文木を見る限り、このcase
文は次のif
文によるプログラムとまったく同じです。
if 42 == 0
p(0)
else
if 42 == 1
p(1)
else
p(42)
end
end
ということで、MinRubyのcase
文のために新たに実装することは何もありません。 現状のMinRubyインタプリタで先ほどのcase
文のプログラムを実行してみてください。
にしても、同じ抽象構文木になってしまうというのに、case
文なんて必要なんでしょうか? if
文だけあればいいのではないでしょうか?
確かに、抽象構文木が同じなのだから、MinRubyを実装しているRubyから見ればif
文もcase
文も同じです。 minruby_parse
は、case
文を見たらif
文の入れ子として抽象構文木を作って返すようにできています。
しかし、プログラムの書き方や見た目が違うのは、MinRubyプログラムを書く人間にとっては大きな違いがあります。 if
の入れ子が深くなれば、分岐の条件を間違える可能性も高くなるでしょう。 そんなときにcase
文を使ってすっきり書けるような問題もたくさんあるのです。
このように、構文解析の段階で他の言語機能を使ったプログラムに変換して扱うものを糖衣構文や構文糖と言います。 文字通り、口当たりのよい構文で他の構文をくるむというわけです。
この連載では糖衣構文を紹介するためにMinRubyのcase
文を糖衣構文として実装しましたが、 本当のRubyのcase
文は単純な糖衣構文ではありません。
まとめ
今回までで、四則演算と変数、それに分岐とループのあるプログラムを実行できるMinRubyインタプリタを実装しました。 現状のソースコードを全部載せておきます。
require "minruby"
def evaluate(tree, env)
case tree[0]
when "lit"
tree[1]
when "+"
evaluate(tree[1], env) + evaluate(tree[2], env)
when "-"
evaluate(tree[1], env) - evaluate(tree[2], env)
when "*"
evaluate(tree[1], env) * evaluate(tree[2], env)
when "/"
evaluate(tree[1], env) / evaluate(tree[2], env)
when "func_call" # 仮の実装
p(evaluate(tree[2], env))
when "stmts"
i = 1
last = nil
while tree[i]
last = evaluate(tree[i], env)
i = i + 1
end
last
when "var_assign"
env[tree[1]] = evaluate(tree[2], env)
when "var_ref"
env[tree[1]]
when "if"
if evaluate(tree[1], env)
evaluate(tree[2], env)
else
evaluate(tree[3], env)
end
when "while"
while evaluate(tree[1], env)
evaluate(tree[2], env)
end
end
end
# ① プログラムの文字列を読み込む
str = minruby_load()
# ② プログラムの文字列を抽象構文木に変換する
tree = minruby_parse(str)
# ③ 抽象構文木を実行(計算)する
env = {}
evaluate(tree, env)
ここまでくれば、もう「プログラミング言語処理系を作った」といっても過言ではありませんが、 もうひとつ重要な機能が足りません。それは関数です。 次回は、この連載で最大の難関になるかもしれない、関数の実装を紹介する予定です。
練習問題
1. 帰ってきたFizzBuzz
第2回の練習問題2や練習問題3で書いたプログラムをMinRubyで動かしてみてください。
また、if
文やwhile
文を使う好きなプログラムを書いて、 MinRubyとRubyの両方で動かしてみてください。 たとえば、階乗を計算するプログラム(1 * 2 * 3 * ...
を計算するプログラム)や、 素数を順番に出力するプログラム(2
、3
、5
、7
、11
...と表示するプログラム)などが 書けると思います。
2. begin ... end while
の実装
本物のRubyには2種類のwhile
文があります。1つは、本文で述べたwhile
文です。
i = 10
while i > 0
p(i)
i = i - 1
end
もう1つは、begin ... end while
で条件が後に書かれるwhile
文です。
i = 10
begin
p(i)
i = i - 1
end while i > 0
これら2つのプログラムは同じ動きをします。
違いは、最初に条件判定を行うかどうかです。 普通のwhile
文はまず最初に条件判定を行いますが、 begin ... end while
文は最初の条件判定を行いません。 最初のi = 10
をi = 0
やi = -1
に置き換えると違いがわかります。
begin ... end while
をMinRubyに実装してみましょう。
なお、実装を開始する前にminruby
ライブラリを更新する必要があります。 gemでインストールした人はgem update
コマンドを実行してください。 ファイルをダウンロードした人は再度ダウンロードして、上書きしてください。
ヒント1:上記のサンプルプログラムをminruby_parse
すると、 while2
というラベルが出てきます。これを実装してください。
ヒント2:普通のwhile
文を実装したときと同じように、 begin ... end while
文自身を使って実装するのでもよいですが、 普通のwhile
文を使ってbegin ... end while
文を実装してみると、 両者の違いをよりいっそう理解できると思います。
3. case
文の意味
MinRubyのcase
文とRubyのcase
文の意味の違いを見るために、 次のプログラムをMinRubyとRubyの両方で実行して、挙動の違いを確認してください。
case p(42)
when 0
p(0)
when 1
p(1)
else
p("others")
end
そして、MinRubyでは何が起きているか、どうしてこのような違いが起きたかを 考えてみてください。
ヒント:関数p
は引数の値をそのまま返り値とします。
第5回の練習問題の答え
1. おさらい
略
2. 足し算のカウント
def evaluate(tree, env)
case tree[0]
when "+"
env["plus_count"] = env["plus_count"] + 1
evaluate(tree[1]) + evaluate(tree[2])
...(略)...
end
end
3. デバッグのコツ
def evaluate(tree, env)
case tree[0]
else
p("error")
pp(tree)
raise("unknown node")
...(略)...
end
end
(raise("...")
と書くことで、プログラムの動作を停止させることができます。)
この連載の記事
-
第9回
プログラミング+
インタプリタの完成、そしてブートストラップへ -
第8回
プログラミング+
関数を実装する(後編) -
第7回
プログラミング+
関数を実装する(前編) -
第5回
プログラミング+
Rubyで変数付き電卓を作ってみる -
第4回
プログラミング+
Rubyで電卓を作る -
第3回
プログラミング+
Rubyで「木」を扱う -
第2回
プログラミング+
Ruby超入門 (後編) -
第1回
プログラミング+
Ruby超入門(前編) -
プログラミング+
Rubyで学ぶRuby<目次> - この連載の一覧へ