前回の記事で、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' ) {

無名関数の生成を行うキーワードは 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' ]]] , [ 'sample' , 2, 3 ] , [ 'console.log' , 'a' , 'b' ]] , globalEnv);

実行結果は以下のようになる。

[ 2, 3 ]

上記のコードの(1)の部分では sample 関数を定義し、(2)で sample 関数を呼び出している。プログラミング言語であれば、関数呼び出しが終わった(3)の時点では変数 a や変数 b には何も入っていない事を期待するはずだ。しかしながら、現状のこのインタープリターでは、実引数を仮引数に代入した操作が、関数呼び出し後も残ってしまう。

この問題を解決するためには、関数内に閉じたローカルスコープの概念を導入する必要がある。

次回はローカルスコープを導入する。

[近棟 稔]