ダメな感じの計算機

PPPUC++の6章本文メモが事故にてぶっ消えてしまった。かなしーです。
#地球のみんな、オラに元気をわけてくれ!

と、後ろばかり見てても仕方が無いので、6章ででてくる計算機を、読む前に自分で書いてみたのをのせるよー
https://github.com/tkuro11/calc--

方針

とりあえずversion から少しずつアップする.

最初バージョン

とりあえず、素直に愚直に作ると掛け算割り算の優先順位が守れない、ということですね。そこだけは初版でもなんとかしたい。かっこ悪いので。
普通にやるとスタック必要です。しかし、よーく考えてみると括弧さえ対応しなければ at most 1段でしかないことに気がつきます。
要するに、メモリつき電卓で、答メモリ(M)を用意して、

  1. 足し引き算のときはメモリ(M)に足しこむ
  2. 掛け割り算が来たら、メモリ(M)には入れずに表示部(D)で計算してしまう
    • で、また足し引き算が来たら、今の表示部の値を足しこんでから、次の数字を足しこむ

てな感じ。これはクラスで具象化ですね。下記、double acc を メモリ(M), double prevを表示部(D)、と思ってください。名前かえとけば良かった

class Calc {
    double acc;
    double prev;        // スタックの代わり
public:
    Calc() {Calc(0); }
    Calc (double init) : acc(0), prev(init) { }

    double result() { return acc+prev; }

    void clear() { acc = prev = 0; }          //ごわさんでー
    void set(double v) { acc = 0; prev = v; } //ねがいましてはー

    void add(double v) { acc += prev; prev = v; }
    void sub(double v) { acc += prev; prev = -v; }
    void mul(double v) { prev *= v; }
    void div(double v) { prev /= v; }
};

あとはこんな感じで入力にあわせてボタンを押すメソッドを呼び出すだけ

double calculate(stream& ss)
{
    double v = nextval(ss);

    Calc c(v);      // 計算機登場

    while (!ss.eof()) {
        char op;
        ss >> op;
        if (ss.eof()) break;
        v = nextval(ss);

        switch (op) {
            case '+': c.add(v); break;
            case '-': c.sub(v); break;
            case '*': c.mul(v); break;
            case '/': c.div(v); break;
            default:  throw runtime_error("no such operator");
        }
    }
    return c.result();
}

どや

Expression: 1+1*3
Result: 4
Expression: (1+1)*3
!!syntax error

いけてるぽいです。まだ括弧は実装してないので吹っ飛びました。

括弧

括弧ねえ。スタック無いと無理よね。って実は再帰呼び出しって要するにスタックよね、といういかにもわざとらしい演繹からそうやって実装してみます。

% git checkout recursive

イメージとしては '(' に出くわすたびに新しい Calc(電卓)を持ってきて ')'が終わるまで計算する。その結果をさっきまで使ってた電卓に戻す、という感じ。なんというブルジョア。お陰で変更は少ない。

diff --git a/calc.cpp b/calc.cpp
index 013496d..cabcaf4 100644
--- a/calc.cpp
+++ b/calc.cpp
@@ -28,7 +28,13 @@ double nextval(stream& ss)
 {
        double ret;
        ss >> ret;
-   if (!ss) throw runtime_error("syntax error");
+ if (!ss) {
+         ss.clear();
+         if (ss.get() == '(') {
+                 double calculate(stringstream& ss);
+                 ret = calculate(ss);
+         } else throw runtime_error("syntax error");
+ }
        return ret;
 }

@@ -42,6 +48,8 @@ double calculate(stream& ss)
                char op;
                ss >> op;
                if (ss.eof()) break;
+         if (op == ')') return c.result();
+
                v = nextval(ss);

                switch (op) {

あとはエラー処理

PPPUC++においての定義。当たり前といえば当たり前なんだけど、

  • ここでのエラーフリーという概念での仮定は以下の通り
    • 全ての正規の入力に期待される結果を返すべき
    • 全ての不正な入力にエラーを返すべき

とのこと。まだ括弧のエラーが抜けてやがった。というわけでエラールーチンを追加。結局、括弧が合わない、というのはさっきの電卓メタファでは

  1. 最初の電卓じゃないのに 式が終わった!
  2. 最初の電卓なのに ')'が来た!

のどちらか。したがってどうやっても「最初の計算機」であることを知らなければいけない。のでデフォルト引数で逃げた。
ちときたなくなった・・・

-double calculate(stream& ss)
+double calculate(stream& ss, bool nested= false)
 {
        double v = nextval(ss);

@@ -48,7 +47,8 @@ double calculate(stream& ss)
                char op;
                ss >> op;
                if (ss.eof()) break;
-           if (op == ')') return c.result();
+         if (op == ')') if (nested) return c.result();
+        else throw runtime_error("unbalanced parenthesis");

                v = nextval(ss);

@@ -60,6 +60,7 @@ double calculate(stream& ss)
                        default:  throw runtime_error("no such operator");
                }
        }
+    if (nested) throw runtime_error("unbalanced parenthesis");
        return c.result();
 }

まあいっか。

結論

本来ならちゃんとpush/downするかパースツリーるのが正解なんだろうけれども、ちょっとひねくれてみました。のココロ。
本書を見るとトークンクラスをこさえてるっぽい。すばらしい。一見回り道に見えても、より高度な抽象レベルでモノを見ると、変更がどんどん簡単になるのだよなーと、具体的な電卓モデルをこさえながら、こりゃだめだの今日の夕日。