JavaScriptでLispインタープリターを作ろう(7) ~関数の導入~
前回の記事で、Lispインタープリターは「順次・反復・分岐」のすべての制御構文をサポート出来るようになった。今回は、いよいよ関数を記述する機能を導入する。
「仮引数」と「実引数」
関数を記述する機能を導入する前に、JavaScriptを例にして、関数について一般的な説明をしておこう。たとえば以下のような関数があるとする。
function sample(a, b) { return a + b; }
このsample
関数の引数a
やb
は、「仮引数(parameter)」と呼ばれるものである。
一方で、このsample
関数を呼び出す以下の様な記述をした時、関数に渡した2
や3
は「実引数(argument)」と呼ばれる。
sample(2, 3);
この違いは、インタープリターに関数を導入する流れの中で意識する必要があるため、理解しておく必要がある。
無名関数
「無名関数」についても説明しておこう。
無名関数とは、その名の通り、名前のない関数である。
JavaScriptでは、以下の様に記述することで無名関数を利用できる。
(function(a, b) {return a + b;})(2, 3);
この例では、まずfunction(a, b) {return a + b;}
という記述によって無名関数を作成している。その後、この無名関数に2
と3
という実引数を渡し、関数を実行している。
なお、名前が付いた関数と、名前の付いていない無名関数の違いは、単純に変数にその関数を代入したか否かの違いであり、大きな違いは無い。
// 名前が付いた関数 var sample = function(a, b) {return a + b;}; sample(2, 3); // 名前の付いていない関数(無名関数) (function(a, b) {return a + b;})(2, 3);
無名関数の導入
無名関数を記述する機能を導入するには、インタープリターのevaluate
関数に以下の記述を追加する事で実現出来る。
以下では、3行目のif
文のブロックが今回追加したコードである。
var evaluate = function(x, env) { if (Array.isArray(x)) { if (x[0] === 'fn') { // 無名関数の生成 return function() { var params = x[1]; // 仮引数 var args = arguments; // 実引数 // 無名関数に渡された実引数を仮引数に代入 params.forEach(function(param, i) {env[param] = args[i];}); // 無名関数の本体を実行 return evaluate(['do'].concat(x.slice(2)), env); }; } else if (x[0] === 'while') { // whileループ
無名関数の生成を行うキーワードはfn
とした。これはClojureの文法に従ったものである。他の多くのLisp族言語では、lambda
というキーワードを用いている。
このfn
というキーワードの際にevaluate
関数が戻すのは、JavaScriptの無名関数である。このJavaScriptの無名関数が後で呼び出されるたびに、5行目から10行目までの処理が実行される。実行される内容は以下の通りである。
- 仮引数名を
params
変数で受け取る。params
には仮引数の変数名の配列が入る。 - 関数が呼び出された際に渡される実引数を
args
変数で受け取る。 - 後続の処理で適切な仮引数の変数名に適切な実引数がマッピングされるよう、
env
への代入操作を行う。 - 関数本体を実行する。関数本体の実行のために、
do
キーワードを先頭に付与し、evaluate
関数にかける。
このようにevaluate
関数を改良することで、このLispインタープリターで無名関数をサポートできる。
以下にソースコード全体を掲載する。
挙動を確認するために、以下のコードを実行してみる。
evaluate([['fn', ['a', 'b'], ['+', 'a', 'b']], 2, 3], globalEnv);
この処理は、以下のJavaScriptの処理と同一である。
(function(a, b) {return a + b;})(2, 3);
つまり、まず['fn', ['a', 'b'], ['+', 'a', 'b']]
の部分で無名関数を作成し、その後、2
と3
という引数を渡し、関数を実行している。
関数の導入
以下のように名前付きの関数を定義し、実行することも可能である。
evaluate(['do', ['def', 'sample', ['fn', ['a', 'b'], ['+', 'a', 'b']]], ['sample', 2, 3]], globalEnv);
上にも説明したとおり、名前が付いた関数と、名前の付いていない無名関数の違いは、単純に変数に無名関数を保持しているか否かの違いである。このLispインタープリターが無名関数を作成する能力を手に入れたいま、その無名関数にdef
を用いて名前を付けることにより、名前のついた関数も定義できることになる。つまり、このLispインタープリターには無名関数のみならず、関数も導入が完了したということになる。
現状の関数の問題点
以上でひとまずLispインタープリターに関数を導入することが出来た。しかしながら、現状の関数には大きな問題が残されている。この大きな問題とは、関数内に閉じたローカルスコープという概念が現状導入できていない事である。このため、仮引数に対して実引数を結びつける際に、グローバル変数領域であるglobalEnv
上にて引数の結びつけを行ってしまっており、この結びつけは、関数呼び出しが終わった以降も引き続き残ったままになるのである。
たとえば以下のような処理を動かしてみる。
evaluate(['do', ['def', 'sample', ['fn', ['a', 'b'], ['+', 'a', 'b']]], // (1)関数定義 ['sample', 2, 3], // (2)関数呼出 ['console.log', 'a', 'b']], // (3)関数呼出後 globalEnv);
実行結果は以下のようになる。
[2, 3]
上記のコードの(1)の部分ではsample
関数を定義し、(2)でsample
関数を呼び出している。プログラミング言語であれば、関数呼び出しが終わった(3)の時点では変数a
や変数b
には何も入っていない事を期待するはずだ。しかしながら、現状のこのインタープリターでは、実引数を仮引数に代入した操作が、関数呼び出し後も残ってしまう。
この問題を解決するためには、関数内に閉じたローカルスコープの概念を導入する必要がある。
次回はローカルスコープを導入する。
[近棟 稔]