JavaScriptでLispインタープリターを作ろう(4) ~順次処理「do」の導入~
この連載の第3回では、条件分岐if
を導入した。これは、プログラムにおける基本3要素「順次・反復・分岐」のうち、「分岐」をサポートしたことと言える。しかし、「順次」や「反復」に関してはまだサポートしていないため、今回は「順次」を四則演算インタープリターに導入する。
順次処理を導入する理由
「順次」とは、プログラムに記された順に、ソースコードの上から下へと順次、処理を行なっていくことである。このような処理はプログラミングにおいては当然過ぎる挙動であるため、意識することも少ないが、四則演算インタープリターではどのように考えればよいだろうか。
たとえば、JavaScriptのconsole.log
を用いて、文字'A'
をコンソールに出力した後、文字の'B'
をコンソールに出力する事を考えてみる。これは非常に単純な順次処理である。
// これを四則演算インタープリターでどう考えるか console.log('A'); console.log('B');
このような処理は、現状の四則演算インタープリターで以下のように実行することができる。
// 四則演算インタープリターのevaluateを複数呼び出す evaluate(['console.log', 'A'], globalEnv); evaluate(['console.log', 'B'], globalEnv);
なお、この処理を実行するための前提として、console.log
を四則演算インタープリターから呼出可能にするために、以下のようにconsole.log
関数をglobalEnv
に追加する必要がある。JavaScriptに不慣れな方へ説明を追加すると、コード上のarguments
はJavaScriptの予約語で、引数の内容がすべて格納されている変数である。
var globalEnv = { // ・・・中略・・・ // 以下の関数を追加 'console.log': function() {console.log(arguments);} };
さて、これで順次処理が出来ると言ってよいだろうか。
たとえばif
を用いて、条件がtrue
だった場合にconsole.log
を順番に実行したい場合はどうすればよいだろうか。その場合、実行したい複数の内容をif
の内側に記述する必要があるが、条件がtrue
だった場合に記述可能なことは1つに限られてしまう。
そのような処理を記述したい場合に用いるのが、ここで説明したいdo
である。do
を用いると、if
の条件がtrue
だった場合に、コンソールにA
とB
を出力し、そうでなかった場合に、コンソールにC
とD
を出力するような処理を以下のように記述可能になる。
evaluate(['if', true, ['do', ['console.log', 'A'], ['console.log', 'B']], ['do', ['console.log', 'C'], ['console.log', 'D']]], globalEnv);
このようにdo
を導入することで、プログラム上のどこでも順次実行を記述できるようになる。
do
関数の導入
do
を実装するために現状のevaluate
関数を変更する必要はない。globalEnv
へ以下のdo
関数を導入することで順次処理が可能になる。
var globalEnv = { // ・・・中略・・・ // 以下の関数を追加 'do': function() {} };
do
関数は不思議な関数である。引数の指定も無ければ、関数の本体も空っぽで、何の処理もない。
このdo
関数を用いて以下のような記述をすると、1 + 2
の計算結果である3
をコンソールに出力した後、文字の'A'
をコンソールに出力するといった、順次処理が実行できる。
evaluate(['do', ['console.log', ['+', 1, 2]], ['console.log', 'A']], globalEnv);
実行結果は次のようになる。
[3] ["A"]
どのように処理が行われたか、お分かりだろうか。
関数do
は、関数であるため、引数として渡された['console.log', ['+', 1, 2]]
や['console.log', 'A']
が順に評価された後に関数本体が実行される。この、「順に評価される」という性質により、順次処理が実現できているのである。
さて順次処理が導入できたので、この段階でソースコード全体を掲載しておこう。
順次処理の評価値(戻り値)
Lispでは通常、順次処理の最後の評価値が一連の順次処理の最終的な値(評価値もしくは戻り値)となる。たとえば、
Lisp族言語がサポートする順次処理の関数群、すなわちClojure言語のdo
、Common LispやEmacs Lispのprogn
、Schemeのbegin
はすべてこのような振る舞いをする。よって、この小さなインタープリターにおけるdo
もそのような挙動に変更してみよう。
変更前
'do': function() {}
変更後
'do': function() {return arguments[arguments.length - 1];}
挙動を確認してみるために、以下のコードを実行してみる。このコードは、文字'A'
をコンソールに出力した後、文字の'B'
をコンソールに出力し、その後、2 + 3
を計算して、その結果をdo
の結果とする。その後、do
の結果をコンソールに出力するというものである。
evaluate(['console.log', ['do', ['console.log', 'A'], ['console.log', 'B'], ['+', 2, 3]]], globalEnv);
実行結果は以下のようになる。
["A"] ["B"] [5]
do
関数の改善により、他のLisp族言語と同じく、2 + 3
の結果が正しくdo
の戻り値として戻ってきている様子がお分かり頂けると思う。変更後のソースコードは次のようになる。
do
をevaluate
関数に取り込む
ここまで説明してきたdo
の仕組みは、evaluate
関数の中で実装する事も可能だ。その場合のソースコードは次のようになる。
変更した部分としては、まず今まで使ってきた以下のdo
関数を廃止している。
'do': function() {return arguments[arguments.length - 1];}
代わりに、evaluate
関数の中に、以下の記述を追加している。
var evaluate = function(x, env) { if (Array.isArray(x)) { if (x[0] === 'do') { // do というキーワードだった場合 ← ここから追加 var result = undefined; for (var i = 1; i < x.length; i++) { result = evaluate(x[i], env); } return result; // ← ここまで追加 } else if (x[0] === 'if') { // if というキーワードだった場合 // ・・・以下略・・・
先ほどのdo
関数では、関数の引数が左から順に評価される性質を用いて実現してきた。この記述の方が、do
の処理を直接実装しているため、分かりやすいかもしれない。また、doを関数で実装したものよりも、メモリーを節約できているはずだ。
さて、以上で順次処理が記述可能になった。プログラムにおける基本3要素である「順次・反復・分岐」の2つまでサポート出来たことで、更に通常のプログラミング言語に近づいたと言えるだろう。
[近棟 稔]