はじめに
「RubyでつくるRuby ゼロから学びなおすプログラミング言語入門」(ラムダノート, Amazon) と PythonでつくるPythonに影響を受けて、Node.jsでミニNode.js作りにチャンレンジ中です。
前回は組み込み関数を呼び出す部分を作りました。今回はユーザ定義関数を呼び出せるようにしてみます。(今回も「RubyでつくるRuby」の第8章をベースにしています)
ユーザ定義関数の宣言
ユーザ定義関数の宣言のAST
関数を宣言している次のソースを用意します。
function add(x, y) {
return x + y;
}
esprimaにかけると、ASTはこうなりました。
Script {
type: 'Program',
body:
[ FunctionDeclaration {
type: 'FunctionDeclaration',
id: Identifier { type: 'Identifier', name: 'add' },
params:
[ Identifier { type: 'Identifier', name: 'x' },
Identifier { type: 'Identifier', name: 'y' } ],
body:
BlockStatement {
type: 'BlockStatement',
body:
[ ReturnStatement {
type: 'ReturnStatement',
argument:
BinaryExpression {
type: 'BinaryExpression',
operator: '+',
left: Identifier { type: 'Identifier', name: 'x' },
right: Identifier { type: 'Identifier', name: 'y' } } } ] },
generator: false,
expression: false,
async: false } ],
sourceType: 'script' }
関数宣言は FunctionDeclaration になり、その中身は BlockStatement に記述されるようです。ここで「RubyでつくるRuby」には登場しなかった ReturnStatement(return) が登場しています。Node.js (JavaScript)の関数で値を返すには、returnに対応しなければなりません。
この例では単純化後のtreeを次のようにすることを目指します。
[ 'func_def',
'add',
[ 'x', 'y' ],
[ 'ret', [ '+', [ 'var_ref', 'x' ], [ 'var_ref', 'y' ] ] ]
]
func_def, 関数名、仮引数の配列、処理内容のtree、の順になっています。
ユーザ定義関数の宣言のsimplify()拡張
関数の宣言(FunctionDeclaration)と、return文(ReturnStatement)に対応させます。
function simplify(exp) {
// ... 省略 ...
if (exp.type === 'FunctionDeclaration') {
const name = exp.id.name;
const astParams = exp.params;
// --- multi params ---
let i = 0;
let treeParams = [];
while (astParams[i]) {
treeParams[i] = astParams[i].name;
i = i + 1;
}
// --- body ---
const body = simplify(exp.body);
const tree = ['func_def', name, treeParams, body];
return tree;
}
if (exp.type === 'ReturnStatement') {
return ['ret', simplify(exp.argument)];
}
// ... 省略 ...
}
関数宣言の実行 evaluate()拡張
実際に関数宣言とreturn処理を行えるように、evaluate()も拡張します。今回はこうしてみました。
function evaluate(tree, genv, lenv) {
// ... 省略 ...
if (tree[0] === 'func_def') {
genv[tree[1]] = ['user_defined', tree[2], tree[3]];
return null;
}
if (tree[0] === 'ret') {
return evaluate(tree[1], genv, lenv);
}
// ... 省略 ...
}
「RubyでつくるRuby」を真似して、ユーザー定義関数の引数と処理内容をgenvのハッシュに格納しています。
ユーザ定義関数の呼び出し
ユーザ定義関数の呼び出しのASTは、前回の組み込み関数の呼び出しと同じです。なのでsimplify()の拡張はありません。実際に呼び出す処理を evaluate() に追加していきます。
evaluate() のユーザ定義関数の呼び出し対応
前回STEP 7で用意した fund_call の処理を、ユーザ定義関数にも対応させます。関数内で使う用の新しいローカル変数用のハッシュ newLenv を用意して引数の値を詰めて渡しています。
function evaluate(tree, genv, lenv) {
// ... 省略 ...
if (tree[0] === 'func_call') {
const mhd = genv[tree[1]];
let args = [];
let i = 0;
while (tree[2 + i]) {
args[i] = evaluate(tree[2 + i], genv, lenv);
i = i + 1;
}
if (mhd[0] === 'builtin') {
// -- call builtin function --
return callBuiltin(mhd[1], args);
}
// ---- STEP 8 ----
if (mhd[0] === 'user_defined') {
let newLenv = [];
let params = mhd[1];
let i = 0;
while (params[i]) {
newLenv[params[i]] = args[i];
i = i + 1;
}
return evaluate(mhd[2], genv, newLenv);
}
println('--- ERROR, unknown function type ---');
abort();
}
// ... 省略 ...
}
ユーザ定義関数を使ってみる
フィボナッチ数列
関数を作って再帰呼び出しをやってみます。定番のフィボナッチ数列です。ソースコードはこうしました。
function fib(x) {
if (x <= 1) {
return x
}
else {
fib(x - 1) + fib(x - 2);
}
}
let i = 0;
while (i < 10) {
println(fib(i));
i = i + 1;
}
実行してみると無事に動きました。再帰呼び出しも大丈夫なようです。やったね!
$ node mininode_step8.js sample/fib8.js
0
1
1
2
3
5
8
13
21
34
FizzBuzzも関数で
Step 6で動かした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;
}
}
let i = 1;
let ret;
while (i <= 20) {
ret = fizzbuzz(i)
println(ret);
i = i + 1;
}
実行結果はこちら
$ node mininode_step8.js sample/fizzbuzz8.js
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
ばっちり動いています! ... と思っていました。STEP 9でブートストラップにチャレンジするまでは。
予告
次回から数回にわたって、ブートストラップで自分自身を動かすことに取り組みます。すんなりとは行かないだろうと予想はしていたのですが、予想以上に苦労しました。
ここまでのソース
長くなったのでGitHubにあげておきます。