クロージャ

非常に便利な assign アクション を使って、どのようにパーサからデータを取り出すことが出来るかはすでに示した:

    int i;
    integer = int_p[assign(i)];

素晴らしい! 我々のintegerルールは、もし成功すれば、構文解析された整数を変数 i に渡す。整数を構文解析する必要があるときはいつでも、我々の integer ルールを呼び、そして変数 i から構文解析された数値を簡単に取り出すことができる。しかし、ここで気づくべきことがある。文法の視点では、 変数 i はグローバルである。文法がより複雑になってしまったときには、 i の現在の状態の追跡を続けることは難しい。 また、再帰ルールを用いると、グローバル変数は単に役立たずになってしまう。

もしあなたがあなたのルール(や文法)を再入可能にする必要があるなら、クロージャが必要となる。 例えば、ルール(や文法)は、それ自身から間接的ないし直接的に再帰呼び出しされることがある。電卓が良い例だ。 expression ルールは、 factor ルールを呼び出した際、 それ自身を間接的に再帰的に呼び出す。

我々は、我々のルール(や文法)に局所変数を取り付けるための機構を必要とする。 クロージャは局所変数が存在するための環境、つまりスタックフレーム、を提供する。 最も重要なことに、クロージャ変数は EBNF 文法仕様からアクセス可能であり、 またパーサ情報を上流や下流に、最上位ルールから終端記号へとトップダウンの再帰下降で渡すために使うことが出来る。 一つには、クロージャは継承属性合成属性を持つことのできるパーサを提供する。これらはほぼ戻り値関数引数に類似したものである。

ルールがクロージャを与えられると、 parse 関数に入る前にそのクロージャの局所変数が作成され、 parse 関数を抜けた後に破壊される。 これらの局所変数はハードウェアのスタックに存在する真の局所変数である。

クロージャPhoenix

Spirit v1.6.0 のクロージャサポートは Phoenix を要求する。 将来的には、 Spirit は BLL を完全にサポートする。 現在、両方のライブラリの機能をマージする作業が進行中である。

クロージャの利用

クロージャの宣言

一般的なクロージャ宣言の構文は:

    struct name : spirit::closure<name, type1, type2, type3..., typeN>
    {
        member1 m_name1;
        member2 m_name2;
        member3 m_name3;
        ...
        memberN m_nameN;
    };

member1... memberN は実際のクロージャ変数への間接リンクである。 それらの間接的な型は type1...typeN に対応する。以下は二つのクロージャメンバを持つ例である:

    struct my_closure : closure<my_closure, double, int>
    {
        member1 x;
        member2 y;
    };

member1member2 は実際のクロージャ変数への間接的なリンクである。それらの間接的な型は double と int にそれぞれ対応する。

クロージャの戻り値(合成属性)

クロージャの member1 はクロージャの戻り値(構文解析の用語では合成属性)である。この戻り値は、 anychar_p によって返されるものと同様に、例えば、データをパーサ階層構造の上の方へと伝播するために使われたり、あるいはセマンティックアクションに渡されたりする。

クロージャの取り付け

クロージャはルール、サブルールおよび文法に適用することができる。 クロージャはこれらの非終端記号で利用できる特殊なパーサコンテキストを持つ。 クロージャのコンテキストはルール、サブルールないし文法へフックするための手段である。上述の my_closure の例に対して、我々はクロージャを持ったあるルールを次のように宣言することで作成できる:

    rule<ScannerT, my_closure::context_t> r;

ここで ScannerT は利用したいスキャナの実際の型である。 同様にサブルールに対しても:

    subrule<ID, my_closure::context_t> sr;

そしてまた、もし文法に取り付けたいのであれば:

    struct my_grammar : grammar<my_grammar, my_closure::context_t>
    {
        /*...*/
    };

    my_grammar g;

クロージャメンバへのアクセス

クロージャのメンバはセマンティックアクションの中からアクセスすることができる。ちょうど構造体のメンバにするように、メンバ名とそれを所有するルール、サブルールあるいは文法とを指定すればよい。例えば:

    r.x  // ルール r のクロージャメンバ x を参照
    sr.y // サブルール sr のクロージャメンバ y を参照
    g.x  // 文法 g のクロージャメンバ x を参照

クロージャメンバの初期化(継承属性)

クロージャがルール、サブルールおよび文法を利用可能にしたクロージャは、デフォルトでは、 そのルール、サブルールおよび文法が parse メンバ関数に入るときにメンバのデフォルトコンストラクタを呼び出す。 もしこれが望み出なければ、ルール、サブルールないし文法に、コンストラクタ引数(構文解析の用語では継承属性)を渡すことが出来る。その構文は関数呼び出しを模倣している。例えば、 my_closure のメンバをそれぞれ 3.6 と 8 で構築したいなら、ルール r を呼び出す際に次のように書けばよい:

    r(3.6, 8) // ルール r を呼び出し、そのクロージャメンバを 3.6 と 8 にセットする

コンストラクタ引数は、実際には Phoenix のラムダ式である。そのため、次のような任意の複雑な式を使うことが出来る:

    // ルール r を呼び出し、そのクロージャメンバを (g.x / 8) * g.y と 0 に設定する
    // ここで g.x と g.y は上記の例の文法 g のクロージャメンバである
    r((g.x / 8) * g.y, 0)

クロージャメンバの実際の数より少ない引数を渡すことができる。 対応するコンストラクタ引数のない右側のメンバはデフォルトコンストラクタで構築される。例:

    r(11.9) // ルール r を呼び出し、そのクロージャメンバを 11.9 と 0 に設定する

クロージャメンバより多くの引数を渡すとエラーになる。

回文構造であっても構文解析するルールを考えよう: "abcddcba" のように原文の鏡像を認識できるパーサである。

クロージャなかりせば

クロージャが無ければ、これはスタックを用意することで達成できる。 最初に構文解析された文字をスタックにプッシュし、それから再帰的にそれ自身を呼び出すのである。 再帰から抜ける際、プッシュした文字をスタックからポップし、 次に構文解析された文字と一致判定するのに用いる。

ではクロージャを宣言しよう。ただ一つのクロージャメンバ char が必要である:

    struct my_closure : spirit::closure<my_closure, char>
    {
        member1 ch;
    };

ルールと文法コンテキストを思い出せるだろうか? これはクロージャを文法とルールにフックするための手段である。 クロージャは特殊なパーサコンテキストを持ち、それはルール、サブルールおよび文法のクロージャを利用可能とするために使われる。

    rule<scanner<>, my_closure::context_t> rev;

このクロージャを使って、ルール rev を宣言する。

    rev = anychar_p[rev.ch = arg1] >> !rev >> fch_p(rev.ch);

回文パーサを定義する。

1) anychar_p はあらゆる文字を受け付け、 rev の唯一のクロージャメンバ rev.ch に代入する。 Phoenix を用いて、 anychar_p の結果は rev.ch に代入される。 arg1 とは何だろう? もしこの時点で、まだラムダ式に馴染んでいないなら、 Phoenix プレースホルダ を さっと読んでみることをお勧めする。ともかく、それはセマンティックアクションが受け取る最初の引数のプレースホルダである:

    anychar_p[rev.ch = arg1]

2) 続いて、 rev はそれ自身を再帰的に呼び出す:

    >> !rev

最後に、

    fch_p(rev.ch)

3) rev.ch に格納した文字を認識しようと試みる。 fch_pch_p に非常に良く似ているが、 クロージャメンバにアクセスするようなファンクタを受け取る点に注意して欲しい( パラメトリックパーサを参照)。

プレースホルダ

Boost.Lambda (BLL) では、 arg1 に相当するものは _1 である。

クロージャの詳細

クロージャとは何か?

クロージャは関数の局所変数を "閉じ込め" て、その関数の外側から参照しアクセスできるようにするオブジェクトである。 より興味深いことは、クロージャが実際には局所的なコンテキスト(変数が存在するスタックフレーム)をパッケージし、それらが実際に存在するスコープの外から利用可能にするという点である。情報は基本的にそれをあらゆる場所、あらゆる時点で参照されるようにしたクロージャによって "捕らえられる"

次の図が図説している状況は次のようなものである。関数 A (あるいはルール)はそのクロージャを露出しており、 別の関数 BA の変数をそのクロージャによって参照している。

クロージャは関数の局所変数を "閉じ込め" て、その関数の外側から参照しアクセスできるようにするオブジェクトである

もちろん、関数 AA.x が参照される際にアクティブであるべきである。これが意味することは、関数 B は関数 A に依存しているということである(もし BA のネストされた関数であるなら、これは常に成り立つ)。 自由な形式であるという Spirit ルールの性質は、いつでもどこでもクロージャ変数にアクセスすることを許す。 A.x にアクセスすることは関数 A の最上位スタック変数 x を参照することと等価である。もし A.x が参照される際に関数 A がアクティブでないなら、実行時例外が投げられる。

ネストされた関数

クロージャの重要性を完全に理解するためには、 Pascal のようにネストされた関数を許す言語を見ておくのが最適である。 我々が扱っているのは C++ であるから、しばらくの間、 C++ がネストされた関数を許すと仮定しよう。次のような仮の C++ コードを考えよう:

    void a()
    {
        int va;
        void b()
        {
            int vb;
            void c()
            {
                int vc;
            }

            c();
        }

        b();
    }

三つの関数 ab および c があり、 cb の中にネストされ、 ba の中にネストされている。また三つの変数 vavb および vc がある。これら各局所変数の生存期間はそれが宣言された関数に入った時に始まり、その関数から抜ける時に終わる。局所変数のスコープは、その変数が宣言された関数が内側に囲んでいるネストされた関数すべての掛かっている。

関数 a から関数 c へ下流に行くと、関数 a に入ったとき、変数 va がスタックに作成される。 関数 b に入ったとき( a に呼ばれて)、 va はスコープのなかで very well であり、また b で参照できる。 その時点で新しい変数 vb がスタック上に作成される。 関数 c に入ったとき、 vavb の両方がスコープのなかで参照でき、 またその時点で新しい局所変数 vc が作成される。

上流に行くと、 vc は関数 c の外側で参照できなくなる。 vd の寿命はひとたび c を抜けた時点ですでに尽きている。 同じことは vb についても真である; vb は関数 c の中でアクセスできるが、関数 b の中ではできない。

ネストされた相互に再帰的なルール

さて ab および c というルールを考えよう:

    a = b >> *(('+' >> b) | ('-' >> b));
    b = c >> *(('*' >> c) | ('/' >> c));
    c = int_p | '(' >> a >> ')' | ('-' >> c) | ('+' >> c);

ab および c を相互に再帰的な関数と見なすことができる。 ab を呼び出し、 bc を、そして c は再帰的に a を呼び出すとする。 さて想像してみよう。もし ab および c のそれぞれが value という名前の局所変数を持っており、 value は明確な限定によって我々の文法から参照されることができる:

    a.value // a の局所変数 value を参照
    b.value // b の局所変数 value を参照
    c.value // c の局所変数 value を参照

上と同様に、 a に入った時点で、局所変数 value がスタック上に作成される。この変数は bc の両方から参照できる。 また、 ba に呼ばれると、 b は局所変数 value を作成する。この変数は c からはアクセスできるが、 a からはできない。

さて、ネスとされた関数のアナロジーはここで終わりだった: c が呼ばれ、新しい変数 value が作成され、通常、 c の生存期間の間だけ存続する。 にもかかわらず、 ca に再帰的に呼ばれることがあるという点に細心の注意を払って欲しい。 これが起こると、 ac の局所変数を参照することになる



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