Racc は Ruby で使えるパーサジェネレータです。これを使うと自分用のプログラム言語を Ruby で記述するのも夢ではありません。しかし、使い方がもう1つ分かりません。ところが、配布パッケージに添附してある calc.y を利用すると、なんとなく使い方のコツが分かるのに気づきました。Racc のダウンロードはここからできます。calc.y は配布パッケージの racc-1.4.5-all/packages/racc/sample/ の中にあります。
最初に calc.y から Racc が何を作ることができるかを見るために、calc.y を Racc でコンパイルしてみましょう。この場合ターゲットファイルの名前を calc.rb にしたいので -o オプションをつけてコンパイルします。
$ racc -o calc.rb calc.y
作成された calc.rb をさっそく実行してみましょう。
$ ruby calc.rb type "Q" to quit. ? 1 + 2 = 3 ? (1 + 2) * 3 = 9 ? q
整数の電卓ですがちゃんと括弧も使えるし積と和の優先順位も判別しています。こんな優れもののプログラムを生成する calc.y の中味がちょっと気になります。そこで、calc.y の中味を見てみることにします。
# $Id: calc.y,v 1.4 2005/11/20 13:29:32 aamine Exp $ # # Very simple calculater. class Calcp prechigh nonassoc UMINUS left '*' '/' left '+' '-' preclow rule target: exp | /* none */ { result = 0 } exp: exp '+' exp { result += val[2] } | exp '-' exp { result -= val[2] } | exp '*' exp { result *= val[2] } | exp '/' exp { result /= val[2] } | '(' exp ')' { result = val[1] } | '-' NUMBER =UMINUS { result = -val[1] } | NUMBER end ---- header # $Id: calc.y,v 1.4 2005/11/20 13:29:32 aamine Exp $ ---- inner def parse(str) @q = [] until str.empty? case str when /\A\s+/ when /\A\d+/ @q.push [:NUMBER, $&.to_i] when /\A.|\n/o s = $& @q.push [s, s] end str = $' end @q.push [false, '$end'] do_parse end def next_token @q.shift end ---- footer parser = Calcp.new puts puts 'type "Q" to quit.' puts while true puts print '? ' str = gets.chop! break if /q/i =~ str begin puts "= #{parser.parse(str)}" rescue ParseError puts $! end end
意外に短いのに驚かされます。また、良く見るといくつかの部分に分かれているのが分かります。最初は class Calcp とパーサの属するクラスの名前を定義する部分。二番目に、prechigh と preclow で囲まれた演算子の優先順位を示す部分。三番目は rule と end の間に記述される文法規則の部分。四番目は、---- header 以下に記述される部分、五番目は、---- inner 以下に記述される部分。最後に、---- footer 以下に記述される部分です。
これは、calc.y を基にコンパイルされた calc.rb とも関係がありそうです。今度は、この calc.rb をのぞいて見ましょう。
# # DO NOT MODIFY!!!! # This file is automatically generated by racc 1.4.4 # from racc grammer file "calc.y". # require 'racc/parser' # $Id: calc.y,v 1.4 2005/11/20 13:29:32 aamine Exp $ class Calcp < Racc::Parser module_eval <<'..end calc.y modeval..id9c22c73d7d', 'calc.y', 27 def parse(str) @q = [] until str.empty? case str when /\A\s+/ when /\A\d+/ @q.push [:NUMBER, $&.to_i] when /\A.|\n/o s = $& @q.push [s, s] end str = $' end @q.push [false, '$end'] do_parse end def next_token @q.shift end ..end calc.y modeval..id9c22c73d7d ##### racc 1.4.4 generates ### racc_reduce_table = [ 0, 0, :racc_error, 1, 11, :_reduce_none, 0, 11, :_reduce_2, 3, 12, :_reduce_3, 3, 12, :_reduce_4, 3, 12, :_reduce_5, 3, 12, :_reduce_6, 3, 12, :_reduce_7, 2, 12, :_reduce_8, 1, 12, :_reduce_none ] racc_reduce_n = 10 racc_shift_n = 19 racc_action_table = [ 7, 8, 9, 10, 6, 18, 3, 4, 11, 5, 7, 8, 9, 10, 3, 4, 13, 5, 3, 4, nil, 5, 3, 4, nil, 5, 3, 4, nil, 5, 3, 4, nil, 5, 7, 8, 7, 8 ] racc_action_check = [ 12, 12, 12, 12, 1, 12, 10, 10, 3, 10, 2, 2, 2, 2, 0, 0, 6, 0, 4, 4, nil, 4, 9, 9, nil, 9, 8, 8, nil, 8, 7, 7, nil, 7, 17, 17, 16, 16 ] racc_action_pointer = [ 8, 4, 7, -1, 12, nil, 16, 24, 20, 16, 0, nil, -3, nil, nil, nil, 33, 31, nil ] racc_action_default = [ -2, -10, -1, -10, -10, -9, -10, -10, -10, -10, -10, -8, -10, 19, -5, -6, -3, -4, -7 ] racc_goto_table = [ 2, 1, nil, nil, 12, nil, nil, 14, 15, 16, 17 ] racc_goto_check = [ 2, 1, nil, nil, 2, nil, nil, 2, 2, 2, 2 ] racc_goto_pointer = [ nil, 1, 0 ] racc_goto_default = [ nil, nil, nil ] racc_token_table = { false => 0, Object.new => 1, :UMINUS => 2, "*" => 3, "/" => 4, "+" => 5, "-" => 6, "(" => 7, ")" => 8, :NUMBER => 9 } racc_use_result_var = true racc_nt_base = 10 Racc_arg = [ racc_action_table, racc_action_check, racc_action_default, racc_action_pointer, racc_goto_table, racc_goto_check, racc_goto_default, racc_goto_pointer, racc_nt_base, racc_reduce_table, racc_token_table, racc_shift_n, racc_reduce_n, racc_use_result_var ] Racc_token_to_s_table = [ '$end', 'error', 'UMINUS', '"*"', '"/"', '"+"', '"-"', '"("', '")"', 'NUMBER', '$start', 'target', 'exp'] Racc_debug_parser = false ##### racc system variables end ##### # reduce 0 omitted # reduce 1 omitted module_eval <<'.,.,', 'calc.y', 12 def _reduce_2( val, _values, result ) result = 0 result end .,., module_eval <<'.,.,', 'calc.y', 14 def _reduce_3( val, _values, result ) result += val[2] result end .,., module_eval <<'.,.,', 'calc.y', 15 def _reduce_4( val, _values, result ) result -= val[2] result end .,., module_eval <<'.,.,', 'calc.y', 16 def _reduce_5( val, _values, result ) result *= val[2] result end .,., module_eval <<'.,.,', 'calc.y', 17 def _reduce_6( val, _values, result ) result /= val[2] result end .,., module_eval <<'.,.,', 'calc.y', 18 def _reduce_7( val, _values, result ) result = val[1] result end .,., module_eval <<'.,.,', 'calc.y', 19 def _reduce_8( val, _values, result ) result = -val[1] result end .,., # reduce 9 omitted def _reduce_none( val, _values, result ) result end end # class Calcp parser = Calcp.new puts puts 'type "Q" to quit.' puts while true puts print '? ' str = gets.chop! break if /q/i =~ str begin puts "= #{parser.parse(str)}" rescue ParseError puts $! end end
冒頭の部分から見て行くと、最初に require 'racc/parser' とあるので、racc/parser.rb を利用しているのが分かります。その下の # $Id: ... の部分は、calc.y で ---- header に記述されていた部分です。その下は class Calcp < Racc::Parser は、calc.rb のパーサークラスの名前が Calcp で、これは Racc::Parser クラスの子クラスであることが分かります。その下の def parse(str) は calc.y の ---- inner で記述されていた部分で、後で述べるように、スキャナーのプログラムです。これを見ると ---- inner で書かれていた Ruby のプログラムは、Calcp クラスの中で定義されることが分かります。その後には謎のテーブルがずらずらと続き、テーブルの後には、_reduce_2 などの名前がついた関数の列が続きます。そうして Calcp クラスの終を表す end # class Calcp が出現します。最後の短いプログラムが calc.y では、---- footer に記述されていたもので、これが calc.rb のメインルーチンになります。
したがって、calc.rb の大半を占める class Calcp から end # class Calcp の部分はパーサクラスを定義している部分だったのです。そこで、これをパーサを生成するための Calcp クラスであると割り切って中味をすっかり忘れることにすると、calc.rb のプログラムの動作は parser = Calcp.new 以下のメインルーチンだけを読めばいいと言う事になります。
そうすると、calc.rb のメインルーチンでは、Calcp クラスから parser というパーサーインスタンスを作り、プログラムから抜けるには 'q' を入力すれば良いことを示し、入力を待つ。コンソールから入力があったら、それを str に格納し、str が q であればプログラムを終了させ、そうでなければメソッド parser.parse(str) を呼び出して構文解析し、その結果を = の後に出力するという単純な操作を行っていることになります。Calcp クラスの中味は幾ど Racc まかせなのです。
つまり構文解析クラス Calcp の作成を、Racc 大明神に丸投げして、でき上がった Calcp をありがたく使わせてもらえるのだと考えると、雲をつかむような Racc に何となく親しみを感じるのではないでしょうか。
calc.y と calc.rb との比較で分かるように、calc.y の記述のうち、構文解析クラスを作成するのに使われているのは、class Calcp と prechigh から preclow までの演算子の優先度を指示する部分、rule から end の間で記述される文法規則、及び、inner に記述される部分のようです。
calc.y の演算子の優先順位の部分を再掲すると次のようになります
prechigh nonassoc UMINUS left '*' '/' left '+' '-' preclow
数値の正負を反転させる UMINUS という演算がもっとも優先度が高く、* と / が続き、+ と - が最も低くなっていることが分かります。UMINUS がキーボードから入力できる演算子の記号でないのは、数値の符合の反転の指示は固有の演算記号がなく -3 のように数値の直前に - 記号を書くようになるからです。1 - 2 と -3 との違いは文法規則のところで定義しますが、- NUMBER という構文の優先順位が UMINUS と同じであるという定義の仕方をします。
また、演算子の左の nonassoc、left などは、1 + 2 + 3 のように演算子による結合が三項以上になるときに、(1 + 2) + 3 のように左から計算するのか、1 + (2 + 3) のように右から計算するのかを指定します。nonassoc は -1 のような単項演算を指示しているので計算の順序は関係ないことを示しています。left は、(1 + 2) + 3 のように左から計算します。right は、累乗のときのように 1 ^ (2 ^ 3) と演算が右から行われることを示します。
calc.y の rule から end 間での間に記述されているのが文法規則です。この文法規則は文脈自由型文法に属するものだそうですが、コツをつかめば、文脈自由型文法がどのようなものかをハッキリと知らなくても文法規則を記述することができます。calc.y の文法規則を再掲すると次のようになります。
rule target: exp | /* none */ { result = 0 } exp: exp '+' exp { result += val[2] } | exp '-' exp { result -= val[2] } | exp '*' exp { result *= val[2] } | exp '/' exp { result /= val[2] } | '(' exp ')' { result = val[1] } | '-' NUMBER =UMINUS { result = -val[1] } | NUMBER end
コロンの左側の target、exp は非終端記号でコロンの右側の記号の並びで定義されます。|は「または」という意味を表します。たとえば、target という非終端記号は、exp という非終端記号か、何も記号がない状態のどちらかで定義されます。また、定義の最初の target という名前に特別な意味はなく、target を program に変えても正常に calc.y をコンパイルすることができます。
非終端記号 exp の定義では、コロンの右側にも exp が現れています。こういう定義のしかたを再帰的定義といいます。再帰的定義の利点は、(1 + 2) * (3 - 4) のような入れ子の構造を持った式を機械的に処理できることです。この例では 1 と言う数は exp の定義から、exp です。また 2 も exp です。従って 1 + 2 は、exp + exp なので exp です。また、( exp ) も exp なので、(1 + 2) も exp です。最終的にはもとの式が exp * exp なのでこれも exp と定義できることが分かります。再帰的定義を使えば、どのような複雑な式も上に定義した簡潔な定義で全て解読できます。複雑な構文を分析するのに、込み入った条件分岐もなく機械的に解読できるのです。
しかし、文脈自由文法についての詳しい知識がなくても、exp: exp '+' exp という定義が、「式と式を+記号でつないだものも式である」という意味だくらいを理解していれば、プログラム言語の設計だってできます。
exp: exp '+' exp は「式と式を+記号でつないだものも式である」という文法上の定義をしているにすぎませんが、exp: exp + exp が表す意味にはもう1つあります。つまり、コロンの右項の第1項の価と第2項の価を加えたものがコロンの左項の価になると言う事です。exp: exp '+' exp という文法の定義に式の価の加算という意味をつけくわえるのが、{ と } で囲まれた部分のプログラムで、アクションといいます。アクションには幾ど全ての Ruby の文を使うことができます。exp: exp '+' exp の場合についていえば、{ result += val[2] } です。
result、val は Racc であらかじめ定義されている Calcp のローカル変数で、式の価を取り扱います。exp: exp '+' exp についていえば、result はコロンの左側の exp の価。val[0] は exp '+' exp の最初の exp の価、val[1]は '+'、val[2]は 二番目の exp の価になります。ここでは { result = val[0] + val[2] } と書くこともできますが、アクションが実行される前は、result == val[0] なので { result += val[2] }とも書けます。
exp の定義を見ると最後の行に NUMBER という1語だけが記述してあります。これは、整数一般を表す記号です。exp (式) は整数1つだけからなるということもあるということです。式を表す記号が exp となっているのに、数値を表す記号が NUMBER と大文字になっているのは、NUMBER はそれ以上分割できないからです。それにたいして、exp は数値と演算子の結合したものや括弧を含んだものなど複数の記号からできている可能性があります。したがって、NUMBER は記号の元素のようなものなので終端記号と呼ばれています。exp はいわば記号の分子で非終端記号と呼ばれます。
exp の定義の下から2番目の行は数値の符合の反転を定義しています。= UMINUS という表現が気になりますが、- NUMBER というのは演算子の優先順位の定義にはでてこないのでこのパターンが UMINUS とおなじ優先順位を持つことを指示しているのです。
演算子の優先順位の定義と文法規則の定義の方法が分かったので、それを応用して整数の累乗の演算を '^' という記号で導入してみましょう。先ず累乗の演算の優先順位は、符合の反転より低く、乗除よりは高いので、UNINUS の行と '*' '/' の行の間に挿入します。また、2^3^4 のように累乗が連続する場合右から計算をしていきますから、計算の順序は right を指定します。
prechigh nonassoc UMINUS right '^' left '*' '/' left '+' '-' preclow
これで累乗の優先順位の指定はできましたが、文法の定義も必要です。rule の部分に次のように追加します。
rule target: exp | /* none */ { result = 0 } exp: exp '+' exp { result += val[2] } | exp '-' exp { result -= val[2] } | exp '*' exp { result *= val[2] } | exp '/' exp { result /= val[2] } | exp '^' exp { result **= val[2] } | '(' exp ')' { result = val[1] } | '-' NUMBER =UMINUS { result = -val[1] } | NUMBER end
改造した calc.y をコンパイルして次のように累乗の計算ができたら成功です。
? (1 + 2) ^ 3 = 27
演算の追加があまりに簡単にできてしまったので拍子抜けしてしまいそうです。
英語の文が色々な品詞の単語から構成されており、その順序に関する規則が文法であるように、calc.y の場合も 1 * (2 - 3) のような式も、1、2、3のような数値と、*、(、-、) のような記号から構成され、その順序は文法規則で決められています。このような文法規則で扱われる数値や記号をまとめてトークンと呼びます。Racc で作成される構文解析メソッド do_parse は、'1 * (2 - 3)' のような生の文字列を解析することはできません。生の文字列は先ず、スキャナによって処理されてトークンの配列として取りだされる必要があるのです。do_parse メソッドはキュー(待ち行列)に積まれたトークンを next_token メソッドによって取りだして構文の解析を行います。このスキャナーや next_token はユーザがプログラムしなくてはならないので、Racc の使い方が難しく感じられるのです。しかし、幸いなことに calc.y にはきちんと動作するスキャナがついています。それが、calc.y の inner に記述されている parse(str) メソッドと、next_token メソッドです。inner の記述を再掲します。
def parse(str) @q = [] until str.empty? case str when /\A\s+/ when /\A\d+/ @q.push [:NUMBER, $&.to_i] when /\A.|\n/o s = $& @q.push [s, s] end str = $' end @q.push [false, '$end'] do_parse end def next_token @q.shift end
ここで parse(str) メソッドがスキャナです。引数として文字列 str をとり、トークンに切り分けてパーサに渡します。next_token は 取りだされたトークンをパーサ do_parse メソッドが取りだすときに呼び出すメソッドです。
まず、parse(str) メソッドの中をのぞいて見ましょう。最初に @q という配列が初期化されています。これは、文字列から取りだされたトークンを貯めておくキューです。キューは FIFO (first in first out) なのでキューに格納されたトークンは、next_token メソッドによって、格納された順番で do_parse メソッドから呼び出されます。
until str.empty? から end までのループがスキャナーの本体です。until str.empty? なので、文字列 str が空になるまでループがくり返されます。case str で文字列を検査します。when /\A\s+/ は str の先頭から空白文字が並んでいた場合で、この場合には何もしません。ループの最後で str = $' で正規表現にマッチした部分が切捨てられますから、結局空白文字をスキップすることになります。when /\A.\d+/ は文字列の並びが続いている場合です。これは、数値データを示していると考えられますから、トークンのキューに、トークンの種別 :NUMBER, とその整数値 $&.to_i を 要素とする配列を格納します。つまり do_parse が next_token でトークンを取りだすとき、do_parse はトークンの種別と値という2種類の情報を得ることになります。/\A.|\n/o は一文字の記号の場合です。この場合のトークンは種別も価もその記号となります。+ や * などの演算子がこの処理を受けます。str = $' は str のうち正規表現にマッチした先頭部分の切捨てを行います。スキャナーのループを抜けた後 @q.push [false, '$end'] というトークン列の終了を意味するトークンをキューに追加しています。do_parse の仕様でこのトークンがないと発見しにくいバグが発生します。最後に do_parse メソッドが呼び出されるので、calc.y では構文解析もやってしまうことが分かります。
next_token の動作は簡単で、キュー @q からトークンを取りだして渡すだけです。
スキャナの構造が大体分かったので、ここで、文字列をトークンとして取りだすことができるように改造してみましょう。文字列のトークン種別は IDENT (identifier; 識別子)とし、価は文字列そのものを使います。文字列を取りだすための正規表現は /\A[a-zA-Z]\w*/ で先頭がアルファベットでその後が数値とアルファベットになるパターンにします。改造したスキャナ parse(str) は、次のようになります。
def parse(str) @q = [] until str.empty? case str when /\A\s+/ when /\A\d+/ @q.push [:NUMBER, $&.to_i] when /\A[a-zA-Z]\w*/ @q.push [:IDENT, $&] when /\A.|\n/o s = $& @q.push [s, s] end str = $' end @q.push [false, '$end'] do_parse end
ただ、文法規則に IDENT トークンが登録されていないのでコンパイルしても文字列が認識されているかどうか分かりません。そこで、calc.y の rule に次のように IDENT を登録します。この位置に登録したのは、ターミナルから文字列を入力してリターンキーを押すとその文字をエコーバックしてくれるようにしたかったからです。rule の変更部分は次のようになります。
rule target: exp | IDENT | /* none */ { result = 0 }
変更した calc.y をコンパイルして次のように動作したら成功です。
? ident = ident
だいぶ Racc の動作にもなれてきたので、calc.rb にも、少しプログラム言語らしいことをやらせたくなってきました。そこで、変数を使った計算をやらせてみましょう。変数のトークンとしては IDENT を利用することにして、変数をどう扱ったら良いでしょうか。1つは、変数への数値の代入です。これは assign という非終端記号で表し、assign: IDENT '=' NUMBER で定義することにします。assign に割り当てられる代入の動作は do_assign メソッドを calc.y の inner に定義します。また、IDENT 単独で代入された価を表すことができなければ、変数を使った計算はできないので exp: IDENT という定義を加えることにします。この際の変数の値を取りだすメソッドを do_refvar としてやはり inner に記述します。実際に行った文法規則の変更を次に示します。
rule target: exp | assign | /* none */ { result = 0 } assign: IDENT '=' NUMBER { result = do_assign(val[0], val[2]) } exp: exp '+' exp { result += val[2] } | exp '-' exp { result -= val[2] } | exp '*' exp { result *= val[2] } | exp '/' exp { result /= val[2] } | exp '^' exp { result **= val[2] } | '(' exp ')' { result = val[1] } | '-' NUMBER =UMINUS { result = -val[1] } | IDENT { result = do_refvar( result ) } | NUMBER end
inner に記述した do_assign、do_refvar のメソッドは次のようになります。インスタンス変数 @vartab の初期化は initialize メソッドの中で行います。
---- inner def initialize @vartab = {} end def do_assign(varname, val) @vartab[varname] = val end def do_refvar(varname) @vartab[varname] end
変更後に calc.y をコンパイルすると、次のように、変数を使った計算ができるようになります。
? a = 1 = 1 ? b = 2 = 2 ? a + b = 3
パーサとスキャナは別々のモジュールとして分離可能です。プログラム言語の文法を色々と変えて実験したいときは、パーサとスキャナを分離してパーサだけをコンパイルするようにできれば便利です。Racc ではそのような操作も可能です。実験のために calc.y のコピーを二つ、calc_rule.y、calc_scan.rb という二つの名前で作ります。それから calc_rule.y からは、header と inner と footer の記述を削除してしまいます。逆に、calc_scan.rb からは、文法規則の部分を消去します。こうしてできた calc_rule.y は次のようになります。
class Calcp prechigh nonassoc UMINUS right '^' left '*' '/' left '+' '-' preclow rule program: target { puts "syntax OK" } target: exp | assign | /* none */ assign: IDENT '=' NUMBER exp: exp '+' exp | exp '-' exp | exp '*' exp | exp '/' exp | exp '^' exp | '(' exp ')' | '-' NUMBER =UMINUS | IDENT | NUMBER end
これは見ると分かるように、文法だけしか書いてありません。ところが、驚いたことに、これを racc calc_rule.y でコンパイルすると、コンパイルが成功して calc_rule.tab.rb というファイルが作成されます。また、スキャナだけを残した calc_scan.rb は次のようになります。
require 'calc_rule.tab.rb' class Calcp def initialize @vartab = {} end def do_assign(varname, val) @vartab[varname] = val end def do_refvar(varname) @vartab[varname] end def parse(str) @q = [] until str.empty? case str when /\A\s+/ when /\A\d+/ @q.push [:NUMBER, $&.to_i] when /\A[a-zA-Z]\w*/ @q.push [:IDENT, $&] when /\A.|\n/o s = $& @q.push [s, s] end str = $' end @q.push [false, '$end'] do_parse end def next_token @q.shift end end parser = Calcp.new puts puts 'type "Q" to quit.' puts while true puts print '? ' str = gets.chop! break if /q/i =~ str begin parser.parse(str) rescue ParseError puts $! end end
calc.y と違うところは、inner の部分を class Calcp 〜 end でクラスの定義に変えてあること、メインルーチンの標示の部分で = と parser.pars(str) メソッドの戻り値を標示しないようにしてあるところです。また、冒頭に require 'calc_rule.tab.rb' として、calc_rule.y をコンパイルしてできた calc_rule.tab.rb を require するようにしています。たったこれだけの工夫で、パーサとスキャナの開発を別々に行うことができます。このプログラムを実行するには ruby calc_scan.rb とします。次のように、式のシンタックスのチェックをしてくれれば成功です。
? 1 + 2 syntax OK
上の例の Racc の使い方の面白い点は、抽象的な文法規則しか作っていないプログラムが動作してしまうということです。個々の文の動作を実現するプログラムはできていなくても、文法的に正しい文を解釈するプログラムを先に作ってしまうことができます。意味論の言葉でいうと、統語論的な分析のみをするプログラムが作成できていることになります。これは、プロトタイプのプログラムを作るのに便利です。完全に仕様が決まっていなくても、取り敢えず操作手順だけを先にコーディングすることができるからです。
使い方が複雑で難しそうにみえた Racc も基本的な利用法は案外定型的で、機械的に書いて行くことができるのが分かったと思います。また、Ruby プログラムを作成してくれるので、変更や応用が非常に簡単です。Racc をどんどん使ってプログラム作成のホビーライフを充実させてください。
このページに書いたことは Racc のほんのさわりです。興味を持たれたかたは是非、『Ruby を 256 倍使うための本 無道編』青木峰郎著、ASCII を買って読んで下さい。今のところ Racc を解説した唯一の参考書で、Racc の作者本人が書いておられます。
上の記事で大体 Racc の使い方が分かったと思いますが。そうすると、calc.y の改造だけではなく、全く別のプログラムに Racc を応用したくなってくるのは人情です。それも、日曜プログラマーとしては、できるだけ楽をする方法を取りたいものです。そこで思いついたのは逆ポーランド記法で計算できる電卓です。
逆ポーランド記法の計算で 1 + 2 は 1 2 + と表記します。よく言われることですが、これは日本語で計算するときの順序に似ています。1 2 + を「1 と 2 を足す」と読むことができるからです。逆ポーランド記法の特徴は括弧のついた計算が不用になることです。たとえば、(1 + 2) * (3 + 4) のような計算も、1 2 + 3 4 + * と書くことができます。これは、「1 と 2 を加えた値に、3 と 4 を加えた値を掛ける」と読むことができます。また、演算が二項演算だけに限定するようにしておけば、演算の優先順位も発生しません。
この逆ポーランド記法で計算できる電卓を作ってみようと思うのですが、スクラッチから作るのは嫌なので、calc.y をコピーして pol.y というファイルを作ります。結局は calc.y の改造と同じですが、この際固いことは言わないようにしましょう。
改造の手順ですが、逆ポーランド記法では演算の優先順位は要らないので、prechigh から preclow まですっぱりと削除します。また、rule の記述は exp exp '+' のように二項を先に書き、演算子を最後に記述します。項と演算子の順序が違うので、アクションの記述の配列のインデックスの数値を少し変更します。また、括弧は要らないので文法から削除します。数値の正負の反転はマイナス記号を使うことができなくなるので '~' を正負反転の演算子に使うことにします。こうしてできた pol.y の文法規則の部分は次のようになります。
# $Id: pol.y,v 1.4 2005/11/20 13:29:32 aamine Exp $ # # Very simple Reverse Polish calculater. class Calcp rule target: exp | /* none */ { result = 0 } exp: exp exp '+' { result += val[1] } | exp exp '-' { result -= val[1] } | exp exp '*' { result *= val[1] } | exp exp '/' { result /= val[1] } | exp '~' { result = -val[0] } | NUMBER end
なんか随分寂しくなって頼りなくなった気がしますがこれでいいのです。これを racc -o pol.rb pol.y でコンパイルして、ruby pol.rb で実行すると次のようになります。
type "Q" to quit. ? 1 2 + 3 4 + * = 21
「なんでこんなに簡単なの!!」と叫びたくなってしまいます。なんだか、自分で作ったプログラムに構造を持った命令文を実装することも夢ではないような気がしてきます。Racc は日曜プログラマこそが活用すべき道具ではないでしょうか。