07/09/01 | コンパイラ作ってみるかも編−その4(VBでLL(1)構文解析 後編)[★★★◎◎] | ||||||||||
前回VBでLL(1)文法の解析を行いました。 ただ、その中で使用した解析表の作り方を解説していなかったので、これを済ませておきます。 例によって実際に動くプログラムは、その44−VBでLL(1)構文解析サンプルにあります。 今回は短め。 1. 始めに 2. FIRSTの生成 3. FOLLOWの生成 4. 解析表の作成 5. 前回と今回のまとめ 参考文献 1. 始めに 前回の復習ですが、作りたい解析表M[X,a]は、 「これから非終端文字列Xを解析するとき、入力が記号aだったらこのルールを適用する」 という情報を持ちます。 前回と同じ以下の文法について、M[X,a]を作ってみます。
この表を作るとき、まずFIRSTとFOLLOWの2つの関数がよく利用されます。 2. FIRSTの生成 FIRST関数は、記号列を受け取って、その記号列から生成されうる終端記号の集合を返します。 たとえば、T -> ( E ) | idのルールから、 Tは"("か"id"のどちらかが先頭に来ることがわかります。 そのためFIRST(T) = { ( , id }です。 Tの場合は単純ですが、FIRST(E)はEの関わるルールだけでなく、他のルールも参照する必要があります。 たとえば、E -> T E'のルールより、Eの先頭はTの先頭と一致するので、FIRST(E) = FIRST(T) = { ( , id }となります。 これがもっと記号やルールが増えると、FIRSTを求めるのが大変そうです。 しかし実際には、単純なルールを繰り返し適用するだけでFIRSTが求まります。
終端記号のFIRSTは自明なので、非終端記号のFIRSTを求めます。 ルールが配列P.ProdListに入っており、個々のルールについて上の処理を行います。 変数bは「FIRSTに変化がなくなるまで繰り返す」判定を行うためのもので、各ループで変化があるとTrueになります。 ソースだけみてもわかりにくいかも知れないので、興味があれば実際にプログラムを動かしてみてください。
実際に、上の文法にこの処理を行うと、FIRSTは以下のようになります。
3. FOLLOWの生成 次に、FOLLOWというものを考えます。 FOLLOW(X)は、Xの後に来る可能性のある記号の集合です。 たとえば、Tの後ろには+が来る可能性があります。 E' -> + T E' | #のルールから、Tの後ろにE'が来ることがあり、さらにE'の先頭が+になりうるからです。 解析表Mの作成には、FIRST同様に各記号のFOLLOWも必要となります。 上の例を見ると、FIRSTに比べFOLLOWの計算は大変そうです。 しかし、逆に上の例が理解できれば、もうFOLLOWの計算はわかったようなものです。 なお、FOLLOWの計算には、先にFIRSTが計算済みである必要があります。
これを実装すると以下の感じになります。
実際に、上の文法にこの処理を行うと、FOLLOWは以下のようになります。
4. 解析表の作成 長々とFIRSTとFOLLOWの解説をしてきました。 これでようやく解析表Mの作成に移れます。 ここではFIRSTとFOLLOWを使います。
これをVBで実装すると、こんな感じになります。 FIRSTやFOLLOWに比べると、実装が素直かな。
5. 前回と今回のまとめ 今回の内容で解析表Mを作ると、ようやく前回の手順で構文解析ができるようになります。 前回と今回は、ほとんど教科書の内容っぽい感じで面白みが少なかったですね。 VBでの実装例は少ないので、参考になればとは思いますが… 次回は、いよいよflexを使った構文解析に入ります。 参考文献
|
07/07/01 | コンパイラ作ってみるかも編−その3(VBでLL(1)構文解析 前編)[★★★◎◎] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
前回の正規表現により、とりあえず字句解析に近いものは作れそうな目処がたちました。 コンパイラの処理としては通常、字句解析の次は構文解析を行います。 そこで、VBで基本的な構文解析を行ってみることにします。 実際に動くプログラムは、その44−VBでLL(1)構文解析サンプルにあります。 1. BNF記法 2. BNFと処理の関係 3. LL(1)構文解析器その問題点 4. ルールの修正 5. スタックを用いた構文解析 6. VBによる実装例 7. まとめ 参考文献 1. BNF記法 構文解析は、文法に沿って文を解析する処理です。 となると、文法をコンピュータが理解できるように表現しなければなりません。 プログラム言語の文法の表現にはBNF(Backus-Naur Form:Wikipediaの説明)記法がよく用いられます。 BNFでは、以下の形式のルール(生成規則)を並べて文法を表現します。
この例に則ると、以下のいずれも式となります。 最後の2つは、定義を再帰的に適用した例です。 10 、 3 + 5 、 ( 20 ) 、 10 + (30 + 40) 、 ( (10 + 20) + 30) + ( 40 ) この中で10 + (30 + 40)がどのようにBNFから導けるか考えて見ます。 まず、「<式> := <数値>」のルールより、数値はそれだけで式になります。 ここでまず式 + ( 式 + 式 )となります。 次に、「<式> := <式> + <式>」のルールより、式と式の和も式なので、 式 + ( 式 ) になり、 「<式> := ( <式> )」のルールより、式をカッコでくくったものも式なので 式 + 式 そしてまた式同士の和なので最終的に式となります。 最初にBNF記法は文法を表すと書きましたが、逆にBNFで表せないものは文法エラーと言えます。 例えば、「1 +」や「5 + 6 )」は上記のルールをどう使っても式にならず文法エラーです。 2. BNFと処理の関係 さて、BNF記法を使うと、文法を表現できることがわかりました。 では、先ほどの10 + (30 + 40)がどのような構成をしているか、木構造で見てみます。 青矢印は<式> := <数値>、 赤矢印は<式> := <式> + <式> 黒矢印は<式> := ( <式> )のルールに対応しています。 この構造がわかると、コンピュータが計算するための順序がわかります。 最終的に求めたいのは一番上、全体の式の値です。 そのためには、この木構造を下から辿り計算していけばよいことになります。 すなわち、BNF記法で示す文法に則って、プログラムからこの木構造を作り出すことができれば、プログラムの処理順序もわかると言えます。 これは単にこのような計算式だけではなく、if文やfor文についても同様です。 3. LL(1)構文解析器その問題点 さて、プログラムをコンパイルするためには、BNF記法に則って処理対象のプログラムを解析し、上の様な木構造を作らなければなりません。 これをどのようにコンピュータにやらせるか、というのが今回の本題になります。 種々の手法のうち、ここでは比較的単純なLL(1)構文解析という手法をVBで実装していきます。 LL(1)はお手軽な分それなりにデメリットもあり、解析できないルールがあります。 例えばXMLはLL(1)で解析できますが、C言語はLL(1)では解析できません。 LL(1)は簡単に言うと、「左から順に記号を読んでいって、行ける方向のルールを辿る」という方式です。 実は上の<式>のルールはLL(1)では解析できません。 例えば10 + ( 30 + 40 )を解析しようとする場合、最初の10だけみても、「<式> := <数値>」と「<式> := <式> + <式>」のどちらを適用した方がいいかは次の+をみるまで判断できません。 LL(1)で解析できないもう少しわかりやすい例として、VBのif文を考えて見ます。 if文にはelseがつく場合とつかない場合の2通りがあります。 <if文> := if <数値> then <処理> | if <数値> then <処理> else <処理> さて、解析中ifが出てきた段階で、左右どちらのルールが適用できるか…を判定することはできません。 もう少し後ろまで探してelseがあるかどうかチェックすれば、判定は可能でしょう。 しかしLL(1)は「左から順に読む」方式であり、後ろのelseをみて行き先を決める、ということはできません。 どちらの例でも、解決法は2つあります。 ・後ろの+やelseをみて判断するよう、もっと高度なアルゴリズムを使う。 ・LL(1)で解析できるようルールを少し書き換える。 今回はLL(1)で行くと決めてしまったので、後者で行きます。 4. ルールの修正 さて、いきなりですが上の<式>の例を以下の様に書き換えました。 少し記号が複雑になっています。
#というのは空文字、つまり何もないことの代用です。(一般にεで書かれることが多いですが、今回は#で。) idというのは数値の変わりだと思ってください。 E、E'、Tの様にルールの左辺に来る記号を、非終端記号と呼びます。 (、)、#、idの様に、ルールの左辺に来ない記号を終端記号と呼びます。 終端記号は、実際に解析対象に現れる記号になります。 これで本当に前と同じ表現が可能か確認します。 まず、E -> T E'のE'を分配法則を使って展開すると、E -> T + T E' | Tになります。 さらにE'を展開するとE -> T + T E' | T + T | Tとなります。 以後これを繰り返すと、結局E -> T | T + T | T + T + T | T + T + T + T | …とT同士の和を取るものは、Tが何個あっても式になります。 さらにTは、idまたは括弧に囲まれた式Eを表すので、結果上に挙げたこれらの表記はいずれも式Eから導けることがわかります。 10 、 3 + 5 、 ( 20 ) 、 10 + (30 + 40) 、 ( (10 + 20) + 30) + ( 40 ) 5. スタックを用いた構文解析 さて、LL(1)の説明に入って行きます。 入力10 + (30 + 40)を解析して、最終的に式Eとなることを示していきます。 LL(1)は以下の処理手順を取ります。
表M[X,a]というのは、「これから非終端文字列Xを解析するとき、入力が記号aだったらこのルールを適用する」というのが入っている表です。 この作り方はおいておいて、先ほどの式の例ではこの表は以下の様になります。
さて、これに則って10 + ( 30 + 40 )を解析してみます。 最初に数字は全部idに置き換えておきます。
上の出力を順に適用して式Eを展開していくと、以下の図の様に確かに10 + ( 30 + 40 )が出来上がります。 6. VBによる実装例 VB講座のその44−VBでLL(1)構文解析サンプルを利用すると、BNF文法と解析したい文字列を入れると表Mや解析結果を得ることができます。 (サイズの都合で、公開しているプログラムとボックスのレイアウトが異なります。) また、このプログラムの実際の構文解析のルーチンは以下の様になっています。 (一部エラー処理を削ってあります) 5.の処理手順と見比べると良いかも知れません。
7. まとめ 早足ですが、構文解析のためのBNF記法およびLL(1)文法を見ていきました。 しかしまだ課題は残っています。 「これから非終端文字列Xを解析するとき、入力が記号aだったらこのルールを適用する」という情報を持つ表Mの作り方が不明です。 次回、この表Mの作り方について見ていきます。 参考文献
|
06/04/15 | コンパイラ作ってみるかも編−その2(VBで正規表現)[★★★◎◎] | ||||||||||||||||||||||||||||||||||||||
字が詰まると見づらいので、今回から行間を少し広くしてみました。CSS対応のブラウザで見てね。 実際に動くプログラムは、その42−VBで簡易正規表現サンプルにあります。 前回のその1(前準備編)からずいぶんな時が経ってしまいました。 コンパイラ自体は2004年の時点で完成していたのですが、つい億劫でサイト掲載をサボっていた次第。 最近サイト更新ネタが軽いものばっかりだったので、ぼちぼち重いネタでも投下。 1. なぜ正規表現か 2. 正規表現を考える 3. NFA(非決定性有限オートマトン) 4. NFAの作成ルール 5. VBによるNFA作成とマッチングの実装 6. まとめ 参考文献 1. なぜ正規表現か なぜ「コンパイラを作る」と言っておいて「正規表現」の話題になるか。 以下のコードを考えてみます。
この文字列を意味ごとに分けろ、といわれたら、空白を除くと 「total」 「=」 「3.5」 「+」 「data」 の5つに分けられます。 このように文字列を意味ごとに分けることを、字句解析と呼んでいきます。 人が見ると「あ〜"total"は一まとめで1つの変数だろうなぁ」と判断できますが、コンピュータが判断する場合はちゃんと判断基準を与えなければなりません。 例えばVB.Netにおける"変数"の定義[2]を簡単にまとめると以下のとおりです。
"total = 3.5 + data"を見たら、上のルールにより確かに"total"までが一まとまりで変数を表すことがわかります。 "="はアルファベット・数字・アンダースコアのいずれでもないですしね。 この様なルールをいちいち文章で書くのは面倒です。 それに文章で書いたルールを、コンピュータで処理できるように落とし込むのも面倒です。 と言うことで、人間にもコンピュータにも、そこそこわかりやすいルールの表現として正規表現を使っていくことにします。 (今後この戯れ言でコンパイラを作成する際に使うflexと言うツールも、正規表現を利用します。) この場合、上のルールは"([a-zA-Z]|_[a-zA-Z0-9_])[a-zA-Z0-9_]*"と表現できます。 以下の項目では、正規表現を使える方を対象とし、正規表現の処理の実装を説明していきます。 2. 正規表現を考える Perl等では高度な機能を持つ正規表現が利用できますが、とりあえず必要最低限の機能のみを考えてみます。 まず、正規表現の定義を行いたいと思います。 再帰的な定義に慣れていない方は、後述の例を参考にしてください。
この定義を用いると、以下の表現は正規表現になります。
一方、「?abc」とか「+|?」は正規表現になりません。 上のルールをどう適用してもこれらの表現は作り出せないからです。 3. NFA(非決定性有限オートマトン) 上のルールで正規表現が出来ているのはわかりました。 では正規表現と文字列のマッチ処理を行うにはどうするか。 方法はいくつかありますが、ここでは一番単純なNFA(Nondeterministic finite automaton:非決定性有限オートマトン)を利用します。 NFAは、簡単に言ってしまえばすごろくみたいなものです。 NFAに文字列を与えると、文字列に従ってすごろくの上で駒が動きます。 駒がゴールに達したらマッチした、ゴールに達しなかったらマッチしない、と判断します。 いきなりですが、NFAの例を見てみます。 先ほどの「([a-zA-Z]|_[a-zA-Z0-9_])[a-zA-Z0-9_]*」を題材にしてみます。 このままだとわかりにくいので、[a-zA-Z]→a、_→b、[a-zA-Z0-9_]→cとそれぞれの文字クラスを1文字に置き換えてみます。 すると 「(a|bc)c*」 とだいぶ単純な形になります。 これをNFAにすると、以下の様になります。 このNFAと文字列のマッチングを行うとき、すごろくの駒が次のルールで動くことを考えてみてください。
文字列の1文字ごとに(A)(B)(C)をこの順番で繰り返します。 この駒がゴールの「T」まで到達すればマッチング成功。 これも実例を見てみます。 「dac」と言う文字をマッチングさせてみます。 (Flashとかでアニメーションがあるとわかりやすいんですが…)
処理がだいぶややこしそうに見えますが、何とかマッチングができそうなことがわかるかと思います。 4. NFAの作成ルール NFAを作ればマッチングができるのはいいとして、どうやってNFAを作るのか?という問題が残ります。 先ほどの「(a|bc)c*」程度の例なら何とか人間でも作れそうですが、複雑な正規表現に対してNFAを作るのは大変です。 ここで、先ほどの正規表現を考えるで挙げた正規表現の定義が役にたちます。 正規表現の定義(1)-(7)に対応するNFAの作り方さえわかれば、あとはその組み合わせでどんな複雑な正規表現に対してもNFAが作れることになります。 では、それぞれに対応するNFAの作成方法を見ていきます。
見てみると、妙に「ε」の矢印がたくさんあります。 そこは、NFAの意味が変わらない程度であれば、必要に応じて削ってもかまいません。 たとえば、(2)のt(E')→s(E'')の間の「ε」を削って、t(E')とs(E'')をくっつけて1つのマスにしてしまっても平気です。 正規表現に対して、このNFA作成ルールを適用すれば、複雑な正規表現でもNFAが作れるでしょう。 5. VBによるNFA作成とマッチングの実装 VBで正規表現によるマッチングを行う場合、行うべき作業は大きく分けて2つあります。 1つは正規表現からNFAの作成、もう一つはNFAと文字列のマッチングです。 NFAの作成 前者の正規表現からNFAの作成については、NFAの作成ルールに示した7つのNFA生成ルールをそのままコードに直せばうまくいくはずです。 プログラム部屋にあるサンプルでは、各マスを構造体で表し、NFA全体は構造体の配列で表しています。 矢印は、移動先の配列の添え字として表現しています。 7つのルールのうち、(1)や(2)では2つのNFAをくっつけなければならないため、配列での処理には少し苦労しています。 他の言語でNFAを作る場合、ポインタをうまく使ったほうが自然に実装できるでしょう。 VBでは構造体に対するポインタ型がないため、今回は配列で実装しました。 (ただし、クラスに対してはポインタ相当の処理が行えます。次回の構文解析で利用します。) なお、正規表現をNFAに変換する際に、実は正規表現自体を字句解析・構文解析する必要があります。 今回はここは力技で解析しています。 マッチング マッチングについては、NFA(非決定性有限オートマトン)で挙げた内容を素直に実装するとうまくいくと思います。 少しややこしいのが(B)の「ε」の処理です。 特に(5)の処理を行う場合、下手をすると無限ループに陥ってしまいます。 この対処には、「全部のマスについて1回だけ『ε』の処理を行う」→「処理前後で駒の位置に変化があったとき(今まで駒がなかったところに分裂で駒ができた場合)は、再度ループ」と言う実装で対処できます。 マスの数は無限ではないので、これでいずれ(B)の処理は終了します。 実行例 以下は、サンプルプログラムの実行例です。 正規表現「ab(a|b+)c+$」に対して、文字列「abbbcccc」のマッチング処理を行っています。 今回扱っていない、文末の「$」の処理も行っています。 右側のテキストボックスには、0-9までの10マスからなるNFAと、マッチング処理の途中経過(MatchProcess)を表示しています。 NFAの[61-61]は文字"a"を表し、[62-62][63-63][89-89]は"b"・"c"・"$"を表しています。 このNFAは以下の様になっています。 上のルール(1)-(7)で生成したものに比べて、だいぶ余分なε矢印を削っています。 MatchProcessを見ると、「abbbcccc」を1文字ずつ処理していくとだんだん0から9に向けて駒があるマス(○)が移動していることがわかります。 文字列最後の「cccc」が正規表現「c+」で何周もするため7と8を何往復もしていることもわかりやすいですね。 6. まとめ 何気なく使っている正規表現ですが、いざ実装しようとするとなかなか難しいです。 ここでは、正規表現の構成要素に対応したNFAを考えることで、複雑な正規表現に対しても個々の要素の組み合わせでNFAを作れるということを説明しました。 実用面を考えると、すべての正規表現マッチング処理がNFAを使っているわけではありません。 例えば確かPerlはNFAを使っていないはず。 NFAを使うと簡単に実装が行えますが、Perlのように括弧の中身に対応する文字列を抜き出すとか、繰り返しの回数を指定するといった処理はかなり難しくなります。 まぁ今回は正規表現の実装の一番単純な形を知るということで、NFAを用いた実装をしてみました。 次回はLL(1)文法の構文解析器をVBで作ってみます。 それが終わると、ようやくflex+bison+C++でコンパイラ作りに入ります。 参考文献
dot用のファイルはここ(zip形式、2KB)においておきます。 掲載している画像は、dotの生成画像から余白などを取り除いたものです。 |
05/08/28 | VBでOOP・STL風味編−3(VBでFunctor)[★★★★◎] | |||||||||||
ここまでVBの機能で汎用なソート関数を実現してみました。 今回はもうちょいトリッキーなことにチャレンジ。 題材自体はそれほど難しくないです。 「配列の各要素に同じ関数を適用させて、配列内の値を置き換える」 というものです。 基本的なアプローチ Binderを作ってみる さらに既定プロパティでトリッキーに まとめ 基本的なアプローチ これ自体は簡単で、配列Arraysの各要素に関数Fooを適用させたければ、
で終了です。 配列ではなくコレクションの場合は結果の代入方法が多少変わりますが、まぁ全体の処理としては似たようなもんです。 配列の要素に関して処理を行っていくというのはよくある処理です。 なので、毎回For文を回すなんで面倒なことをしなくても配列と関数だけ与えればいいようにできないかなぁ〜とか考えたくならないでしょうか。(ならないなら今回の話は意味がない…) 前回までのソートの話を思い出すと、以下の様な関数はさらっと書けるかと思います。 1つの引数を受け取って何か処理をして返すExecというメソッドを定義したインターフェースとして、IUnaryFuncクラスというものを定義しています。 あとはこのクラスをImplementsしたクラスを作って、Algorithm_ForEachに渡せば行いたい処理が行えます。 VBではコレクションの要素を直接編集することが出来ないので、削除と追加を繰り返すことで同じような処理をしています。
例として入力値を2乗して返すようなSqrというクラスと、その使用例を作ってみます。
下の様に数値の入ったコレクションと、IUnaryFuncインターフェースをImplementsしたSqrクラスをAlgorithm_ForEachに渡すと、コレクション内の各数値が2乗されます。 (もしコレクション内に数値ではない型のものが入っていると、Sqr.Execの段階で初めてエラーが発生する) IUnaryFuncをImplementsしたクラスを色々作ると、それだけでコレクション内の要素に同じ処理を行うプログラムが簡単に作成できます。 Sqrクラスの様に数値だけを対象とするクラスを作ってもいいですし、文字列やクラスを対象としても大丈夫です。 Binderを作ってみる 上の方法を使う場合、異なる処理を行いたい場合には異なるクラスを作成する必要があります。 とはいえ、「1を足す」「3を足す」「100を足す」と言う処理を行いたい場合、似たような処理なのに3つもクラスを作るのは面倒です。 対処法としては、別のプロパティを作成しておいて先に足すべき数値を指定しておき、Exec内で引数と事前に設定したプロパティの数値を足すという手法があります。 以下のクラスは事前にAddValueプロパティに入れておいた値を足すクラスです。
確かにこれでも目的は達成できるのですが、 「事前に指定した値と組み合わせて(2つの引数を取る)別の関数を呼ぶ」 という処理はよく出てきます。(上の例ではExecは関数は呼びませんが、2値の加算をしているので似たようなもの) この事前に指定した値と組み合わせるという処理を汎用化してみます。 これまで1つの引数を取る関数Execを持つインターフェースをIUnaryFuncとして作成してきました。 同様に、2つの引数を取る関数Execを持つインターフェースIBinaryFuncを作成してみます。
このインターフェースをImplementsするクラスとして、2値の引き算を行うSubClsというクラスを作成してみます。
このクラスはあくまでIBinaryFuncをImplementsしたものであるため、Algorithm_ForEachにはそのまま適用することは出来ません。 そこで、IBinaryFuncと値をひとつ与えて、IUnaryFuncを生成するような仕組み(Binder)を作成します。 (関数型言語を触ったことがある人だと、カリー化みたいな物とか言った方がわかりやすいかも) IBinaryFunc.Execは2つの引数を取るので、どちらに事前に準備した値を代入して、どちらにコレクション内の値を代入するかを選択する必要があります。 ここでは2種類のパターンを別々のクラスで作成してみます。 (プログラム中にSubstitute_Variant(a,b)という関数がありますが、これは単にa=bという代入を行うだけの関数です。bがクラスだったらsetをつけて代入をします。詳細は前回分のおまけ:型によらない代入を参照。)
どちらのクラスも、InitメソッドでIBinaryFuncクラスと、事前に指定する値を指定します。 いずれもExecメソッドが呼ばれると、Execメソッドの引数と事前に指定した値を元に、Initで指定されたIBinaryFuncのExecメソッドを呼び出します。 これらのクラスのExecメソッド自身は引数が1つなので、IUnaryFuncをImplements出来ることになります。 実際に使うときには以下のような感じ。 Bind1stクラス型の変数Bin1にSubClsと10という値を指定してInitを呼び、UnaryFunc(0)を生成しています。 このBind1stはコレクションの値を1つ目の引数に代入するようなクラスなので、このUnaryFunc(0)をAlgorithm_ForEachに渡すと、コレクション内の値から10引くという処理をやってくれます。 UnaryFunc(1)はBind2ndクラス型の変数にSubClsと0を与えて生成しています。 この場合、コレクション内の値は第2引数に代入されます。 第1引数は0であるため、Algorithm_ForEachを利用すると、コレクション内の値を0から引く、すなわち正負の符号を反転する処理を行ってくれます。
このBind1st・Bind2ndを利用すると、より多彩な処理をAlgorithm_ForEachで行うことが出来るようになります。 似たようなBinderをいくつか作っておくと、Binderを組み合わせて様々な処理を行うことが出来ます。 コレクションに対して掛けて足して割って…みたいな処理をループを使わなくてもAlgorithm_ForEachで表現できるようになります。 さらに既定プロパティでトリッキーに ここまでの話も余りVBでは行わないトリッキーな処理を行っています。 しかしここら辺はC++やSTLではちょくちょく行われている処理であり、そこまで珍しいものでもありません。 ここではさらにVBならではの手法を用いて変わった処理を行ってみたいと思います。 上のBinderを使った処理では、2値を取る関数と1つの値を指定することで多彩な処理を行うことが出来ました。 しかし、この「1つの値」がAlgorithm_ForEachの間中ずっと変化しないのは残念です。 例えば、「現在の時間の値を引く」という処理を行う場合、高い精度が必要なのであれば、Algorithm_ForEach内のループ中でも「現在の時間の値」も毎回更新されたものを引きたくなります。 しかしここまでの手法では事前にInitで指定しておいた時間の値しか引くことが出来ません。 対策としては、BinderであるクラスがExecの度に毎回「事前に指定する値」を更新するようにすることが考えられます。 現在のBinderは「事前に指定する値」として数値でもクラスでも利用できるようになっています。 しかし、「値」を更新するようにしてしまうと、時間の値を計算する機能を持つBinderを作るか、時間を返す関数を持つクラスを作成する必要があります。 前者の場合はBinderが非常に限定的な機能を持つものになってしまい、汎用性がなくなるという欠点があります。 後者の場合、Binderの中で何らかの形で値を更新するための関数呼び出しを行う必要があり、Binderが「事前に指定する値」として扱えるものがクラスだけになってしまい、数値を取れなくなってしまいます。 さて困った。 Binderが「事前に指定する値」として数値でもクラスでも取れるようにしつつ、クラスだったらこっそり値を更新できるような仕組みが欲しい。 ここで登場するのがVBならではの言語機能である「既定プロパティ」です。 VBの言語仕様として以下の様なものがあります。
例えば、参照するたびに1ずつ大きな値を返すクラスSeqCountを作ってみます。
このクラスはCounterプロパティを読み込むたびに1ずつ大きな値を返します。 このCounterプロパティを既定プロパティにしてみましょう。 特定のプロパティを既定プロパティにするやり方はあまり触れられることがないので説明します。(ただし、以下はVB5の場合です。VB6は未確認) まずはカーソルがCounterプロパティのコード内にある状態にします。 ここでメニューの「ツール」→「プロシージャ属性」を選択すると、メニューが出てきます。 そこで「詳細>>」を押すと出てくる「プロシージャID」のコンボボックスから「(既定値)」を選ぶとそのプロパティは既定プロパティになります。 「このプロパティは既定プロパティである」という情報は、ソースをコードペインで見ているだけではわかりません。 しかし、作成したクラスのソースファイルを見てみると、以下の様にAttributeという項目がこっそり書かれていることがわかるはずです。
さて、このSeqCountクラスを使って、コレクションの値から1、2、3…と値を引いていく処理を行ってみたいと思います。
New SeqCountで生成されたSeqCountオブジェクトは、Bind1stクラス内では特に何も行いません。 Initメソッド内でのCall Substitute_Variant(rhs, r)の時には変数rhsに代入されるだけです。 ExecメソッドでもCall Substitute_Variant(IUnaryFunc_Exec, BinFunc.Exec(v, rhs))でrhsはそのままInitで指定したIUnaryFunc.Execに渡されます。 この例ではBind1stクラスのInit時にSubClsを指定しているので、BinFunc.Exec(v, rhs)は実際にはSubClsのExecが呼ばれます。 このSubClsのExecでは、ようやく引き算が行われます。 IBinaryFunc_Exec = lhs - rhsBind1stクラスは第1引数としてコレクション内の値を渡すので、lhsはコレクション内の値です。 問題はrhs。rhsは元はといえばInitで渡されたSeqCountオブジェクトが入っています。 当然ながらオブジェクトを引き算するということは出来ないため、ここで初めて既定プロパティが参照されます。 すなわち、実際にはここでは IBinaryFunc_Exec = lhs - rhs.Counter()という処理が行われています。 しかしVBの仕様上、正しく既定プロパティを指定しておけばプロパティ参照の形式をとる必要がなくなります。 Algorithm_ForEachの実行後は、コレクションに入っていた[1,2,3,10,100,123,1000]という値はそれぞれ[1,2,3,,,]という値を引かれて[0,0,0,6,95,117,993]という値になります。 まとめ ではここら辺でまとめ。 VBのクラスで色々試すのは今回で最後なので、「VBでOOP・STL風味編」3回分全体をまとめときます。
どうもこのサイトの内容ではCRCネタやFAT16ネタなど、資料性の高い話の方が興味を引いているらしい。 今回の様な「こんな実験やってみました」的なものは余り反応がよろしくないけど、VB好きには興味深く見てもらえたりするとうれしいなぁ。 |