作れる!数式評価プログラム

2002/06/25 さ〜(HTML 化 nukina)

1.はじめに

 こんにちは、佐波です。

 皆さんはプログラムを作るときに分からないことがあったらどうしますか?一番手っ取り早い解決法は近くの人に聞くことです。それでは聞いてみましょう。「○○く〜ん、今、暇?」「なんですか?」「ちょーっとプログラム手伝って欲しいんだけどー」・・・失敗です。

 それではプログラム関係の掲示板かメーリングリストに聞いてみましょう。メールの Subject は「はじめまして」か「わかりません」です(注1)。肝心の内容は・・・「数式を計算するプログラムの作り方がわかりません」程度にして返事を待ちましょう。おっと、返事が来ました。なになに?「それだけでは何を作りたいかがわかりません」・・・失敗です。

 これではプログラムが完成しません。そんなときには・・・おしえて、google さん!今回の場合なら「数式」「解析」「プログラム」あたりで検索をかけるとそれっぽいプログラムを見つけることができました。

 というわけでインターネット上の情報は有用なのですが、求めているプログラムがそのままの形で公開されていることは非常に非常に非常に稀です。これが意味するところは、ありあわせで我慢するか、自分で作るか、です。数式を評価する程度のプログラムは、基本的な知識とそれを応用する気構えさえあれば難しいことではないので、作ってしまいましょう。

 ここでは、数式を評価するプログラムの作成に際し、必要な基本的な知識と、それをどのように適用するかを見ていきます。

2.BNF

BNFは Backus Naur Form の略で、構文を定義する際に用いられる表記法です。BNF自体は単純で、定義とその内容を次のように記述していきます。

<定義>::=<内容>

 例えば、

<我輩>::=<猫>

という記述は、「我輩は猫である」を意味します。ここで、猫が箱の中に入っているとしましょう。そうです、アレです、シュレディンガーの猫です。この猫は箱を開けてみるまで生きている状態か、死んでいる状態かがわかりません。この場合、箱の中の猫は、二者のどちらかであることを示す「|」を使って

<猫>::=<生きている猫>|<死んでいる猫>

と定義することができます。

 さて、箱の中には猫が入っているわけですが、猫が何匹かわからないとしましょう。この場合、0回以上の繰り返しを表す「*」を使って次のように定義します。

<箱の中身>::=<猫>*

ただし、「*」は0個以上なので、このままでは箱の中に猫がいないことも有り得ます。少なくとも1匹は、という場合は次のようになります。

<箱の中身>::=<猫><猫>*

 これが1匹2匹ならたいしたことはありませんが、4匹、5匹と増えるほどに名前を付けるのが大変になってきます。んで、どうなるかと言えば・・・

<猫の兄弟>::=<太郎><次郎><三郎><四郎><五郎>

このとき注意しなければならないのは、最初が太郎で、二番目が次郎なことです。逆にはなりません。猫にもこんな名前をつけるかは問題にしないでください。

 名前を付けるのに困るほどなら、オスメスあわせてたくさん、です。オスたくさんか、メスたくさん、なら

<猫たくさん>::=<オス猫>*|<メス猫>*

ですが、オスとメスをいっしょくたに扱うために「{ }」を使います。

<猫たくさん>::={<オス猫>|<メス猫>}*

これでは、猫がいなくても猫たくさんになりますが、まぁいいです。

 ところで箱の中身ですが、箱を開けたらすぐに猫がいるとは限りません。もしかしたら、箱の中には箱があって、またその中に箱が・・・という入れ子になっていることも考えられます。

<箱の中身>::=<箱>|<猫>

箱の中身には箱か猫が入っているわけですが、箱の場合はもう一回箱を開けてみることになります。そのとき、また箱が入っていればまた箱を開けて・・・何回繰り返すか知りませんが、最終的には猫が入っているのです。

3.数式のBNF定義

箱の中の猫をBNFで表すのに比べれば、数式をBNFで表すことは簡単です。無理がないだけに話を理解するのも簡単です。

 数式には四則演算「+−×÷」と括弧「( )」を含めることができるとします。評価する順序が等しいものは、「+」と「−」、「×」と「÷」ですね。

 足し算「+」で結ばれた二つの数から成る数式は次のようになります。

<数式1>::=<数>+<数>

 では、演算子「+−」のどちらかで結ばれた二つの数から成る数式はどうでしょう。

<数式2>::=<数>{+|−}<数>

こうです。それでは、演算子「+−」だけを使える数式はと言うと・・・

<数式3>::=<数>{{+|−}<数>}*

 同様に、演算子「×÷」だけを使える数式について考えてみると・・・

<数式4>::=<数>{{×|÷}<数>}*

 最後に、括弧「()」はと言えば、これは入れ子の中の箱と一緒です。

<数式5>::=(<数式6>)|<数式7>

このとき、数式6は括弧で囲まれた式でも構いませんから、実は

<数式5>::=(<数式5>)|<数式7>

です。括弧が何重になっていても、最終的には括弧で囲まれない数式7があるのです。

 ここまでをまとめて、番号を振りなおします。

演算子「+−」 <数式1>::=<数>{{+|−}<数>}*

演算子「×÷」 <数式2>::=<数>{{×|÷}<数>}*

括弧「()」 <数式3>::=(<数式3>)|<数式4>

 数式を評価するため、評価順序を考慮してこれらのBNFを結合します。まず、数式1と数式2について、四則演算「+−×÷」を含む次の数式を考えます。

1×2+3÷4−5

この数式に対して、数式1と2のBNFを当てはめてみます(図1)。

図1.「+−×÷」を含む数式と対応するBNF

数式1の<数>にあたる項は、単項の「5」を含め、数式2で表せることに注目してください。このことから、数式1のBNFは次のように書き直されます。

<数式1>::=<数式2>{{+|−}<数式2>}*

 次に、括弧「( )」も含む数式

(1×2+3)×4

を考え、数式2、3のBNFを当てはめます(図2)。

図2.「+−×÷( )」を含む数式と対応するBNF

数式2の<数>に当たる項を数式3で表すために、

<数式2>::=<数式3>{{×|÷}<数式3>}*

<数式3>::=(<数式3>)|<数式4>|<数>

と書き改めます。さて、括弧内の数式

1×2+3

は、数式1の形です。これを考慮すれば、数式3の定義は次のようになります。

<数式3>::=(<数式3>)|<数式1>|<数>

ちょっと待ってください。数式1は数式2→数式3へと戻ってくることを考えると、

<数式3>::=(<数式1>)|<数>

これで十分です。

 これで、数式の構造に関するBNFでの定義は終わりました。最後に数の定義をしておきます。

<数>::={0|1|2|3|4|5|6|7|8|9}*

最低限、1文字は欲しいところですが、簡単のためにこれでいきます。

 四則演算「+−×÷」と括弧「( )」を含む数式を定義するBNFをまとめると、次のようになりました。

<数式1>::=<数式2>{{+|−}<数式2>}*

<数式2>::=<数式3>{{×|÷}<数式3>}*

<数式3>::=(<数式1>)|<数>

<数>::={0|1|2|3|4|5|6|7|8|9}*

4.BNFに基づいたプログラム化

 数式を定義するBNFが書けたので、これに基づいて評価するプログラムを作成します。呼び出す側は次にようなプログラムにしておきます(リスト1)。

 


#include <stdio.h>
#include <ctype.h>            ←isdigit(文字判定ルーチン)使用のため
char expr[] = "(1+2*3)*4/5";
void main(void) {
    printf("%s = %d\n", expr, eval( ));
 }

リスト1.数式を評価する関数を使うプログラム例

 評価する関数 eval は、評価プログラムを初期化するだけで、実際の評価は数式1の評価に丸投げします(リスト2)。初期化する変数 index は、現在見ている文字が何文字目かを表します。


int index;
int eval(void) {
    index = 0;
    return expr1( );
 }

リスト2.数式評価関数の導入部

 次に、数式1のBNFに従って数式を評価します(リスト3)。


int eval1(void) {
    int value = eval2( );           ←A
    while(1) {              ←B
        if(expr[index] == '+') {        ←C
            index ++;
            value += eval2( );  ←D
         }
        else if(expr[index] == '-') {   ←C
            index ++;
            value -= eval2( );  ←D
         }
        else    break;          ←C
     }                  ←B
    return value;
 }

リスト3.数式1の評価関数 eval1

BNFとの対応を見ていくことにします(図3)。

図3.数式1の評価プログラムとBNFの対応

 まず、先頭には数式2があるので、これを評価した値を取っておきます(A)。 残りの部分は何回繰り返されるか不明なので while ループで対応します(B)。 繰り返しの先頭は必ず「+」か「−」で(C)、そうでない場合は評価は終了です(Break 文)。 演算子「+−」があった場合は、その後に数式2が現れるので、これを評価(D)し、 演算子に応じて加算・減算します。  丸投げされた下請け(eval2 関数)では数式2のBNFに従って評価していきます(リスト4)。


int eval2(void) {
    int value = eval3( );           ←A
    while(1) {              ←B
        if(expr[index] == '*') {        ←C
            index ++;
            value *= eval3( );  ←D
         }
        else if(expr[index] == '/') {   ←C
            index ++;
            value /= eval3( );  ←D
         }
        else    break;          ←C
     }                  ←B
    return value;
 }

リスト4.数式2の評価関数 eval2

数式2のBNFは数式1と同じ形をしているのでプログラムも同じになります(注2)。 BNFとの対応も同様になる(図4)ので詳説は省略します。

図4.数式2の評価プログラムとBNFの対応

 次に数式3の評価関数 eval3 です(リスト5)。


if(expr[index] == '(') {            ←A
        index ++;
        int value = eval1( ); int eval3(void) {  ←B
        if(expr[index] != ')')      ←C
            puts("')'が閉じていない。");
        index ++;
        return value;
     }
    else    return number( );       ←D
 }

リスト5.数式3の評価関数 eval3

評価関数 eval3 とBNFの対応を見ていきます(図5)。

図5.数式3の評価プログラムとBNFの対応

まず最初の1文字が左括弧「(」であれば(A)、BNFに従って括弧内を数式1として評価します(B)。 右括弧「)」のチェック(C)が申し訳程度にできます。 最初の1文字が左括弧「(」でない場合は、数であるとみなして評価します(D)。

 最後に数を評価する関数 number です(リスト6)。


int number(void) {
    int value = 0;
    while(isdigit(expr[index])) {
        value = value*10 + (expr[index] - '0');
        index ++;
     }
    return value;
 }

リスト6.数の評価関数 number

順番に1文字ずつ見ていき、文字が数字だったら今までに読んだ数を 10 倍し、 新しい文字に対応する数字を加えるという、極めて単純な関数です。

 以上のリストを併せると数式評価のプログラムは完成です(expr.cpp)。

5.おわりに

今回は四則演算「+−×÷」と括弧「( )」だけの数式でしたが、 BNFを起こすことができるようになれば各種演算子を簡単に加えることができます。 整数で演算していることを利用してビット演算を加えたり、 浮動小数点表現を使って計算の精度をあげるということもできます。

 今回のプログラムでは説明の流れ上、エラー処理を大幅に省略しています。 使う場合には、エラーを見つけたときにどうするかを含め、悩みはつきません。

 yacc や lex を使っての構文解析が有名ですが、手作業で作成するのもプログラムの楽しみです。 簡単な構文程度ならそんなに時間がかかることもありませんしね。 あ、今回一番時間がかかっているのは当然ながらこの文章ということで。

参考

宮田高志さんのページ(http://cl.aist-nara.ac.jp/staff/takashi/home.html)内

  再帰下降構文解析:http://cl.aist-nara.ac.jp/staff/takashi/rec-desc/index.html

C MAGAZINE 2000年5月号:特集1 スクリプト言語を作ろう 水野順

  内容概要:http://www.cmagazine.jp/contents/200005.html


注1)メールの Subject は「はじめまして」か「わかりません」です

絶対に真似してはいけません。

注2)数式2のBNFは数式1と同じ形をしているのでプログラムも同じになります

除算の除数が0の場合の対応はすべきです。