RakuのGrammarで四則演算を構文解析してみる

作成日:

概要

PerlやRubyのような言語には正規表現が言語機能として組み込まれています。正規表現はいろいろなところで活躍していますが、読みにくくなってしまいがちであったり、再起のある構文は解析できなかったりなどの弱点がありました。

Raku(旧称Perl6)には、その点への対処として、Perl5とは異なる正規表現記法が導入されたほか、Grammarというさらに強力な構文解析機能が組み込まれています。これは、Parsing Expression Grammar (PEG)と呼ばれる文法に従う言語を解析できるものらしい1です。Raku公式サイトのトップページのプログラム例で真っ先に挙げられているなど、Rakuの目玉機能の一つと思われるこのGrammarですが、日本語の解説を見ないので、公式チュートリアルを元に試しに使ってみた記事を書きます。

Rakuのインストール

1.3 Rakuのインストール - Raku 入門を参考にインストールしてください。

参考文献

  • Grammar tutorial: 今回の元ネタたるGrammarのチュートリアル。
  • Grammars: チュートリアルではない包括的なGrammarの解説。
  • Regexes: Rakuの正規表現の解説。Grammarのルールの記述には正規表現を用いるので、チュートリアルの例文によくわからないものが出てきたら適宜参照するとよさそうです。
  • Parsing Expression Grammar - Wikipedia

構文解析

コード例

短いので、いきなり完成形を示してしまいましょう。shisoku.rakuを次のように書きます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> }
    token 多項式 { <単項式> <加算> <多項式> | <単項式> }
    token 加算 { '+' | '-' }
    token 単項式 { <数> <乗算> <単項式> | <数> }
    token 乗算 { '*' | '/' }
    token 数 {<.ws> \d+ <.ws>}
}

my $m = 四則.parse('6*82 /4 + 43 -2*4');
say $m;

これを実行すると次のように表示されます。

$ raku shisoku.raku
「6*82 /4 + 43 -2*4」
 多項式 => 「6*82 /4 + 43 -2*4」
  単項式 => 「6*82 /4 」
    => 「6」
   乗算 => 「*」
   単項式 => 「82 /4 」
     => 「82 」
    乗算 => 「/」
    単項式 => 「4 」
      => 「4 」
  加算 => 「+」
  多項式 => 「 43 -2*4」
   単項式 => 「 43 => 「 43加算 => 「-」
   多項式 => 「2*4」
    単項式 => 「2*4」
      => 「2」
     乗算 => 「*」
     単項式 => 「4」
       => 「4」

いい感じに構文木ができていますね。日本語の鉤括弧が出てきていて、初めてみる方はなんだこれと思うかもしれませんが、Rakuは非ASCIIな記号をどんどん使っていくノリなので慣れてください。

なお、この構文木の各要素は

# オブジェクトのメンバとして読める
say $m.<多項式>.<単項式>.<単項式>.<数> # 「82 」
# .はなくてもいい
say $m<多項式><単項式><単項式><数> # 「82 」

のように、根から辿ることで参照することができます。

解説

文法の定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> }
    token 多項式 { <単項式> <加算> <多項式> | <単項式> }
    token 加算 { '+' | '-' }
    token 単項式 { <数> <乗算> <単項式> | <数> }
    token 乗算 { '*' | '/' }
    token 数 {<.ws> \d+ <.ws>}
}

my $m = 四則.parse('6*82 /4 + 43 -2*4');
say $m;

classを使ってクラスを定義するように、grammarを使って文法を定義します。ここで四則という名前で定義しています。今回は日本語にしましたが、これは四則演算を英語で何と言うかが出てこなかったからです。別に英語(や他の言語)でも構いません。

定義した文法に対してparseを呼ぶと、構文解析結果を返してくれます(この時の返り値$mをマッチオブジェクトといいます)。このマッチオブジェクトをsayすると、先ほど示したようにいいかんじに表示してくれます。

なお、もう少し厳密にオブジェクトの構造を見たい場合は

say $m.raku;

とします。rakuメソッドは、全てのオブジェクトに生えている2、そのオブジェクトのRaku的な表現を返す函数です。

トークンの定義(前半)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> }
    token 多項式 { <単項式> <加算> <多項式> | <単項式> }
    token 加算 { '+' | '-' }
    token 単項式 { <数> <乗算> <単項式> | <数> }
    token 乗算 { '*' | '/' }
    token 数 {<.ws> \d+ <.ws>}
}

my $m = 四則.parse('6*82 /4 + 43 -2*4');
say $m;

grammarの中には、文法の内容をtokenを用いて書いていきます。

まず、「全体は〜である」という主張をTOPに書きます。今回の場合、全体は多項式です。多項式は別に定義することにして、とりあえず<多項式>とだけ書いています。このように<>で囲むことで、別に定義したトークンを参照することができます。

次は、多項式。「多項式は単項式である」または「多項式は単項式と加算記号と多項式を順に並べたものである」という定義を素直に書いています。ここで若干注意すべきは、複雑なものを後に書いていることです。PEGはルールを前から順にあてはめていくので、両方当てはまりうる場合は注意する必要があります。まあ、なんとなくマッチしにくそうなものを前に持っていけばいいのではないでしょうか。なお、Rakuで正規表現を書くときの式では、空白が無視されます。{ <単項式> <加算> <多項式> | <単項式> }と書いても、{<単項式><加算><多項式>|<単項式>}と書いても同じです。

3つめは「加算」ですが、これは見ての通りです。こう書かずに

    token 多項式 { <単項式> [<和> | <差>] <多項式> | <単項式> }
    token 和 { '+' }
    token 差 { '-' }

のようにしてもよかったのですが、そうはしませんでした。これは、この後で行う演算がこの形式では不便そうだったからです。

トークンの定義(後半)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> }
    token 多項式 { <単項式> <加算> <多項式> | <単項式> }
    token 加算 { '+' | '-' }
    token 単項式 { <数> <乗算> <単項式> | <数> }
    token 乗算 { '*' | '/' }
    token 数 {<.ws> \d+ <.ws>}
}

my $m = 四則.parse('6*82 /4 + 43 -2*4');
say $m;

単項式と乗算に関しては多項式のところと同じです。

少し説明の必要があるのが数の定義(整数の定義)のところです。\d+は正規表現で「1文字以上の連続した数字」なのですが、これだけだと数式にスペースを入れたときに認識されなくなってしまいます。そこで、スペースを入れても良いようにするために、「0文字以上の空白」を表す組み込みルールである<.ws>を前後に入れました。「0文字以上の空白」は<ws>でもいいのですが、このように前に.を入れ<.ws>とすることで、キャプチャされない(構文木に現れない)ようにすることができます(この挙動はws以外でも同じです)。

なお、<.ws>は「0文字以上の空白」であると書きましたが、実は置かれた場所が普通の文字の間である場合(スペースがないと単語区切りがわからない場合)は、「1文字以上の空白」を表します。例えば、'a'<.ws>'*'a*にマッチしますが、'a'<.ws>'b'abにはマッチしません(a bにはマッチする)。詳しくは、wsの解説を読んでください。また、今回の記事では使いませんが、tokenの代わりにruleを使うと、正規表現の表記中の空白を自動で<.ws>に置き換えてくれます。

計算の実行

action object

さて、以上で構文木をつくることができました。この木を何らかの函数で読み込んで処理してやれば計算ができるはずですが、それはたくさんの場合分けが必要になって少し面倒かもしれません。そこで、構文解析時に処理を行って値を返す機能(action object)を使ってみましょう。

まずは、何もせずに適当な値を返す例からです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> }
    token 多項式 { <単項式> <加算> <多項式> | <単項式> }
    token 加算 { '+' | '-' }
    token 単項式 { <数> <乗算> <単項式> | <数> }
    token 乗算 { '*' | '/' }
    token 数 {<.ws> \d+ <.ws>}
}

class 四則計算処理 {
     method TOP($/) { make 6 }
     method 多項式($/) { make 7 }
}

my $m = 四則.parse('4', actions => 四則計算処理.new);
say $m.made[0]; # 6
say $m<多項式>.made[0]; # 7

まず、四則計算処理というクラスを作っています。ここに、値を計算して返す処理を入れていきます。この例ではTOP多項式という名前でメソッドを定義していますが、これはそれぞれ同名のトークンが見つかった時に為される処理になります。返す値は、returnではなくmakeという特別な函数を使って返します。

このクラスのインスタンスを作ってparse函数のactions引数に渡してやると、この処理を実行してくれます。得た返り値は、madeという配列に入ります。今回はただ整数の6と7を返しているだけなので、それがただ入っています。

数を返す

今度は、マッチした値を使ってみます。まずは簡単なからいきましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> }
    token 多項式 { <単項式> <加算> <多項式> | <単項式> }
    token 加算 { '+' | '-' }
    token 単項式 { <数> <乗算> <単項式> | <数> }
    token 乗算 { '*' | '/' }
    token 数 {<.ws> \d+ <.ws>}
}

class 四則計算処理 {
    method 数($/) { make $/.Int }
}

my $m = 四則.parse('4', actions => 四則計算処理.new);
say $m<多項式><単項式><数>.made[0]; # 4

こうすると、マッチしたのと同じ4が返ってきます。$/でマッチオブジェクトが受け取れるので、それをIntで整数にして返しているわけです。整数以外で返したい場合は、class Matchで適当な函数を探してみてください。

単項式を計算する

次は、単項式です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> }
    token 多項式 { <単項式> <加算> <多項式> | <単項式> }
    token 加算 { '+' | '-' }
    token 単項式 { <数> <乗算> <単項式> | <数> }
    token 乗算 { '*' | '/' }
    token 数 {<.ws> \d+ <.ws>}
}

class 四則計算処理 {
    method 単項式($/) {
        given $/<乗算> {
            when Nil {
                # ただの数の場合
                make $/<数>.made[0]
            }
            when '*' {
                # 掛け算の場合
                make $/<数>.made[0] * $/<単項式>.made[0]
            }
            when '/' {
                # 割り算の場合
                make $/<数>.made[0] / $/<単項式>.made[0]
            }
        }
    }
    method 数($/) { make $/.Int }
}

my $m = 四則.parse('5*6/7', actions => 四則計算処理.new);
say $m<多項式><単項式>.made[0]; # 4.285714
say $m<多項式><単項式>.made[0].raku; # <30/7>

今度は場合分けが生じるので、少し複雑になっています。givenは他の言語でいうswitchですから、$/<乗算>の値を見て「ただの数の場合」「掛け算の場合」「割り算の場合」に分けています。あとは、子要素の返した値をmadeで受け取って、計算して返しているだけです。

なお、ここで整数の割り算/をしていますが、Rakuの割り算/は、切り捨てるでも浮動小数点数を返すでもなく有理数を返すという変態仕様です。例でも、4.285714と表示されるものが内部では30/7で持たれていることがわかります。

多項式を計算する(完成)

同様にして多項式も実装すれば、完成です。

#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> }
    token 多項式 { <単項式> <加算> <多項式> | <単項式> }
    token 加算 { '+' | '-' }
    token 単項式 { <数> <乗算> <単項式> | <数> }
    token 乗算 { '*' | '/' }
    token 数 {<.ws> \d+ <.ws>}
}

class 四則計算処理 {
    method TOP($/) { make $/<多項式>.made[0] }
    method 多項式($/) {
        given $/<加算> {
            when Nil { make $/<単項式>.made[0] }
            when '+' { make $/<単項式>.made[0] + $/<多項式>.made[0] }
            when '-' { make $/<単項式>.made[0] - $/<多項式>.made[0] }
        }
    }
    method 単項式($/) {
        given $/<乗算> {
            when Nil { make $/<数>.made[0] }
            when '*' { make $/<数>.made[0] * $/<単項式>.made[0] }
            when '/' { make $/<数>.made[0] / $/<単項式>.made[0] }
        }
    }
    method 数($/) { make $/.Int }
}

my $m = 四則.parse('6*82 /4 + 43 -2*4', actions => 四則計算処理.new);
say $m.made[0]; # 158

確かに、6*82 /4 + 43 -2*4の計算結果である158が返ってきています。このように、makemadeを使うことで構文木の親に値を渡していくことができ、自然に再起的な処理を書くことができます。

以上の例では計算処理を別のクラスに書いてactionsで渡していましたが、実はgrammarの中に直接書いてしまうこともできます。

#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> { make $/<多項式>.made[0] } }
    token 多項式 {
        [ <単項式> <加算> <多項式> | <単項式> ]
        {
            given $/<加算> {
                when Nil { make $/<単項式>.made[0] }
                when '+' { make $/<単項式>.made[0] + $/<多項式>.made[0] }
                when '-' { make $/<単項式>.made[0] - $/<多項式>.made[0] }
            }
        }
    }
    token 加算 { '+' | '-' }
    token 単項式 {
        [ <数> <乗算> <単項式> | <数> ] 
        {
            given $/<乗算> {
                when Nil { make $/<数>.made[0] }
                when '*' { make $/<数>.made[0] * $/<単項式>.made[0] }
                when '/' { make $/<数>.made[0] / $/<単項式>.made[0] }
            }
        }

    }
    token 乗算 { '*' | '/' }
    token 数 {<.ws> \d+ <.ws> { make $/.Int } }
}

my $m = 四則.parse('6*82 /4 + 43 -2*4');
say $m.made[0]; # 158

パターンの後に中括弧で囲った処理を書くとそれが実行されるというわけです。orの処理があるときはそれぞれに対して処理を書くようになっているらしく、もとの例と同様に書くならば、[ <単項式> <加算> <多項式> | <単項式> ]のようにパターンを括弧で括ってorが外に出ないようにする必要がありました。

もちろん、orごとに処理が書けることを利用して次のように書くこともできます。

#!/usr/bin/env raku
grammar 四則 {
    token TOP { <多項式> { make $/<多項式>.made[0] } }
    token 多項式 {
          <単項式> '+' <多項式> { make $/<単項式>.made[0] + $/<多項式>.made[0] }
        | <単項式> '-' <多項式> { make $/<単項式>.made[0] - $/<多項式>.made[0] }
        | <単項式> { make $/<単項式>.made[0] }
    }
    token 単項式 {
          <数> '*' <単項式> { make $/<数>.made[0] * $/<単項式>.made[0] }
        | <数> '/' <単項式> { make $/<数>.made[0] / $/<単項式>.made[0] }
        | <数> { make $/<数>.made[0] }
    }
    token 数 {<.ws> \d+ <.ws> { make $/.Int } }
}

my $m = 四則.parse('6*82 /4 + 43 -2*4');
say $m.made[0]; # 158

加算乗算のトークンが不要になってすっきりしましたね。使い方などで、アクションオブジェクトを使うか否か適宜決めるとよさそうです。

まとめ

以上、RakuのGrammar機能で四則演算を構文解析し、計算を行う方法についてみてきました。「再帰的な正規表現」くらいの気持ちで気軽に使えるものだ、というのが印象です。PEGパーサーライブラリは多くの言語にありますが、標準機能に組み込んでいるのは、文字列処理に強い言語Perlを継ごうという意志を感じられて面白いなと思います。

問題は、今あまり構文解析したい対象がないことですね……。一時期は構文解析のことばかり調べていたのですが、その熱意が失われてしまっています。XMLやJSON、YAML、TOMLといった有名どころのパーサは誰かが公開しているので、あまり自分で書く必要が出てこないというのが大きい理由です。Visual C++を触っていた高校生の頃に比べると外部ライブラリを使う手間は100分の1くらいになっているのですが、そのぶん自分で難しいことをこなす動機がなくなって、少し寂しいかなとも思うのです。

次に読むとよさそうな資料

参考文献に挙げたもののほかに、次のようなものが参考になりそうです。


  1. 「らしい」と書いたのは、現在のRakuのドキュメントにはこのGrammarがPEGを解析できるといったことが記されている部分が見当たらないからです。Perl6の開発段階のドキュメントにはPEGを実装する旨が書かれているようです(Raku rules - Wikipediaの出典を参照)。まあ、機能を見たらPEGが解析できるか否かわかる人にはわかるのでしょうが……。 ↩︎

  2. Rakuでは全てのクラスがMu(無)クラスを継承しています。rakuメソッドはMuクラスで定義されています。 ↩︎



© 神和電子 2017-2023