クイックスタート

Spirit を使いたくなるのはなぜ?

Spirit は実用的な構文解析ツールとなるように設計されている。 控えめに言っても、 形式的な EBNF 仕様から、C++ でインラインに書かれた完全に動作するパーサを生成する能力は、開発時間を著しく削減する。 C や Pascal のような計算機言語を開発する際に、 YACC や ANTLR のような本格的なスタンドアローンパーサを使うことは現実的な話だ。 その一方で、非常に小さなマイクロパーサを書きたい時に大きな銃を持ち込むようなことは、明らかにやり過ぎである。 その一方、典型的にプログラマは目先の仕事を形式的な構文解析タスクとしてではなく、 scanf のような基本的なツールを用いた場当たり的なハックとして取り組む。 確かに、正規表現ライブラリ( boost regex のような) やスキャナ( boost tokenizer など)のようなツールはあるが、 これらのツールはより精巧なパーサを書く必要がある時にも使えるわけではない。 それなりに複雑なパーサでさえ、こうしたツールで書こうと考えているのなら、 そのコードは理解し難いものとなり、メンテナンスも容易ではなくなるだろう。

基本的な目的の一つはツールを使いやすく作ることである。 ある人がパーサジェネレータを考えるとき、よくある反応は"それは大きくて複雑で、急勾配な学習曲線を持つべきだ。"というものだ。 そうではない。 Spirit は完全に拡張可能になるように設計されている。 フレームワークは多層構造になっている。 このため、最小限のコアと基本概念だけを学べば、後は必要に応じて学べば良いようになっている。

開発の単純さと配置の簡便さのため、フレームワークの全体はヘッダーファイルのみから構成されており、 ライブラリのリンクやビルドは不要である。 spirit の配布ファイル群をインクルードパスに入れ、コンパイルして走らせるだけでよい。 コードの大きさだって?非常に小さい。 この後すぐに紹介するクイックスタートの例では、コードサイズは主に std::vector と std::iostream の インスタンス化によって占められている。

取るに足らない例 #1

浮動小数点数をパースするパーサを作る。

    real_p

(取るに足らないって言ったでしょ!)上のコードは実際には Spirit の real_parser(組み込みパーサの一つ)を生成する。これは浮動小数点数を構文解析するものである。 Spirit の流儀では、ユーザに直接使われるパーサの名前は "_p" で終わるということに注意して欲しい。 Spirit は多くの定義済みパーサを持っており、命名規則に拘ることはあなたが発狂するのを防いでくれるのだ!

取るに足らない例 #2

二つの浮動小数点数からなる一行を受け付けるパーサを作る。

    real_p >> real_p

慣れ親しんだ浮動小数点数パーサ real_p が二度、それぞれの数値に一度ずつ使われていることが分かるだろう。 >> 演算子は何をしているのだろうか? そう、それら [訳注:real_p] は何かによって隔てられなければならず、 これ [訳注: >> 演算子] が "後に続く" を意味する順序演算子として選ばれたのだ。 上述のプログラムは、二つのより単純なパーサを順序演算子でのり付けして、ある一つのパーサを作成している。 その結果は、より小さなパーサのコンポジション(composition)であるパーサである。 数値の間の空白は、パーサの呼び出し方に応じて暗黙のうちに消費される(後で述べる)。

注意:パーサを合成すると結局は "より大きな" パーサになるが、それでもそれはパーサである。 パーサはより大きくなり、入れ子を重ねることができるが、 二つのパーサをのり付けすれば、いつでもより大きなパーサを一つ得ることになる。 これは重要な概念である。

取るに足らない例 #3

任意の個数の浮動小数点数を受け付けるパーサを作る。(任意というのは 0 から無限までのいずれかである)

    *real_p

これは正規表現の Kleene Star に似ているが、この構文は C++ プログラマには少し奇妙に見えるだろう。 このようにオーバーロードされた * 演算子は見慣れないからだ。 実際、もし正規表現を知っているなら、星が修飾する式のにあることも同様に奇妙に映るだろう。 まあ人生はそんなものだ。 C++の文法規則でやらなくてはならないという現実を受け入れるしかない。

パーサが評価するあらゆる式は Kleene Star とともに使う事ができる。 注意して欲しいのは、にもかかわらず C++ の演算子プロシージャ規則のために、 複雑な式は括弧に入れる必要があるということだ。 Kleene Star は Kleene 閉包としても知られているが、我々はほとんどの場所で 星 と呼んでいる。

それなりに複雑な例

カンマで区切られた数値のリストを受け付け、その数値をベクタに入れるようなパーサを作る。

    real_p >> *(ch_p(',') >> real_p)

ch_p('x') に注目して欲しい。 これはカンマ ',' を認識することができるリテラル文字パーサである。 この場合、 Kleene Star はより複雑なパーサ、すなわち以下の式で生成されるものを修飾している:

    (ch_p(',') >> real_p)

これは括弧が必要な場合である点に注意して星。この Kleene star は上記の式をすべて囲い込む。

(出来上がった)パーサを使う

パーサが出来上がったところで、どうやって使うのだろうか? あらゆる C++ テンプレートオブジェクトの結果と同様に、 それを変数に格納したり、直接それで関数を呼び出したりすることができる。

いくつかの低レベルな C++ の詳細を解説し、良いものにしよう。 (We'll gloss over some low-level C++ details and just get to the good stuff.)

もし r がルールならば (それが正確にはどんなルールなのかは、この段階では気にしなくても良い。このことは後述する。 ルールはパーサを保持するプレースホルダ変数であるというだけで十分だろう。)、 以下のようにルールとしてパーサを格納できる:

    r = real_p >> *(ch_p(',') >> real_p);

そんなに興奮するようなものじゃない。これはあなたが何年も使ってきた他のどんな C++ 式とも同様に、 ただの代入である。 ルールにパーサを格納することの利点は: ルールはパーサであり、いまやそれを名前で参照する事ができるということだ。 (この場合、名前は r である)。 これは完全に代入式であるから、セミコロン ";" で終える点に注意して欲しい。

これで全てだ。我々はパーサを定義した。 従って次のステップはパーサがそれの仕事をするように呼び出すだけだ。 これを成すにはいくつかの方法がある。 とりあえず、 char const* を取る非メンバ関数 parse を使う事にしよう。 その関数は三つの引数を受け付ける:

NULL 終端を持つ const char* の入力
パーサオブジェクト
スキップパーサと呼ばれる別のパーサ

この例では、スペースやタブはスキップしたい。 Spirit の定義済みレパートリーの中に space_p と言う名前のもう一つのパーサがある。 これは単に空白を認識するだけの非常にシンプルなパーサである。 ここでは space_p をスキップパーサとして使おう。 そのスキップパーサは、スキップする文字 real_pch_p のようなパーサ要素の間にある読み飛ばされる文字に対して責任を持つ。

オーケー、それじゃあパースしよう!

    r = real_p >> *(ch_p(',') >> real_p);
    parse(str, r, space_p) // Not a full statement yet, patience...

parse 関数はあるオブジェクト(parse_info と呼ばれる)を返す。 このオブジェクトは、他のものと一緒に、構文解析の結果を保持している。 この例では、我々は以下の事を知らなければならない:

パーサは入力 str の認識に成功したか?
パーサは完全にパースし、入力を最後まで消費したか?

我々がこの時点で何を得たのか、その全体像を得るために、このパーサを関数内にラップしよう:

    bool
    parse_numbers(char const* str)
    {
        return parse(str, real_p >> *(',' >> real_p), space_p).full;
    }

注目すべきは、この場合、プレースホルダーのルールをそぎ落とし、パーサを parse の呼び出しの中に 直接インラインで記述しているという点である。 parse を呼び出す際、その式はテンポラリに評価され、無名のパーサが parse() 関数に渡され、利用され、その後破棄される。

char と wchar_t のオペランド

注意深い読者はお気づきだろうが、このパーサの式は前述の例のように ch_p(',') を 用いるかわりに、 ',' を使っている。 C++ 構文の変換ルールのおかげで、これはまったく問題がない。 >> 演算子は、 charwchar_t の引数が左右どちらか(しかし両方ではない)にあるものを 受け付けるようオーバーロードされている。 もし少なくとも一つのパラメータがユーザ定義型ならば、ある演算子がオーバーロードされただろう。 この場合、 real_p>> 演算子の第二引数であり、 そしてそのためプロパーな >> のオーバーロードが使われ、 ',' は文字リテラルパーサに変換される。

ch_p を省略して呼ぶことは明白にされるべきだ: 'a' >> 'b' は spirit パーサではなく、数値表現であり、 'a' の ASCII(や別のエンコーディング)での値を 'b' の ASCII 値だけ右シフトする。 いずれにせよ、 ch_p('a') >> 'b''a' >> ch_p('b') はどちらも Spirit の順序パーサであり、 'a' とその後に続く 'b' の文字をパースする。 このことには、遅かれ早かれ、慣れるだろう。

parse 関数が返すオブジェクトは full と呼ばれるメンバを持っていることに注意して欲しい。 このメンバは、上述の要求を全て満たすとき(つまり、パーサが入力を完全にパースしたとき)真となる。

セマンティックアクション

上述のパーサは実際にはただ認識するだけのものである。 "入力が文法にマッチするか?" という問いには答えるが、 いかなるデータも記憶せず、何の副作用も施さない。 思い出して欲しい:我々はパースした数値をベクタに入れたかったのだ。 これは適切なパーサにリンクされたアクションによって成される。 例えば、実数をパースするとき、そのマッチが成功した後にパースした数値を保持したいとする。 この時、我々はパーサから情報を取り出したい。 セマンティックアクションがこれを行う。 セマンティックアクションは文法仕様のあらゆるポイントに取り付けられることができる。 これらのアクションは C++ の関数かファンクタであり、パーサの一部が認識の一部を上手く認識した場合に呼び出される。 パーサを P 、 C++ の関数を F とすると、以下のように F を添付する事で、 入力がマッチしたときにパーサが F を呼ぶようにできる:

    P[&F]

あるいは、もし F がファンクタならば:

    P[F]

関数 / ファンクタのシグネチャは取り付けられるパーサの型に依存する。 real_p パーサはただ一つの引数: 構文解析された数値を渡す。 そのため、もし関数 Freal_p に取り付けたいのであれば、 F は次のように宣言する必要がある:

    void F(double n);

しかしこの例では、いくつかの定義済みファンクタとファンクタジェネレータを再び利用する事ができる ( ファンクタジェネレータはファンクタを返す関数である)。 この例の用途では、 Spirit はファンクタジェネレータ append(c) を持っている。 手短に言えば、このセマンティックアクションは、呼び出されると、それが取り付けられたパーサからパースされた値を受け取り、 これをコンテナ c追加する

最終的に、完全なカンマ区切りリストパーサはこのようになる:

    bool
    parse_numbers(char const* str, vector<double>& v)
    {
        return parse(str,

            //  文法のはじまり
            (
                real_p[append(v)] >> *(',' >> real_p[append(v)])
            )
            ,
            //  文法の終わり

            space_p).full;
    }

これは上述の物と同じパーサである。 今回は、 戦略的な位置に適切なセマンティックアクションが取り付けられており、パースされた値を取り出してそれをベクタ v に放り込む。(This time with appropriate semantic actions attached to strategic places to extract the parsed numbers and stuff them in the vector v.) parse_numbers 関数は、成功すると真を返す。

完全なソースコードはここから見る事ができる。 これは Spirit の配布ファイル群の一部である。
[ libs/spirit/example/fundamental/number_list.cpp を参照 ]



このドキュメントの対象: Boost Version 1.30.0
最新版ドキュメント(英語)