はじめに
「RubyでつくるRuby ゼロから学びなおすプログラミング言語入門」(ラムダノート, Amazon) と PythonでつくるPythonに影響を受けて、Node.jsでミニNode.js作りにチャンレンジ中です。
前回はモジュールに処理を追い出すことで、オブジェクトの利用を回避しました。いよいとブートストラップ成功かと思いきや、謎のエラーに会いました。
前回のエラーの追跡
mininodeで「mininodeでfizzbuzz」を実行しようとしたところ、evaluate()で処理できるはずの func_def が理解できていません。
$ node mininode_step10.js mininode_step10.js fizzbuzz8.js
-- ERROR: unknown node in evluate() ---
[ 'func_def',
'fizzbuzz',
...省略 ...
いったい、どういうことでしょうか?
訳も分からずデバッガーで調べ始めましたが、よくよく考えると問題は外側のmininode上で実行している内側のmininodeで起きています(fizzbuzz関数を扱うのは内側だから)。デバッガーで追えるのはNode.jsで直接実行している外側のmininodeの方だけです。なるほど、インタープリターをデバッグするのは厄介ですね。
しかたなくprintln()でデバッグメッセージを吐きならが実行するも、外側と内側からメッセージが出てしまい、いったいどちらから出力されているメッセージなのかわかりません。途方にくれてしまいます。
が、数日考えて「外側と内側で違うソース動かせば良い」と思いつきました。ほぼ同じでデバッグメッセージだけ異なる2つのソースをつくり、動かします。
$ node mininode_step10_outer.js mininode_step10_inner.js fizzbuzz8.js
そしてようやく原因にたどり着きました。
「これ、return文が動いてないじゃん...」
return文の動作
fizzbuzzを書き換えて確認
一見動いているように見えたreturn文の動きがおかしい疑いが強まったので、fizzbuzzを書き換えて確認してみます。
function fizzbuzz(n) {
if (n % (3*5) === 0) {
return 'FizzBuzz';
}
else if (n % 3 === 0) {
return 'Fizz';
}
else if (n % 5 === 0) {
return 'Buzz';
}
else {
return n;
}
// --- Should not come here ---
return 'zzz';
}
let i = 1;
let ret;
while (i <= 20) {
ret = fizzbuzz(i)
println(ret);
i = i + 1;
}
そして実行して見ると...
$ node mininode_step10.js fizzbuzz11.js
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
zzz
ショックです。returnで関数から抜けているつもりが、抜けずに関数の最後まで実行されてしまっていることが分かりました。
return 文の実装
冷静に自分のreturn文の実装を見て見ると、こうなっています。
function evaluate(tree, genv, lenv) {
// ... 省略 ...
if (tree[0] === 'ret') {
return evaluate(tree[1], genv, lenv);
}
// ... 省略 ...
}
実は今評価中の処理から戻るだけで、関数から抜ける処理になっていません。動くハズがないですね。
そういえば「RubyでつくるRuby」でも明示的なreturnは実装していませんし、使ってもいませんでした。おそらく意図的にそうしているのでしょう。
きちんとreturn文を実装するには、関数の処理結果を返すと同時に、evaluate()の呼び出し元(再帰的なのでそれもevaluateですが)に、関数からの脱出することを伝えなければなりません。多値の戻り値を返せる言語ならできそうですし、Node.jsでやるならオブジェクトを返せばできそうです。が、今回はオブジェクトは使えないので別の手段を考える必要があります。
しばし考えて、ここでも割り切ったズルイ手段を取ることにしました。「return文での関数脱出は諦めよう」と。
return文のよる関数脱出を排除
モジュールに追い出してあるsimplify()はそのままにし、ブートストラップ対象であるevaluate()の方を if 〜 else if 〜 else if 〜 else を使って書き換えます。そういえば「RubyでつくるRuby」ではcase-whenを使うことで、これがすっきり書けていました。再びなるほどです。
function evaluate(tree, genv, lenv) {
if (tree === null) {
return null;
}
else if (tree[0] === 'stmts') {
let i = 1;
let last;
while (tree[i]) {
last = evaluate(tree[i], genv, lenv);
//println(last);
i = i + 1;
}
return last;
}
// ... 省略 ...
else if (tree[0] === '>=') {
return evaluate(tree[1], genv, lenv) >= evaluate(tree[2], genv, lenv);
}
else if (tree[0] === 'in') {
// --- STEP 11 ---
return evaluate(tree[1], genv, lenv) in evaluate(tree[2], genv, lenv);
}
else {
println('-- ERROR: unknown node in evluate() ---');
printObj(tree);
abort();
}
}
そういえば evaluate()の中で 'in' を使っていたので、それもサポートするように追加しておきました。
関数スコープのローカル変数
else if を使って書き換えてたmininodeで、ブートストラップに再チャンレンジしてみると、次のエラーが発生。
$ node mininode_step11.js mininode_step11.js fizzbuzz8.js
---ERROR: varibable ALREADY exist --
変数の2重定義です。これもprint文による原始的なデバッグメッセージで追いかけると、例えば次のような箇所でひっかかっていることが分かりました。
else if (tree[0] === 'hash_new') {
let hsh = {};
let i = 1;
while (tree[i]) {
const key = evaluate(tree[i], genv, lenv); // <-- ここ
const val = evaluate(tree[i + 1], genv, lenv); // <-- ここ
hsh[key] = val;
i = i + 2;
}
return hsh;
}
whileブロック内で宣言している変数が、2重定義だと言われます。これもよくよく考えてみれば、今回の実装で変数用の新しいハッシュは関数呼び出しの時だけでした。つまりローカル変数は関数スコープしか存在しません。ifやwhileのブロックのスコープは無いため、確かに2重定義になっています。
そこで、上記の例は次のように直します。
else if (tree[0] === 'hash_new') {
let hsh = {};
let i = 1;
let key;
let val;
while (tree[i]) {
key = evaluate(tree[i], genv, lenv);
val = evaluate(tree[i + 1], genv, lenv);
hsh[key] = val;
i = i + 2;
}
return hsh;
}
他にも同様の箇所があり、関数スコープで衝突がないように修正しました。
3度目のブートストラップ
そして、、、
$ node mininode_step11.js mininode_step11.js fizzbuzz8.js
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
ついに動きました! 苦労した分、感激もひとしおです。さらに調子に乗って3重にしてみます。
$ node mininode_step11.js mininode_step11.js mininode_step11.js fizzbuzz8.js
... 省略 ...
目に見えて遅くなりますがこれも動きました。意味はないですがワクワクしてしまいます。
次回
ここまでのソース
- mininode_step11.js
- sample/fizzbuzz8.js
- sample/fizzbuzz11.js ... returnで関数脱出しているので、まだ実行できない