こんにちは。 機能限定版のRubyインタプリタ(MinRubyインタプリタ)を作りながらRubyとプログラミングを学ぶ連載、今回は前回に引き続き「関数」の実装です。
前回は、関数のうち、MinRubyインタプリタに最初から機能として用意しておく「組み込み関数」を実装しました。 最初に関数名のための環境を作り、そこにp
などの基本的な関数の定義を組み込んで、それをMinRubyプログラムから呼び出せるようにしました。
現状のMinRubyインタプリタは次のようになっています。
require "minruby"
def evaluate(tree, genv, lenv)
case tree[0]
when "lit"
tree[1]
when "+"
evaluate(tree[1], genv, lenv) + evaluate(tree[2], genv, lenv)
when "-"
evaluate(tree[1], genv, lenv) - evaluate(tree[2], genv, lenv)
when "*"
evaluate(tree[1], genv, lenv) * evaluate(tree[2], genv, lenv)
when "/"
evaluate(tree[1], genv, lenv) / evaluate(tree[2], genv, lenv)
when "stmts"
i = 1
last = nil
while tree[i]
last = evaluate(tree[i], genv, lenv)
i = i + 1
end
last
when "var_assign"
lenv[tree[1]] = evaluate(tree[2], genv, lenv)
when "var_ref"
lenv[tree[1]]
when "if"
if evaluate(tree[1], genv, lenv)
evaluate(tree[2], genv, lenv)
else
evaluate(tree[3], genv, lenv)
end
when "while"
while evaluate(tree[1], genv, lenv)
evaluate(tree[2], genv, lenv)
end
when "func_call"
args = []
i = 0
while tree[i + 2]
args[i] = evaluate(tree[i + 2], genv, lenv)
i = i + 1
end
mhd = genv[tree[1]]
if mhd[0] == "builtin"
minruby_call(mhd[1], args)
else
# 埋める(次回)
end
end
end
# ① プログラムの文字列を読み込む
str = minruby_load()
# ② プログラムの文字列を抽象構文木に変換する
tree = minruby_parse(str)
# ③ 抽象構文木を実行(計算)する
genv = { "p" => ["builtin", "p"] }
lenv = {}
evaluate(tree, genv, lenv)
今回は、まずMinRubyプログラムの中で関数を定義できるようにし、 それから上記の「# 埋める(次回)
」となっている部分を埋めることで、その関数を同じプログラムで実行できるようにします。 インタプリタを開発する人だけでなく、インタプリタを使う側の人が関数を定義できるようにしようというわけです。
インタプリタを使う側の人が定義する関数のことを、「ユーザ定義関数」といいます。 難しそうに聞こえるかもしれませんが、ようするに、これまでdef
文を使って定義してきた関数が「ユーザ定義関数」です。 したがって今回の課題は、MinRubyインタプリタにdef
文を実装することだといえます。
def
文を使った関数の定義をサポートするためには、 def
文の抽象構文木に対応する処理をevaluate
に追加しなければなりません。 具体的には、関数名の環境genv
に新しく関数を登録する必要があります。
それから、そのようにして定義された関数をプログラムの中で使えるようにする必要があります。 これには、環境genv
から、関数の定義を呼び出してこなければなりません。
関数名の環境genv
から関数の定義を呼び出す部分は、組み込み関数のときと同じでいいように思えるかもしれません。 しかし、実は「引数」の扱いに、組み込み関数のときにはなかった注意が必要です。 その注意を理解するために、まずは「引数」の区別について話をしておきましょう。
仮引数と実引数
プログラミング言語で関数の引数について考えるときには、仮引数と実引数という区別をします。
仮引数というのは、関数定義に出てくる引数のことです。 つまり、def add(x, y)
のx
やy
です。
一方、実引数というのは、関数呼び出し側で引数の場所に書くものを指します。 つまり、add(10, 30 + 2)
の10
や30 + 2
のことです (この例のように、実引数は値のこともあれば、評価前の式のこともあります)。
このように区別はしますが、引数というものが2種類あるわけではありません。 「呼び出される側」の呼び方が仮引数、「呼び出す側」の呼び方が実引数というだけの違いです。
ただし、いまのようにインタプリタを実装しているときは、 仮引数と実引数を区別しておかないとユーザ定義関数の実装で混乱してしまいます。 「呼び出される側」と「呼び出す側」のどちらの立場で引数について考えているのかを区別するために、 それぞれ言葉だけ覚えておいてください。
関数定義を実装する
それでは実装に入りましょう。 まずは関数定義、つまりdef
文を実装していきます。
いつものように、まずは抽象構文木を見てみます (抽象構文木とその確認方法は第4回の記事を参照してください)。 例として下記のような関数定義を考えましょう。
def add(x, y)
x + y
end
このdef
文の抽象構文木は次のようになっています。
["func_def",
"add",
["x", "y"],
["+", ["var_ref", "x"], ["var_ref", "y"]]]
絵にするとこうです。
これを見る限り、抽象構文木そのものはそれほど複雑ではありませんね。 ["func_def", 関数名, 仮引数名の配列, 関数本体]
という構造になっていることが見て取れると思います。
問題は、この抽象構文木をevaluate
でどのように扱うかです。 MinRubyインタプリタの利用者がdef
文でやりたいことは新しい関数の定義なので、 この抽象構文木の「関数名」に対応するエントリを環境genv
に追加する必要があります。
def evaluate(tree, genv, lenv)
case tree[0]
when "func_def"
genv[tree[1]] = ["user_defined", tree[2], tree[3]]
...(略)...
end
end
抽象構文木tree
は、先ほど確認したように["func_def", 関数名, 仮引数名の配列, 関数本体]
です。 したがって、ここでハッシュgenv
に追加しているのは次のような対応です。
関数名 => ["user_defined", 仮引数名の配列, 関数本体]
あとでMinRubyプログラムの中でこの関数名が使われたときに、組み込み関数ではなくユーザ定義関数として扱いたいので、 組み込み関数の関数定義を表す"builtin"
と区別するためのラベルとして"user_defined"
という文字列を使うことにしました。
関数定義そのものは、このラベル"user_defined"
に続けて「仮引数名の配列」と「関数本体」を並べるだけでおしまいです。 新しい関数を定義しても、その場で関数の中身が実行されるわけではありませんよね。 関数定義というのは、あくまでも「関数の定義」であり、「この関数はこういうことをするものとして覚えておけ」とインタプリタに命令するだけです。 そのため、このように素直に関数定義を覚えておくだけで十分なのです。
ユーザ定義関数の呼び出し
次は、ユーザ定義関数を関数名の環境から呼び出して実行する部分を実装します。 現状のMinRubyインタプリタはこんなふうになっているはずです。
def evaluate(tree, genv, lenv)
case tree[0]
when "func_def"
genv[tree[1]] = ["user_defined", tree[2], tree[3]]
when "func_call"
args = []
i = 0
while tree[i + 2]
args[i] = evaluate(tree[i + 2], genv, lenv)
i = i + 1
end
mhd = genv[tree[1]]
if mhd[0] == "builtin"
minruby_call(mhd[1], args)
else
# 埋める(ユーザ定義関数の呼び出しの実装)
end
...(略)...
end
end
MinRubyプログラムの中で関数が呼び出されていて、しかもそれがユーザ定義関数だったら、 mhd
には["user_defined", 仮引数名の配列, 関数本体]
という配列が入っています (そういう配列がgenv
に登録されるように、前節で"func_def"
の場合の処理を実装したのでしたね)。
いまやりたいのは、この配列の最後の要素である「関数本体」を評価することです。 ここで、「関数本体」のようすを思い出しておきましょう。 関数定義が次のような内容だったら、
def add(x, y)
x + y
end
抽象構文木は次のようになっています。
["func_def",
"add",
["x", "y"],
["+", ["var_ref", "x"], ["var_ref", "y"]]]
この木の最後の葉が「関数本体」でした。
["+", ["var_ref", "x"], ["var_ref", "y"]]
いまは、このような関数本体を、MinRubyプログラムで関数が呼び出されるときに指定されていた引数、 つまり実引数を使って評価したいわけです。
実引数は、前回組み込み関数を実装したときに、while
を使って配列args
に集めるようにしてあります。 実引数は、関数本体のどこで使えばいいでしょうか? いうまでもありませんね。 関数を定義したときに使った引数、つまり仮引数が出てくる箇所で、対応する実引数が使われるようにする必要があります。
言い換えると、いまやるべきことは「仮引数のそれぞれに、対応する実引数を覚えさせた状態で、関数本体を評価する」です。 何かを覚えさせるということは、変数の出番です。 現在、仮引数名の配列はmhd[1]
に、実引数の配列はargs
に入っているので、 それぞれの対応を順番に変数の環境lenv
へと入れていきます。
params = mhd[1]
i = 0
while params[i]
lenv[params[i]] = args[i]
i = i + 1
end
この状態で関数本体を評価すれば、仮引数の箇所で実引数が使われます。 関数本体はmhd[2]
に入っているので、次のようにすればいいでしょう。
evaluate(mhd[2], genv, lenv)
これでようやく、ユーザ定義関数のできあがりです。まとめて書くと以下のようになります。
def evaluate(tree, genv, lenv)
case tree[0]
when "func_call"
args = []
i = 0
while tree[2][i]
args[i] = evaluate(tree[2][i], genv, lenv)
i = i + 1
end
mhd = genv[tree[1]]
if mhd[0] == "builtin"
minruby_call(mhd[1], args)
else
params = mhd[1]
i = 0
while params[i]
lenv[params[i]] = args[i]
i = i + 1
end
evaluate(mhd[2], genv, lenv)
end
...(略)...
end
end
さっそく動作確認してみましょう。次のようなプログラムを用意してください。
def add(x, y)
x + y
end
p(add(1, 1))
これをprogram1.rb
のような名前でファイルに保存し、おそるおそる完成したMinRubyインタプリタで実行してみてください。
C:¥Ruby > ruby interp.rb program1.rb ⏎
2
C:¥Ruby >
こんなふうに2
が出たら成功です!
変数のスコープ
実は、現状のMinRubyにおける関数の実装は、本物のRubyの関数とは少し意味が違っています。
少しややこしいのですが、変数は関数ごとに定義されます。 例えば次のRubyプログラムでは、x
という名前の変数が関数foo
の定義の中と外でそれぞれ使われていますが、これらは本物のRubyでは別々の変数です。
def foo()
x = 0
p(x)
end
x = 1
foo() #=> 0
p(x) #=> 1
プログラムとしては、まず関数foo
を定義し、その外側にあるx
に1を代入して、それから関数foo
を呼び出します。 呼び出された関数foo
では、x
に0が代入され、直後にそれを出力します。
本物のRubyでは、0を代入する「foo
内のx
」と、「foo
外のx
」とが、まったくの別変数です。 したがって、最初に1を代入した「foo
外のx
」には、foo
内でx
に0を代入しても影響がありません。 関数foo
の呼び出しから帰ってきた後で出力されるのは、「foo
外のx
」です。 これは最初に代入した通り、1となります。
一方、現状のMinRubyインタプリタで上記と同じプログラムを実行すると、0が2回出力されてしまうことでしょう(ぜひ確かめてみてください)。
原因は、関数foo
の中の代入x = 0
で、関数の外側の変数x
が書き換えられてしまっていることです。 MinRubyインタプリタでは、今のところ、関数の中のx
と外のx
が同じ変数として扱われてしまっているのです。 変数が参照できる範囲のことを変数のスコープといいますが、現状のMinRubyでは変数のスコープが関数定義の中で閉じていないのです。
ではどうすればよいかというと、関数の中で新たに変数名のための環境を用意します。 関数定義の中と外で変数のための環境を変えればいいというわけです。
def evaluate(tree)
case tree[0]
when "func_call"
args = []
i = 0
while tree[2][i]
args[i] = evaluate(tree[2][i], genv, lenv)
i = i + 1
end
mhd = genv[tree[1]]
if mhd[0] == "builtin"
minruby_call(mhd[1], args)
else
new_lenv = {} # 挿入
params = mhd[1]
i = 0
while params[i]
new_lenv[params[i]] = args[i] # lenv を new_lenv に変更
i = i + 1
end
evaluate(mhd[2], genv, new_lenv) # lenv を new_lenv に変更
end
...(略)...
end
end
今までの実装では、関数呼び出しの文脈における変数の環境lenv
を、そのままevaluate
に渡していました。 これを上記では変更して、new_lenv
という新しい環境を作ってから、それに仮引数をセットした上でevaluate
に渡すようにしています。
こうすることで、関数の外の環境はlenv
、関数の中の環境はnew_lenv
というように環境を分けることができます。 変数のスコープを区別することは、インタプリタの実装の観点から言えば、関数を呼び出すたびに新しい環境を作って渡すという意味にほかなりません。
なお、関数名の環境はどの関数の中でも共通です。 MinRubyの実装では関数名の環境をgenv
と名づけましたが、先頭のgはglobal(大域的)のg(つまりどこでも共通)を表しています。 一方、変数名の環境lenv
の先頭のlは、local(局所的)のl(つまり関数という局所ごとで異なる)を表しています。
まとめ
ついに関数の定義と呼び出しをひととおり実装できました。
本連載の最終目標は、自分で作ったMinRubyインタプリタを使って、自分で作ったMinRubyインタプリタを動かすこと(ブートストラップ)です。 そのためには、MinRubyインタプリタで使っている言語機能をすべて実装する必要があります。 まだ未実装なのは、残すところ配列とハッシュだけです。 次回はこれらを実装し、ブートストラップまでやる予定です。ということで、次回、最終回。こうご期待。
練習問題
1. フィボナッチ関数
次の数列を見てください。
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
これは、最初の2つが0と1で、それ以降は「直前2つの値の和」になっています(例えば2は直前2つの1と1の和、3は1と2の和、5は2と3の和)。これをフィボナッチ数列といいます。
フィボナッチ数列のx
番目を計算する次のような定義の関数を「フィボナッチ関数」といいます。
def fib(x)
if x <= 1
x
else
fib(x - 1) + fib(x - 2)
end
end
MinRubyインタプリタでフィボナッチ関数を動かしてみてください。
フィボナッチ関数は、プログラミング言語処理系の実装のテストおよびベンチマークプログラムとして有名です。また、プログラミング言語の入門書においても再帰呼び出しの典型的な例題です。
ヒント:第4回の練習問題1で出てきた比較式の実装が必要です。
2. 相互再帰
ある関数と別の関数がお互いを呼び出し合うことを、相互再帰といいます。 たとえば、次の関数even?
とodd?
は相互再帰しています。
def even?(n)
if n == 0
true
else
odd?(n - 1)
end
end
def odd?(n)
if n == 0
false
else
even?(n - 1)
end
end
MinRubyインタプリタでeven?(2)
やeven?(3)
の結果を見てみてください。 そして、これらの関数がどのように動いているかを考えてみてください。
even?
とodd?
は、関数名の最後に?
がついていますが、 この?
は何か特殊な言語機能というわけではありません。 実は、Rubyの関数名は?
や!
で終わることができます。 したがって、even?
やodd?
は、それ全体で単なるふつうの関数名です。
Rubyでは、真偽を判定する関数には、?
で終わる名前を付ける慣習があります。 ここでもその慣習にならって、「偶数か否か」を判定する関数にはeven?
、 「奇数か否か」を判定する関数にはodd?
という名前を付けました。
なお、何かしらの注意が必要な関数(たとえば、何かを破壊的に書き換える関数)には !
を付けるという慣習があります。
第7回の練習問題の答え
1. 組み込み関数raise
の追加
genv
にraise
のエントリを追加するだけです。
genv = {
"p" => ["builtin", "p"],
"raise" => ["builtin", "raise"],
}
2. 独自の組み込み関数の追加
(略)
この連載の記事
-
第9回
プログラミング+
インタプリタの完成、そしてブートストラップへ -
第7回
プログラミング+
関数を実装する(前編) -
第6回
プログラミング+
分岐を実装する -
第5回
プログラミング+
Rubyで変数付き電卓を作ってみる -
第4回
プログラミング+
Rubyで電卓を作る -
第3回
プログラミング+
Rubyで「木」を扱う -
第2回
プログラミング+
Ruby超入門 (後編) -
第1回
プログラミング+
Ruby超入門(前編) -
プログラミング+
Rubyで学ぶRuby<目次> - この連載の一覧へ