parsimonious (arbitrary-lookahead parser for python) のへろわるど

Parsing In Python: Tools And Libraries より。

parsimoniouspyPEG と違ってこちらは「ちゃんと PEG」なことはドキュメントからすぐにわかったし、ノードトラパースのやり方も「書いてあるから印象いいよな」、と思った、初見で。

やってみたらノードのトラバースはやや鬱陶しい。ともあれ「へろわるど」:

 1 # -*- coding: utf-8 -*-
 2 from parsimonious.grammar import Grammar, NodeVisitor
 3 
 4 g = Grammar(
 5     """
 6     sentences   = sentence (~"\s+" sentence)*  #
 7     sentence    = word (~"\s+" word)* "."      # Grouping, One or more things.
 8     word        = styled_word / alnum          #
 9     styled_word = bold_word / italic_word      # Ordered choice.
10     bold_word   = "((" alnum "))"              #
11     italic_word = "''" alnum "''"              #
12     alnum       = ~"[A-Z0-9]+"i                # identical to re.compile(r"[A-Z0-9]+", flags=re.I)
13     """)
14 
15 class _Visitor(NodeVisitor):
16     def visit_sentences(self, node, visited_children):
17         return " ".join(visited_children)
18 
19     def visit_sentence(self, node, visited_children):
20         return " ".join(visited_children)
21 
22     # word        = styled_word / alnum
23     def visit_word(self, node, visited_children):
24         return " ".join(visited_children)
25 
26     # styled_word = bold_word / italic_word
27     def visit_styled_word(self, node, visited_children):
28         return "".join(visited_children)
29 
30     def visit_bold_word(self, node, visited_children):
31         return "<strong>{}</strong>".format(node.text[2:-2])
32 
33     def visit_italic_word(self, node, visited_children):
34         return "<i>{}</i>".format(node.text[2:-2])
35 
36     def visit_alnum(self, node, visited_children):
37         return node.text
38 
39     def generic_visit(self, node, visited_children):  # fallback
40         return " ".join(visited_children)
41 
42 tree = g.parse(
43     "Everything that exists ''works''.  Test coverage is ((good)).")
44 visitor = _Visitor()
45 print(visitor.visit(tree))

Visitor を自作せずとも「ぷりちぃぷりんと」は(この例での) tree オブジェクトが勝手にやってくれる。「何か価値あること」をしたいなら Visitor を書く、というノリ。「やや鬱陶しい」は後述。けどやや鬱陶しかろうが、pyPEG で悶々としたような類のストレスではなくて、だってほんの一時間程度でここまで辿りついたもの。印象はいいよ、すごく。

「構文定義としての PEG 記述」部分はやや独自。けどすぐにわかるであろ。Wikipedia の説明との対応も取りやすいであろう。

「ビジターの記述はやや鬱陶しい」については、基底クラスの NodeVisitor のせい:

parsimonious/nodes.py
161 class NodeVisitor(with_metaclass(RuleDecoratorMeta, object)):
162     # ...
163     def visit(self, node):
164         # ...
165         method = getattr(self, 'visit_' + node.expr_name, self.generic_visit)
166 
167         # Call that method, and show where in the tree it failed if it blows
168         # up.
169         try:
170             return method(node, [self.visit(n) for n in node])
171         except (VisitationError, UndefinedLabel):
172             # Don't catch and re-wrap already-wrapped exceptions.
173             raise
174         except self.unwrapped_exceptions:
175             raise
176         except Exception:
177             # Catch any exception, and tack on a parse tree so it's easier to
178             # see where it went wrong.
179             exc_class, exc, tb = exc_info()
180             reraise(VisitationError, VisitationError(exc, exc_class, node), tb)
181 
182     def generic_visit(self, node, visited_children):
183         # ...
184         raise NotImplementedError('No visitor method was defined for this expression: %s' %
185                                   node.expr.as_rule())

何が鬱陶しいかといえば、node と visited_children のどちらに関与すべきかを、Rule 記述を踏まえつつ考えなければならんから…、というか伝わりにくいな。yacc とかだと、各 produce は基本的に上の visitor で言うところの「node」だけ意識し、「最終的な木」とは別々に考えるわけなので、(それが場合によってはわずらわしいことはあるにせよ)少なくとも「同時に二つの異なる思考をする必要はない」のね。けどこのビジターは、自ノードと木全体を両方一気に意識しながら書かないといけない。なのでちと慣れないと相当頭混乱する。

アタシが書いたビジターで、visit_wordvisit_styled_word にだけあえてコメント入れてるのは、この例の場合は visit_bold_wordvisit_italic_word が「処理済」のテキスト(<strong>, <i> で修飾したテキスト)を「横流しすべきその時」だから。つまり node.text に言及してはダメ、そこでは。

それと、なんとなく「必要なとこだけ書いて、そうでないとこは全部 generic_visit で誤魔化したい」気分にはなるんだけれど、「そういう仕組み」な都合、現実には結局全ルール分書くことになるような気がする。まぁ yacc なんかも全部書くんだから、なんとなく釈然としない、というのもヘンな話ではあるんだけれど、「誤魔化せそうじゃん」と思うシカケになってるのと、あと yacc と違って「ルールとアクションが一体」じゃないからそう感じるんだろうね。

そういうわけで、「ほんの少しだけ、むむ」はあるけれど、非常に直接的で素直だし簡単。あと定義を文字列で記述するスタイルが好きなんだよね、あたしゃ。「普通の Python に混ぜ込む」スタイルって、書きながら自分が何を書いてるんだか混乱しがち(普通の python 使いをしてるのか PEG 定義部分を書いてるのか、てこと)だし、読み手も苦労するのさ。無論、pyPEG に感じた「PEG ってる気になれない」のとは正反対。なんなら「設計書を PEG で書いたとして、それをほぼストレートに記述出来る」であろう。

これは気に入りそうだ。よいとオモイマス


2:50 追記:
もう一つ第一印象の良い Arpeggio をみているところなのだが、比較していて parsimonious のビジターの印象が良くない真の原因がなんとなくわかってきた。要するに terminal、non-terminal 「全部」について書かなければならないのが大変鬱陶しい原因。Arpeggio は、terminal に関しては「必要ならば」、基本 non-terminal だけ書けばいいようだ。

実際 parsimonious のビジターがいやらしいのは、ワタシが書いた例だと「\s+」が generic_visit にディスパッチされてしまうからなのだ。けれどこれはルール sentencesentences の右辺なのだから、「child」として入ってくれてればそれで良かったのである。まだ実際に動かして試してはいないけれど、Arpeggio のほうのビジターの例(対応する構文定義) がまさにそうなっているようで、だから遥かにずっとこちらが印象が良い。圧倒的にわかりやすく、ワタシが良く知る yacc なんかとほとんど同じ思考でスムーズに理解出来る。