関数を実装する(前編)

アスキー 2016年12月07日(水)12時00分配信

こんにちは。 RubyでRubyを実装していく連載もいよいよクライマックスが近づいてきました。

前回までに完成したのは、四則演算ができて、変数を扱えて、分岐やループがあるプログラムを書けるインタプリタです。 前回は、そのインタプリタを使って、それなりにプログラムらしいプログラム、たとえばFizzBuzzのようなプログラムを書いて実行する段階までこぎつけました。 ここまでくれば、どんなプログラムだって書けそうに思えるかもしれません。

しかし、現状のインタプリタで現実的に書けるプログラムは、実はFizzBuzzくらいが限界です。 さらに複雑なプログラム、それこそインタプリタのようなプログラムを書くには、まだまだ足りない機能がたくさんあります。

そのような機能のうち、それなりに複雑なプログラムを書くときに特に必須になるのは「関数」です。 関数がないと、この連載ではすっかりお馴染みの「木をたどるプログラム」すら簡単には書けませんでしたね。

そこで次はMinRubyインタプリタに関数を実装していきましょう。 この連載でいちばんの難所になると思うので、ちょっと気合を入れて読んでください。 記事としても長くなるので、前編と後編の2回に分けてお届けします。

今回と次回で関数の実装を終えてしまえば、これまで出てきた「Rubyの機能」がだいたい実装できたことになります(残るはハッシュと配列くらいです)。 つまり、連載の目標としてきたMinRubyインタプリタの完成までもう一息です。 さらにいうと、本物のRubyインタプリタの代わりにMinRubyインタプリタ自体を使ってMinRubyインタプリタを実行する、そんな不思議な世界までもう一歩です!

ユーザ定義関数と組み込み関数

ひとくちに関数と言っても、実は2種類あります。 1つは、から始まる文を使って自分で定義する関数です。 これは「ユーザ定義関数」と呼ばれます。 連載の第3回で学んだのは、このユーザ定義関数です。

もう1つは、インタプリタに最初から用意されている「組み込み関数」です。 組み込み関数も、実は初めて見るものではありません。 第1回から何度となく使ってきたは、本物のRubyに最初から用意されている組み込み関数のひとつです。

は、Rubyに最初から定義されている点を除けば、他の関数となんら変わるところがないただの関数です。 過去の記事では、説明をぼかして「命令」のような呼び方をしていたこともありますが、関数と違う何か特別なものというわけではありません。

インタプリタに実装するという観点では、関数を定義する部分と使う部分の両方を実装しないといけないユーザ定義関数のほうが少しだけ説明する内容が多いので、まずは組み込み関数のほうから実装していきます。

関数名の環境

連載の第3回でユーザ定義関数を学んだときは、定義する関数に名前をつけて、その名前をプログラムの中で使いました。 組み込み関数にも、たとえばのような名前がついていて、使うときにはその名前で呼び出します。 ということは、インタプリタには、関数の名前を定義されている中身と一緒に覚えておく仕組みが必要だということです。

インタプリタで名前と中身の対応を覚えておく仕組みといったら、第5回に出てきた環境です。 変数についての環境は、変数名と値とを対応付けるハッシュでした。 同じように、関数名の環境は、関数名と関数定義を対応付けるハッシュです。

第5回では、MinRubyインタプリタに変数を実装するため、の引数にという名前をつけた環境を追加しました。 関数名を管理する環境もの引数に追加すればいいのですが、変数名の環境と区別できなければなりません。 関数名の環境にはという別の名前をつけ、変数名の環境もからという名前に改名しておきましょう。

上記では一部だけを示していますが、のなかでを再帰的に呼んでいる箇所をすべて変更してください。 これでMinRubyインタプリタに関数を導入する準備ができました。

7

自前のを組み込む

さっそく組み込み関数をMinRubyインタプリタに追加してみましょう。 に、関数名に対応するエントリを最初から入れておくだけです。 (のほうは、いままでのと同様に空のままでかまいません。)

が関数名、が関数の決め打ちの定義です。

というラベルは、この関数定義が「組み込み関数」であることを表しています。 当然、「ユーザ定義関数」に対応するラベルもありますが、それは次回の記事でユーザ定義関数を実装するときにお話します。

関数定義に出てくるは、定義されるMinRubyの関数と同じ名前なので紛らわしいかもしれませんが、本物のRubyのほうの関数を表しています。 つまり、この関数定義は、 「MinRubyで組み込み関数を呼んだときの処理は、 本物のRubyの関数に実行させる(つまり丸投げする)」 という意味です。 (「そんなのいかさまだ!」と感じたなら、第6回を読み直しましょう。)

7

関数呼び出しを実装する

ここまでやったことを整理しておきましょう。

  1. 関数名のための環境を用意した
  2. その環境に、組み込み関数を追加した(いまのところのみ)

次は、組み込んだ関数をプログラムで使えるようにする必要があります。 つまり、関数がプログラムに出てきたときにMinRubyインタプリタがそれを評価できるように、にルールを書いていきます。

過去の連載と同様に、まずは関数呼び出しの抽象構文木を見てみましょう。 それを見ながら実装を考える作戦です。

いままで(本物の)Rubyの組み込み関数には引数を1つしか渡してきませんでしたが、 実はは上記の2つめの例のように複数の引数が取れることになっていて、 その場合には引数として渡した各値が行区切りで出力されることになっています。

図にすると、それぞれ次のとおり。

7
7

関数呼び出しの抽象構文木は、という構造になっているようです。 ということは、の文に、というラベルに対応する処理を書けばよさそうですね。

ところが、前の記事で、というラベルに対応する部分にはすでに以下のような仮実装をデバッグのために入れていました。

この仮実装では、「ラベルがのときに、抽象構文木の3つめの枝(先ほどの例だと)を評価した結果を、Rubyので表示する」という動作になっています。 この動作は、「MinRubyのに引数を1つだけ渡した場合」にそうなっていてほしい結果ですよね?

つまり、ある意味でこの仮実装は、「という名前の関数に引数を1つだけ渡す場合」に特化した関数呼び出しの実装になっていたと言えます。 この仮の実装を、しかるべき数の引数を伴って呼び出されたどんな関数に対してもきちんと動作する実装にしていきましょう。

引数をすべて評価する

まずは引数の評価の改修です。 関数によっては、1つだけでなく複数の引数を指定できると便利なこともあるでしょう。 先ほどの抽象構文木を見ると、複数の引数は、、...の位置に入るので、 まずは文を使って各引数を順番にしていくようにします。

各引数の評価結果は、という配列へ順番に入れるようにしました。

組み込まれている定義を関数名から取ってくる

次に、関数名を管理している環境から、評価すべき関数の定義を引っ張ってきます。 は次のような形をしたハッシュでした。

ハッシュからエントリの値を取り出すには、たとえばのようにすればよいのでした(忘れた人は第5回の記事を見ましょう)。 いま、から定義を引っ張り出してきたい関数の名前は、抽象構文木の2つめの枝()に入っているので、 次のようにすれば関数名に対応する関数定義を取り出してこれます。

これで、呼び出そうとしているのが組み込み関数なら、その定義であるという配列をに取り出せたことになります。 つまり、の先頭がなら、いま処理している抽象構文木は組み込み関数です。

に手を入れて、の先頭()を見て動作を変えるようにしておきましょう。

評価した引数に、指示されたRubyの関数を適用する

ふたたび、ここまでやったことを整理しておきます。

  1. 関数名のための環境を用意した
  2. その環境に、組み込み関数を追加した(いまのところのみ)
  3. MinRubyプログラムで組み込み関数が使われていると、抽象構文木はのような形になるので、というラベルに対する処理をに追加した
  4. では、まず引数を順番に評価して、それをという配列に入れた
  5. 関数の定義を、関数名のための環境から取り出してきて、それをに入れた

MinRubyプログラムで呼び出されたのが組み込み関数なら、には、その組み込み関数の動作を丸投げすべき「本物のRubyの関数」の名前が入っています。 MinRubyの組み込み関数を処理するには、そこで指示されている本物のRubyの関数を、引数にを与える形で呼び出す必要があります。

そのために必要な道具は、パッケージの中にという組み込み関数を用意したおきました(ちょっとずるいですね)。 この関数を、たとえば次のように本物のRubyで呼び出すと、

次のように本物のRubyで呼び出したのと同じ動きをします。

この関数を使えば、MinRubyの組み込み関数の呼び出しは以下のように実装できます。

これで組み込み関数の仮実装をきちんと実装し直す作業は終わりです。 今までどおりにMinRubyプログラムでが呼べることを確認しておきましょう。 次のプログラムをMinRubyインタプリタで実行してみてください。

前回までの仮実装ではの引数を1個に限定してしまっていましたが、 今回の改修でには複数の引数を渡せるようになっているはずですから、それも試してみましょう。

それぞれの値が行区切りで出力されたでしょうか? ぜひMinRubyインタプリタで確認してみてください。

7

まとめ

今回は、関数の実装の第一弾として、組み込み関数の呼び出しを実装しました。 具体的には、関数名のための環境をサポートし、組み込み関数(引数が1つとは限らない)の抽象構文木を評価できるようにを拡張しました。

現状のMinRubyインタプリタのコードは次のようになっています。

次回はこれにユーザ定義関数の実装をします。 上記のコードで「埋める(次回)」というコメントの部分を実装していきます。

練習問題

1. 組み込み関数の追加

組み込み関数を増やしてみてください。 例えば、エラーを出して終了する組み込み関数を増やしてください。

2. 独自の組み込み関数の追加

ターゲット言語の組み込み関数を定義するのに、 ホスト言語の組み込み関数をそのまま使いました。 しかし、ホスト言語のユーザ定義関数をターゲット言語の組み込み関数にすることもできます。 たとえば、MinRubyインタプリタのソースコードを

とすれば、次のようなプログラムを動かせるようになるはずです。

FizzBuzzを組み込み関数として用意してみてください。また、独自の面白い組み込み関数を考えてみてください。

第6回の練習問題の答え

1. 帰ってきたFizzBuzz

2. の実装

3. 文の意味

構文糖を手で分解してみればわかります。

の後に出てくる式がすべてのの条件式に複製されてしまうので、比較式を評価するたびに関数が呼ばれてしまいます。

これを防ぐには、いったん変数に入れるという手があります。

ただ、こんな構文糖は嫌ですよね。本格的にインタプリタを作るとしたら、構文糖を展開する際に自動的に変数を用意して関数呼び出しを複製しないようにする必要があるでしょう。 構文糖はユーザにとっては便利で素敵なものですが、下手に設計すると意外な副作用をもたらすことがあるので、言語設計者は慎重になるべきでしょう。

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

【あわせて読む】

    最終更新: 2016年12月07日(水)12時00分

    【関連ニュース】

    【コメント】

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

    【あなたにおススメ】