JavaScript
Node.js

Node.jsでつくるNode.js - Step 8: ユーザー定義関数を使えるようにする

はじめに

「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();
  }

  // ... 省略 ...
}

ユーザ定義関数を使ってみる

フィボナッチ数列

関数を作って再帰呼び出しをやってみます。定番のフィボナッチ数列です。ソースコードはこうしました。

fib8.js
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をユーザ定義関数を使ってやってみます。ソースはこちら

fizzbuzz8.js
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にあげておきます。