2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Node.jsでつくるNode.js - Step 4: 四則演算で電卓を作る

Last updated at Posted at 2018-06-02

はじめに

「RubyでつくるRuby ゼロから学びなおすプログラミング言語入門」(ラムダノート, Amazon) と PythonでつくるPythonに影響を受けて、Node.jsでミニNode.js作りにチャンレンジ中です。

前回は「+ 演算子」を評価して足し算ができるところまで作りました。今回は「RubyでつくるRuby」の第4章と同じく、四則演算(+, -, *, /)で電卓を作ります。さらに余りや比較が行えるところまでを目指します。

ASTの単純化 simplify()の拡張

simplify()を拡張して、+以外の演算子に対応させます。

function simplify(exp) {
  if (exp.type === 'Literal') {
    return ['lit', exp.value];
  }
  if (exp.type === 'BinaryExpression') {
    if (exp.operator === '+') {
      return ['+', simplify(exp.left), simplify(exp.right)]
    }
    if (exp.operator === '-') {
      return ['-', simplify(exp.left), simplify(exp.right)]
    }
    if (exp.operator === '*') {
      return ['*', simplify(exp.left), simplify(exp.right)]
    }
    if (exp.operator === '/') {
      return ['/', simplify(exp.left), simplify(exp.right)]
    }
  }

  println('-- ERROR: unknown type in simplify) ---');
  printObj(exp);
  return null;
}

試しに '1+5 - 3*4/2' のASTを渡して見ると、次のツリーが得られます。

[ '-',
  [ '+', [ 'lit', 1 ], [ 'lit', 5 ] ],
  [ '/', [ '*', [ 'lit', 3 ], [ 'lit', 4 ] ], [ 'lit', 2 ] ] ]

どうやら大丈夫そうです。

四則演算の評価:evaluate()の拡張

同様に実行を担当する evaluate()も拡張して四則演算に対応します。

function evaluate(tree) {
  if (tree[0] === 'lit') {
    return tree[1];
  }
  if (tree[0] === '+') {
    return evaluate(tree[1]) + evaluate(tree[2]);
  }
  if (tree[0] === '-') {
    return evaluate(tree[1]) - evaluate(tree[2]);
  }
  if (tree[0] === '*') {
    return evaluate(tree[1]) * evaluate(tree[2]);
  }
  if (tree[0] === '/') {
    return evaluate(tree[1]) / evaluate(tree[2]);
  }

  println('-- ERROR: unknown node in evluate() ---');
  printObj(tree);
  return null;
}

先ほどの '1+5 - 3*4/2' を実行してみると、答えは 0 (ゼロ)になりました。正解です。

ここまでソース

一旦ここまでのソースはこちらです。この次は演算子を追加します。

// -------------------------
// mininode.js - Node.js by Node.js
// Step4:
// - Calculator +,-,*,/
// -------------------------

const esprima = require("esprima");

// --- parser ----
function parseSrc(src) {
  const ast = esprima.parseScript(src);
  return ast;
}

function makeTree(ast) {
  const exp = ast.body[0].expression;
  return simplify(exp);
}

function simplify(exp) {
  if (exp.type === 'Literal') {
    return ['lit', exp.value];
  }
  if (exp.type === 'BinaryExpression') {
    if (exp.operator === '+') {
      return ['+', simplify(exp.left), simplify(exp.right)]
    }
    if (exp.operator === '-') {
      return ['-', simplify(exp.left), simplify(exp.right)]
    }
    if (exp.operator === '*') {
      return ['*', simplify(exp.left), simplify(exp.right)]
    }
    if (exp.operator === '/') {
      return ['/', simplify(exp.left), simplify(exp.right)]
    }
  }

  println('-- ERROR: unknown type in simplify) ---');
  printObj(exp);
  return null;
}

// --- common ----
function printObj(obj) {
  console.dir(obj, {depth: 10});
}

function println(str) {
  console.log(str);
}

// --- evaluator ---
function evaluate(tree) {
  if (tree[0] === 'lit') {
    return tree[1];
  }
  if (tree[0] === '+') {
    return evaluate(tree[1]) + evaluate(tree[2]);
  }
  if (tree[0] === '-') {
    return evaluate(tree[1]) - evaluate(tree[2]);
  }
  if (tree[0] === '*') {
    return evaluate(tree[1]) * evaluate(tree[2]);
  }
  if (tree[0] === '/') {
    return evaluate(tree[1]) / evaluate(tree[2]);
  }

  println('-- ERROR: unknown node in evluate() ---');
  printObj(tree);
  return null;
}

// --------

const ast = parseSrc('1 + 5 - 3 * 4 / 2');
const tree = makeTree(ast);
println('--- tree ---');
printObj(tree);

println('--- answer ---');
const answer = evaluate(tree);
println(answer); // expect: (1+5) - (3*4/2) = 6 - 6 = 0

余り(%)と比較演算子を追加

FizzBuzzを作る予定なので、ちょっと先回りして余りを求める % 演算子と、比較のための演算子(===, !==, <, >, <=, >=)を追加しておきます。

まずは simplify() に追加します。

function simplify(exp) {
  if (exp.type === 'Literal') {
    return ['lit', exp.value];
  }
  if (exp.type === 'BinaryExpression') {
    // ... 省略 ...

    if (exp.operator === '%') {
      return ['%', simplify(exp.left), simplify(exp.right)]
    }


    if (exp.operator === '===') {
      return ['===', simplify(exp.left), simplify(exp.right)]
    }
    if (exp.operator === '!==') {
      return ['!==', simplify(exp.left), simplify(exp.right)]
    }

    // ... まだまだ追加 ...
  }
}

とここまで書いていて、すべての演算子で同じパターンなことに気がつきました(遅い)。simplify()はこうしました。

function simplify(exp) {
  if (exp.type === 'Literal') {
    return ['lit', exp.value];
  }
  if (exp.type === 'BinaryExpression') {
    return [exp.operator, simplify(exp.left), simplify(exp.right)];
  }

  println('-- ERROR: unknown type in simplify) ---');
  printObj(exp);
  return null;
}

一方 evaluate() は、一つ一つ処理して行きます。

function evaluate(tree) {
  // ... 省略 ...

  if (tree[0] === '%') {
    return evaluate(tree[1]) % evaluate(tree[2]);
  }

  if (tree[0] === '===') {
    return evaluate(tree[1]) === evaluate(tree[2]);
  }
  if (tree[0] === '!==') {
    return evaluate(tree[1]) !== evaluate(tree[2]);
  }
  if (tree[0] === '<') {
    return evaluate(tree[1]) < evaluate(tree[2]);
  }
  if (tree[0] === '>') {
    return evaluate(tree[1]) > evaluate(tree[2]);
  }
  if (tree[0] === '<=') {
    return evaluate(tree[1]) <= evaluate(tree[2]);
  }
  if (tree[0] === '>=') {
    return evaluate(tree[1]) >= evaluate(tree[2]);
  }

  println('-- ERROR: unknown node in evluate() ---');
  printObj(tree);
  return null;
}

これで今後のFizzBuzzも実現できそうです。

次回

ここまでのソースコード

Step4までの全体のコードはこちらです。

// -------------------------
// mininode.js - Node.js by Node.js
// Step4:
// - Calculator +,-,*,/, %, ===,!==,<,>,<=,>=
// -------------------------

const esprima = require("esprima");

// --- parser ----
function parseSrc(src) {
  const ast = esprima.parseScript(src);
  return ast;
}

function makeTree(ast) {
  const exp = ast.body[0].expression;
  return simplify(exp);
}

function simplify(exp) {
  if (exp.type === 'Literal') {
    return ['lit', exp.value];
  }
  if (exp.type === 'BinaryExpression') {
    return [exp.operator, simplify(exp.left), simplify(exp.right)];
  }

  println('-- ERROR: unknown type in simplify) ---');
  printObj(exp);
  return null;
}

// --- common ----
function printObj(obj) {
  console.dir(obj, {depth: 10});
}

function println(str) {
  console.log(str);
}

// --- evaluator ---
function evaluate(tree) {
  if (tree[0] === 'lit') {
    return tree[1];
  }
  if (tree[0] === '+') {
    return evaluate(tree[1]) + evaluate(tree[2]);
  }
  if (tree[0] === '-') {
    return evaluate(tree[1]) - evaluate(tree[2]);
  }
  if (tree[0] === '*') {
    return evaluate(tree[1]) * evaluate(tree[2]);
  }
  if (tree[0] === '/') {
    return evaluate(tree[1]) / evaluate(tree[2]);
  }
  if (tree[0] === '%') {
    return evaluate(tree[1]) % evaluate(tree[2]);
  }
  /*
  if (tree[0] === '**') {
    return evaluate(tree[1]) ** evaluate(tree[2]);
  }
  */

  if (tree[0] === '===') {
    return evaluate(tree[1]) === evaluate(tree[2]);
  }
  if (tree[0] === '!==') {
    return evaluate(tree[1]) !== evaluate(tree[2]);
  }
  if (tree[0] === '<') {
    return evaluate(tree[1]) < evaluate(tree[2]);
  }
  if (tree[0] === '>') {
    return evaluate(tree[1]) > evaluate(tree[2]);
  }
  if (tree[0] === '<=') {
    return evaluate(tree[1]) <= evaluate(tree[2]);
  }
  if (tree[0] === '>=') {
    return evaluate(tree[1]) >= evaluate(tree[2]);
  }

  println('-- ERROR: unknown node in evluate() ---');
  printObj(tree);
  return null;
}

// --------

const ast = parseSrc('1 % 2');
const tree = makeTree(ast);
printObj(tree);
const answer = evaluate(tree);
println(answer); // expect: 1

printObj(evaluate(makeTree(parseSrc('1 < 2')))); // expect: true
printObj(evaluate(makeTree(parseSrc('1 > 2')))); // expect: false
printObj(evaluate(makeTree(parseSrc('1 === 2')))); // expect: false
printObj(evaluate(makeTree(parseSrc('1 !== 2')))); // expect: true
printObj(evaluate(makeTree(parseSrc('1 >= 2')))); // expect: false
printObj(evaluate(makeTree(parseSrc('1 <= 2')))); // expect: true

2
0
0

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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?