LoginSignup
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-10

はじめに

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1