エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JavaScriptでLispインタープリターを作ろう(4) ~順次処理「do」の導入~

この連載の第3回では、条件分岐ifを導入した。これは、プログラムにおける基本3要素「順次・反復・分岐」のうち、「分岐」をサポートしたことと言える。しかし、「順次」や「反復」に関してはまだサポートしていないため、今回は「順次」を四則演算インタープリターに導入する。

順次処理を導入する理由

「順次」とは、プログラムに記された順に、ソースコードの上から下へと順次、処理を行なっていくことである。このような処理はプログラミングにおいては当然過ぎる挙動であるため、意識することも少ないが、四則演算インタープリターではどのように考えればよいだろうか。

たとえば、JavaScriptconsole.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に不慣れな方へ説明を追加すると、コード上のargumentsJavaScript予約語で、引数の内容がすべて格納されている変数である。

var globalEnv = {
  // ・・・中略・・・
  // 以下の関数を追加
  'console.log': function() {console.log(arguments);}
};

さて、これで順次処理が出来ると言ってよいだろうか。

たとえばifを用いて、条件がtrueだった場合にconsole.logを順番に実行したい場合はどうすればよいだろうか。その場合、実行したい複数の内容をifの内側に記述する必要があるが、条件がtrueだった場合に記述可能なことは1つに限られてしまう。

そのような処理を記述したい場合に用いるのが、ここで説明したいdoである。doを用いると、ifの条件がtrueだった場合に、コンソールにABを出力し、そうでなかった場合に、コンソールにCDを出力するような処理を以下のように記述可能になる。

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言語のdoCommon LispEmacs LispprognSchemebeginはすべてこのような振る舞いをする。よって、この小さなインタープリターにおける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の戻り値として戻ってきている様子がお分かり頂けると思う。変更後のソースコードは次のようになる。

doevaluate関数に取り込む

ここまで説明してきた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つまでサポート出来たことで、更に通常のプログラミング言語に近づいたと言えるだろう。

[近棟 稔]