Rubyで変数付き電卓を作ってみる

アスキー 11月09日(水)09時00分配信

こんにちは。Rubyを作りながらRubyを学ぼうという連載企画、第5回です。

前回は、Rubyで四則演算インタプリタ(電卓)を作りました。 今回からは、このインタプリタを拡張して、いよいよMinRubyインタプリタを作っていきます。

まずは前回の四則演算インタプリタを改造して、入力をファイルに書いて渡せるようにしましょう。 本物のRubyインタプリタも、ファイルに書かれたプログラムを読み取って実行できます。 MinRubyインタプリタも、ファイルに書いたMinRubyプログラムを実行するようにしたいので、 ベースにする前回の四則演算インタプリタもそのように改造します。

ruby5

そのあとで、四則演算インタプリタに、今回の目標である「変数」を導入していきます。 連載の第1回では、変数のことを「値を覚えてくれる人」であると説明しました。 それを作るわけですから、値を覚えておくための仕組みが何かしら必要です。 Rubyで使えるそのような仕組みとして、「ハッシュ」というものを学びます。

ファイルから入力を読み取る

電卓のソースコード(再掲)

前回作った電卓のソースコードは次のようなものでした (長ったらしくなるので少し改変しています)。

見てのとおり、現在の四則演算インタプリタでは、ユーザが手で入力した計算式を読み込み(①)、それを計算の木にして(➁)から評価し(③)、その結果を本物のRubyの関数で出力しています(④)。 覚えていない人は第4回を復習しておいてください。

ソースコードを読み込むようにする

まずは、①の部分の改造です。四則演算インタプリタでは計算式をコンソールに直接打ち込んでいましたが、計算式をソースコードとしてファイルから読み込むようにしましょう。

としていたところを、次のように書き換えてください。

は筆者のライブラリが提供している関数で、コマンドラインに渡されたファイルを読み込んで文字列として返します。

デバッグ出力関数を実装する

次は、計算結果を出力する④の部分を改造します。

現在の四則演算インタプリタでは、計算の木を評価してから、その結果を本物のRubyの関数を使って出力しています。 しかし、ここで四則演算インタプリタの関数として、式を評価して内容を表示してくれるデバッグ出力関数を作っておくことにします。

名前がどちらもなので混乱してしまうかもしれませんが、四則演算インタプリタへの入力で本物のRubyの関数を使うわけにはいかないので、 ソースコードとして計算式を渡すときには四則演算インタプリタの関数であるを実装して、それを使う必要があります。

とはいえ、関数の実装については次々回できちんと詳しく解説するので、今回は内容を深く気にせずにの定義の最後の部分に次のような仮の実装を入れておいてください。 (かいつまんで何をしているか説明すると、木の一部として「」を表すような節があった場合、引数である「」を評価した結果を本物のRubyので表示しています。)

デバッグ関数を実装したことで、プログラム自身が計算結果を出力する命令を利用可能になったので、最後に本物のRubyで結果を出力することはもう不要です。 もともとの四則演算インタプリタに入れていた次の命令は消してください。

これで四則演算インタプリタに渡すソースコードの中でデバッグ出力関数を使えるようになりました。

四則演算インタプリタにソースコードを渡して実行する

ここまでの改造により、ファイルをソースコードとして四則演算インタプリタに渡せば、ファイルに書かれた計算式の結果が表示されるようになりました。 さっそく試してみましょう。 次の1行を書いたファイルをという名前で保存してください。

そして次のように実行してみてください。

念のために補足すると、はRubyプログラムではなく、四則演算インタプリタのプログラムですよ。 四則演算インタプリタのほうは、という部分で、本物のRubyにより実行しています。

ちょっとインタプリタっぽくなってきましたね!

ruby5

複数の式を扱えるようにする(複文)

計算式とプログラムの違い

ここで、計算式とプログラムの違いを考えてみます。計算式は、例えば次のような文字列で表されます。

プログラムは、例えば次のような文字列です。

プログラムには、計算式には出てこなかった変数や関数などが出てきます。が、実はもっと本質的な違いがあります。さらに簡単なプログラムを見てみましょう。

変数も関数もありませんが、これも立派なプログラムです。 計算するだけで何も出力しないので、まったく無意味なプログラムではありますが、それでも計算式とは本質的に異なるところが現れています。それは「式が複数ある」ということです。

計算式は、その1つの式を実行して1つの値になったら終わりです。一方プログラムは、そのような計算式を何個も並べることができ、それらを上から順番に実行していきます。 このように複数の計算式を並べたものを複文といいます。計算式とプログラムの大きな違いは、プログラムは全体として複文になっているという点です(なお、プログラム全体以外にも文の中身や関数定義の中身も複文になっています。これらについてはおいおい出てきます)。

現状のインタプリタを改造して、単一の計算の木ではなく複文を扱うようにしていきましょう。

複文の抽象構文木

まず、複文とは何かを観察するため、先の無意味なプログラムをしてみましょう。 次のようなRubyプログラムを書いて実行してみてください(改行は見やすさのために入れているのではありません。この通りに入力する必要があります)。

次のような出力が表示されるはずです(上記のプログラムをというファイルに保存したとします)。

これらを木の図として表現すると、次のようになります。

ruby5

計算の木が3つ並んでいます。それぞれ、、です。 そして、これらを束ねたという名前の節が、木全体となっています。 はstatements(複文)の略です。 これは、「子どもの計算の木を左から順番に計算せよ」という意味を持つ木です。

複文の実装

関数を拡張して、複文に対応しておきましょう。 まずはという節に対するを追加します。 現状の四則演算インタプリタのの最後に次のように追記してください。

複文の木を表す配列の0番めにはという文字列が入っていて、 それ以降の要素に肝心の複数の文が入っています。 そこで1番めから最後までを順番に処理していきます。 順番に計算するには文を使うのでしたね。

はが配列の長さを超えるとを返します。 よって、という条件を使えば、「配列の1番目から終わりまで」という繰り返しを書くことができます。

あとは、複文の木が子どもとして持っている部分木を順次実行していけば出来上がりです。 これは再帰呼び出しで実装できます。

複文の実行

次のプログラムを実行してみましょう。というファイルで保存してください。

そして次のように実行してみてください。

3つの計算式の結果がそれぞれ表示されましたね!

変数を実装する

では、いよいよ変数の実装に入っていきます。

ひとくちに変数といっても、作らないといけない操作は2つあります。 変数を作る「変数代入」(のような記述)と、 変数を使う「変数参照」(のような記述)です。

まずは変数を作らないと使えないので、変数代入のほうから考えていきましょう。

変数代入の抽象構文木

複文のときのように、が作る抽象構文木の姿を見ながら考えることにしましょう。 次のような変数代入を含むプログラムを用意してください。

これを実行すると、こんな抽象構文木が得られます(上記のプログラムをという名前で保存した場合)。

図にすると次のようになります。

ruby5

左側のの部分木がに対応し、右側の部分木がに対応します。 つまり、の意味は「右側の部分木を計算した結果を、左側の変数名の変数に代入する」ということになります。

の実装をしていきましょう。例によってにを追加します。

さて、ここで手が止まります。どう実装したものでしょうか。

ハッシュ

代入は「この名前の変数に値を覚えさせろ」という、インタプリタに対する命令でした。 今私たちはインタプリタを作っているので、覚えておく立場になります。 変数名という文字列と、それに対応する値を対応付けて覚えておくにはどうすればいいでしょうか。

このようなときに、ハッシュという言語機能が使えます。 ハッシュとはまさに、値と値の対応表のようなデータ構造です (Ruby以外ではハッシュテーブル辞書連想配列などとも呼ばれます)。

ハッシュを作るには次のように書きます。

これで、という文字列に1という数を、という文字列に2という数を対応づけたハッシュができます。 しかし、このままでは別の場所で使えないので、ふつうは次のように、変数を用意してそこに作ったハッシュを代入します。

これで変数にハッシュが代入されました。とりあえずの中身をで見てみます。

に対応する値を読み出すには次のように書きます。

さらに、すでにあるハッシュに新たな対応を追加したり、すでにある対応の値を更新したりすることもできます。

環境:変数名と値の対応関係

では、ハッシュを使って、変数名と値の対応を覚えるようにしましょう。 インタプリタの業界では、この変数名と値の対応関係のことを環境といいます。

環境は、関数の中で、変数を定義したり参照したりするときに使います。 そのため、関数の中で常に参照できる必要があります。 そこで関数の引数を増やし、その増やした引数を環境の受け渡しに使うことにします。

環境は英語で「environment」なので、引数の名前はとしました。

関数の中では再帰的にを呼んでいるので、その部分も引数をとるように直す必要があります。 しかしその前に、一番最初にを呼ぶときに渡す環境を用意しましょう。 最初はどんなハッシュを環境として用意すればいいと思いますか?

ここでは、空のハッシュを作り、それをに渡すことにしました。 これは、「このインタプリタの実行を開始するときには何の変数も定義されていない」ことを表しています。

次は、関数の中で再帰的にを呼んでいる部分を改造していきましょう。

少し面倒くさいのですが、定義の部分をと書き換えるだけでなく、 関数を再帰呼び出しする全箇所で、受け取った環境をそのまま渡す必要があります。 いわばバトンリレーですね。 このようにすることで、関数の中で常に、その時点でのが参照できるようになります。

変数代入を実装する

環境を受け渡す仕組みが整ったところで、ようやく変数代入を実装できるときがきました。

この穴を埋めます。 変数代入の木はという形だったので、 は変数名、は式を表す部分木です。 まず、式を表す部分木を実行して値にします(再帰呼び出しですね!)。 その結果を、変数名に対応する値としてハッシュに書き込みます。

これで、ついに変数代入が実装できました。

ruby5

変数参照を実装する

代入は実装できたので、次は参照を実装しましょう。 やはり、まずは変数参照を含む次のプログラムをして、抽象構文木がどんな姿になるのかを見てみましょう。

これを実行すると、こんな抽象構文木になりました(上記のプログラムをという名前で保存した場合です)。

図にすると次のようになります。

ruby5

左のは先に出てきたものと同じです。 右のも似ていますが、右上にという木ができています。 これが変数参照を表しています。 ここでは、という変数名の変数が覚えている値を返す、という意味です。

を実装していきます。 といっても、環境のハッシュの中で、変数名に対応する値を読みだすだけです。 変数名はに入っているので、それでを引けばOK<3808>

これで変数参照も実装できました。

動作確認

では、変数代入と変数参照を使ったプログラムを動かしてみましょう。 次のプログラムをというファイル名で保存して、と実行して3が出れば成功です。

動きましたか?

まとめ

四則演算と複文と変数のあるプログラムを実行できるインタプリタを実装しました。 現状のソースコードを全部載せておきます。

今回、実質的に追加したのはととだけなので、 行数にするとたったの12行です。 しかし、なかなか濃密な12行と感じられるのではないでしょうか。 このインタプリタで、いろいろなプログラムを走らせてみてください。

次回は分岐を実装します。どんどんプログラムっぽくなっていきますのでお楽しみに。

練習問題

1. おさらい

第1回と第2回(変数のみ)のプログラムをMinRubyインタプリタで動かしてみてください。 たとえば、第2回で登場した次のを、

こんなふうに実行してみましょう。

期待通りの結果になるでしょうか?

2. 足し算のカウント

足し算を実行した回数を返す特殊な変数を追加してみてください。 次のように動けば正解です。

ヒント:の中で足し算を処理するところでは、は単にたらいまわしするだけでした。 そのときにをカウントアップするように変更すればOK<3808>です。

プログラムの高速化を考えるとき、どういう種類の計算をどのくらい行っているかを 数えたくなることが多々あります。これを行うツールをプロファイラといいます。 プロファイラの指標や実装には非常にさまざまな方法がありますが、 このように興味のある処理を実行した回数を数える特殊な変数を入れるのは、 最も原始的なプロファイラであると言えます。

3. デバッグのコツ

の再帰呼び出しに与える部分木を間違えると、よくわからないエラーが発生すると思います。 そんなときはの最初にを入れ、どこでエラーが起きているかを調べるといいのですが、普段はそのようにすると大量の出力が出て煩わしいでしょう。 そこで、がどのように扱っていいかわからない節に出会ったとき、そのことを報告させるようにするとよいでしょう。これを実装してみてください。

ヒント:文には、どの節にもマッチしなかった場合に実行すべき処理を示す 節を書くことができます(第4回参照)。これを利用してください。

第4回の練習問題の答え

1. 演算の追加 と 2. 比較式の追加

3. 最大の葉

アスキー
もっと見る もっと見る

【あわせて読む】

    最終更新: 11月09日(水)09時00分

    【関連ニュース】

    【コメント】

    • ※コメントは個人の見解であり、記事提供社と関係はありません。

    【あなたにおススメ】