気まぐれな戯れ言 バックナンバー12


気まぐれな戯れ言バックナンバーです。全体の一覧はこちら

07/09/01 コンパイラ作ってみるかも編−その4
07/07/01 コンパイラ作ってみるかも編−その3
06/04/15 コンパイラ作ってみるかも編−その2
05/08/28 VBでOOP・STL風味編−3

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]を作ってみます。

E -> T E'
E' -> + T E' | #
T -> ( E ) | id

この表を作るとき、まず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が求まります。
文法に登場するすべての記号X(終端記号または非終端記号)に対し、以下の処理を行う。
各処理は、FIRSTに変化がなくなるまで繰り返す。
  1. Xが終端記号の時、FIRST(X) = { X }
    これはわかりますね。非終端記号の先頭は、それ自体です。

  2. Xが非終端記号で、かつX -> #のルールがあれば、FIRST(X)に{ # }を加える。
    これは次の処理で使うため、ある非終端記号が空文字#になりうるかを覚えておきます。

  3. Xが非終端記号で、X -> Y1 Y2 Y3 ... Ynというルールがある場合、まずFIRST(X)にFIRST(Y1)を加える。
    次にFIRST(Y1)が{ # }を含むなら、FIRST(X)にFIRST(Y2)も加える。
    FIRST(Y1)、FIRST(Y2)がともに{ # }を含むなら、FIRST(X)にFIRST(Y3)も加える。
    同様に、FIRST(Y1) ... FIRST(Yi)がすべて{ # }を含むなら、FIRST(X)にFIRST(Y(i+1))を加える。

    一見わかりにくいこの処理ですが、Y1〜Yiが全部から文字なら、Xの先頭はY(i+1)の先頭と一致するということを言っているに過ぎません。
下は、この処理をVBで実装したものです。
終端記号のFIRSTは自明なので、非終端記号のFIRSTを求めます。
ルールが配列P.ProdListに入っており、個々のルールについて上の処理を行います。
変数bは「FIRSTに変化がなくなるまで繰り返す」判定を行うためのもので、各ループで変化があるとTrueになります。

ソースだけみてもわかりにくいかも知れないので、興味があれば実際にプログラムを動かしてみてください。

Public Sub CreateFirst(P As Parser)
(変数宣言、初期化は省略)
Do
  b = False

  For i = 0 To P.NumProd - 1 '各ルールについてループ
    prod = P.ProdList(i)
    Set TC = P.First(IndexInCollection(prod.NonTerm, P.NontermList) - 1)
        
    If prod.SymbolList(0) = "#" Then 'このルールが空文字で始まるなら、FIRSTに空文字を追加。上の2.に相当
      b = b Or AddToCollection("#", TC)
    
    Else
      'それ以外の場合
      For j = 0 To UBound(prod.SymbolList)
        For k = 0 To j - 1 'j番目までの項目が全て空文字か?
          s = prod.SymbolList(k)
          
          If s <> "#" And IsTerm(s) Then Exit For '空文字以外の非終端記号なら抜ける
          'この非終端記号のFIRSTに空文字が含まれないなら抜ける
          If IndexInCollection("#", _
              P.First(IndexInCollection(s, P.NontermList) - 1)) < 0 Then Exit For
        Next
        
        If k < j Then Exit For '空文字がなく、途中でExit forしてきたなら終了
        
        'j番目の項目のFIRSTを追加
        s = prod.SymbolList(j)
        If IsTerm(s) Then '終端記号なら、それ自体がFIRSTの要素
          b = b Or AddToCollection(s, TC)
        Else
          '非終端記号なら各FIRSTを追加
          Set TC2 = P.First(IndexInCollection(s, P.NontermList) - 1)
          For k = 1 To TC2.Count
            b = b Or AddToCollection(TC2.Item(k), TC)
          Next
        End If
      Next
    End If
  Next
Loop While b '変化がなくなるまで繰り返す
End Sub

実際に、上の文法にこの処理を行うと、FIRSTは以下のようになります。
FIRST(E) = FIRST(F) = { ( , id }
FIRST(E') = { + , # }

3. FOLLOWの生成

次に、FOLLOWというものを考えます。
FOLLOW(X)は、Xの後に来る可能性のある記号の集合です。
たとえば、Tの後ろには+が来る可能性があります。
E' -> + T E' | #のルールから、Tの後ろにE'が来ることがあり、さらにE'の先頭が+になりうるからです。

解析表Mの作成には、FIRST同様に各記号のFOLLOWも必要となります。
上の例を見ると、FIRSTに比べFOLLOWの計算は大変そうです。
しかし、逆に上の例が理解できれば、もうFOLLOWの計算はわかったようなものです。

なお、FOLLOWの計算には、先にFIRSTが計算済みである必要があります。
まず、開始記号Sに対し、FOLLOW(S) = { $ }とする。
$は前回も出た文末を示す記号です。
開始記号は、この場合は全体がEであるような構文解析を行いたいので、E。

次に、文法に登場するすべての記号X(終端記号または非終端記号)に対し、以下の処理を行う。
各処理は、FOLLOWに変化がなくなるまで繰り返す。
  1. ルール A -> p B q (p,qは記号列)があった場合、FOLLOW(B)にFIRST(q)のうち#以外を追加する。
    これは上のTの例そのままですね。B=T、q=E'と置き換えるとまんまです。

  2. ルール A -> p B q に対し、かつFIRST(B)が#を含む場合、もしくはA -> p B というルールがある場合、FOLLOW(B)にFOLLOW(A)を加える。
    これもなんとなくわかるでしょうか。
    qが#になりうる場合、BはAのルールにおける最後尾になるわけです。
    なので、FOLLOW(A)であるものはFOLLOW(B)にもなります。
注意点として、例えばX -> S T U V Wというルールがある場合、上のBにはS〜Wが入る5通りを考える必要があります。
FIRSTと比べ少し複雑ですね。

これを実装すると以下の感じになります。

Public Sub CreateFollow(P As Parser)
(変数・初期化は省略)
P.Follow(0).Add "$"

Do
  b = False
  
  For i = 0 To P.NumProd - 1
    prod = P.ProdList(i)
    
    '各リストについて調べて行く  A := a b c・・・のとき AのFollowを検索
    Set TC = P.Follow(IndexInCollection(prod.NonTerm, P.NontermList) - 1)
    
    'リスト中の各非終端記号について計算
    For j = 0 To UBound(prod.SymbolList)
      If IsTerm(prod.SymbolList(j)) = False Then
      
        Set TC3 = P.Follow(IndexInCollection(prod.SymbolList(j), P.NontermList) - 1)
        Set TC2 = GetListFirst(P, prod.SymbolList, j + 1) '後ろのリストのFIRSTを得る

        For Each s In TC2
          If s <> "#" Then b = b Or AddToCollection(s, TC3) '空文字以外を追加
        Next
        
        '最後尾もしくは最後まで空文字で到達するか?
        If IndexInCollection("#", TC2) > 0 Or j = UBound(prod.SymbolList) Then
          For Each s In TC
            b = b Or AddToCollection(s, TC3)
          Next
        End If
      End If
    Next
  Next
Loop While b '変化がなくなるまで繰り返す

End Sub

実際に、上の文法にこの処理を行うと、FOLLOWは以下のようになります。
FOLLOW(E) = FOLLOW(E') ={ $ , ) }
FOLLOW(T) = { $ , ) , + }

4. 解析表の作成

長々とFIRSTとFOLLOWの解説をしてきました。
これでようやく解析表Mの作成に移れます。
ここではFIRSTとFOLLOWを使います。

文法中の各ルールA → pに対して、以下の処理を行う。

  1. FIRST(p)に含まれる各終端記号aについて、M[A,a]にA -> pを代入する。
    これは、今からAを解釈しようというときに終端記号aが来たら、ルールA -> pが適用できるだろう、ということを示します。

  2. FIRST(p)に空文字#が含まれていれば、FOLLOW(A)の各終端記号bに対し、M[A,b]にA -> pを代入する。
    また、FOLLOW(A)が文末記号$を含むなら、M[A,$]にA -> pを代入する。
    これも同様ですね。pが#になりうるなら、Aの後に来る文字が来うる。
もし、表の1つの項目に2つのルールが入りそうな場合、それはLL(1)では解釈できない文法であり、エラーになります。
一方、表の中で空の項目があってもかまいません。

これをVBで実装すると、こんな感じになります。
FIRSTやFOLLOWに比べると、実装が素直かな。

Public Sub CreateTable(P As Parser)
(変数定義・初期化は省略)
For i = 0 To P.NumProd - 1 '各ルールごとに処理
  prod = P.ProdList(i)
  ni = IndexInCollection(prod.NonTerm, P.NontermList) - 1
  Set Follow = P.Follow(ni)
  Set First = GetListFirst(P, prod.SymbolList, 0)
  
  'FIRST内の各終端記号について処理
  For Each s In First
    If IsTerm(s) And s <> "#" Then

      ti = IndexInCollection(s, P.TermList) - 1
      If P.Table(ni, ti).NonTerm <> "" Then
        'エントリに2つルールがある場合、エラー
        MsgBox "M[" + prod.NonTerm + "," + s + "] で重複"
      Else
        P.Table(ni, ti) = prod
      End If
    End If
  Next
  
  'FIRSTに空文字を含む場合
  If IndexInCollection("#", First) >= 0 Then
    For Each s In Follow 'FOLLOWの内容を見てルールを代入する
      ti = IIf(s = "$", P.TermList.Count, IndexInCollection(s, P.TermList) - 1)
      
      If P.Table(ni, ti).NonTerm <> "" Then
        'エントリに2つルールがある場合、エラー
        MsgBox "M[" + prod.NonTerm + "," + s + "] で重複"
      Else
        P.Table(ni, ti) = prod
      End If
    Next
  End If
Next

End Sub
実際に、上の文法にこの処理を行うと、FOLLOWは以下のようになります。
FOLLOW(E) = FOLLOW(E') ={ $ , ) }
FOLLOW(T) = { $ , ) , + }

5. 前回と今回のまとめ

今回の内容で解析表Mを作ると、ようやく前回の手順で構文解析ができるようになります。
前回と今回は、ほとんど教科書の内容っぽい感じで面白みが少なかったですね。
VBでの実装例は少ないので、参考になればとは思いますが…

次回は、いよいよflexを使った構文解析に入ります。


参考文献
  • [1] A. V. エイホ, R. セシィ, J. D. ウルマン, 原田 賢一 『コンパイラI・II』サイエンス社, 1990年(amazon).

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では、以下の形式のルール(生成規則)を並べて文法を表現します。
<X>  := <A> | <B> | <C> | ...
これは、「<X>は<A>または<B>または<C>または〜〜で定義される」ことを意味します。
<A>や<B>は複数の記号を羅列することができます。

例: <式> := <数値> | <式> + <式> | ( <式> )
この規則では、「式は、"1つの数値"か、"式+式"か"(式)"からなる」と読めます。
右辺にも<式>が出るように、再帰的な定義もできます。

この例に則ると、以下のいずれも式となります。
最後の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. ルールの修正

さて、いきなりですが上の<式>の例を以下の様に書き換えました。
少し記号が複雑になっています。
E -> T E'
E' -> + T E' | #
T -> ( E ) | id
Eというのが上の<式>に相当します。
#というのは空文字、つまり何もないことの代用です。(一般にεで書かれることが多いですが、今回は#で。)
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)は以下の処理手順を取ります。

開始時、まずスタックには文末を示す記号$および最終的に求めたいもの(この場合E)を入れる。
また、入力の最後にも$を入れておく。
スタックの最上位の記号をX、入力の先頭をaとして、エラーまたは終了するまで以下の処理を繰り返し行う。
  1. Xが終端記号の時
    1. X=a=$の時、完了。スタックの中身が解析結果。
    2. X=a!=$の時、スタックから最上位にあるaを除き、入力の先頭からaを除く。
    3. X != aの時、エラー。

  2. Xが非終端記号の時
    1. 表M[X,a]を参照し、X=A B C…Zというルールが入っていた場合、スタックからXを取り除き、A B C…Zを逆順に(Aが最上位になるように)積む。
      また、このM[X,a]を出力とする。
    2. 表M[X,a]が空ならエラー。

表M[X,a]というのは、「これから非終端文字列Xを解析するとき、入力が記号aだったらこのルールを適用する」というのが入っている表です。
この作り方はおいておいて、先ほどの式の例ではこの表は以下の様になります。
+ # ( ) id $
E     E := T E'   E := T E'  
E' E' := + T E'     E' := #   E' := #
T     T := ( E )   T := id  

さて、これに則って10 + ( 30 + 40 )を解析してみます。
最初に数字は全部idに置き換えておきます。

スタック入力出力解説
$ Eid + ( id + id ) $  
$ E' T id + ( id + id ) $(1) E := T E'2-aより、表M[E,id]をスタックに
$ E' id id + ( id + id ) $(2) T := id2-aより、表M[T,id]をスタックに
$ E' + ( id + id ) $ 1-bより、スタックと入力を1つ取り除く
$ E' T + + ( id + id ) $(3) E' := + T E'2-aより、表M[E',+]をスタックに
$ E' T( id + id ) $ 1-bより、スタックと入力を1つ取り除く
$ E' ) E (( id + id ) $(4) T := ( E )2-aより、表M[T,(]をスタックに
$ E' ) Eid + id ) $ 1-bより、スタックと入力を1つ取り除く
$ E' ) E' Tid + id ) $(5) E := T E'2-aより、表M[E,id]をスタックに
$ E' ) E' idid + id ) $(6) T := id2-aより、表M[T,id]をスタックに
$ E' ) E' + id ) $ 1-bより、スタックと入力を1つ取り除く
$ E' ) E' T + + id ) $(7) E' := + T E'2-aより、表M[E',+]をスタックに
$ E' ) E' T id ) $ 1-bより、スタックと入力を1つ取り除く
$ E' ) E' id id ) $(8) T := id2-aより、表M[T,id]をスタックに
$ E' ) E' ) $ 1-bより、スタックと入力を1つ取り除く
$ E' ) ) $(9) E' := #2-aより、表M[E',)]をスタックに
ただし、E'は空文字なので実際はスタックには何も加えない。
$ E' $ 1-bより、スタックと入力を1つ取り除く
$ $(10) E' := #2-aより、表M[E',)]をスタックに
ただし、E'は空文字なので実際はスタックには何も加えない。
   1-aより解析終了

上の出力を順に適用して式Eを展開していくと、以下の図の様に確かに10 + ( 30 + 40 )が出来上がります。



6. VBによる実装例

VB講座のその44−VBでLL(1)構文解析サンプルを利用すると、BNF文法と解析したい文字列を入れると表Mや解析結果を得ることができます。
(サイズの都合で、公開しているプログラムとボックスのレイアウトが異なります。)


また、このプログラムの実際の構文解析のルーチンは以下の様になっています。
(一部エラー処理を削ってあります)
5.の処理手順と見比べると良いかも知れません。

'実際の構文解析
Public Function ParseTokenlist(P As Parser, StartSymbol As String, Tokenlist() As TokenCls) As TTNode
Dim TopNode As New TTNode
Dim BottomNode As New TTNode
Dim TmpNode As TTNode, TmpNode2 As TTNode
Dim Stack As New Collection
Dim i&, j&, Token As TokenCls
Dim prod As Production
Dim ni&, ti&

'スタックに初期値を積む
'文末記号をまずスタックに入れる
BottomNode.Label = "$"
Stack.Add BottomNode

'解析したいルールをスタックに入れる
TopNode.Label = StartSymbol
Stack.Add TopNode

Do
  
  Set Token = Tokenlist(i) '現在の入力文字を取得
  Set TmpNode = Stack(Stack.Count) 'スタックの最上位を取得
  
  '終端記号の場合
  If IsTerm(TmpNode.Label) Or TmpNode.Label = "$" Then
    If TmpNode.Label = Token.Symbol Then
      'スタックと入力が一致するなら、それぞれ1文字取り除く
      i = i + 1  次の入力へ

      Call Stack.Remove(Stack.Count)
    Else
      'エラー
      Exit Function
    End If
  Else
    '非終端記号の場合
        
    '記号に対応する番号を取得
    ni = IndexInCollection(TmpNode.Label, P.NontermList) - 1
    If Token.Symbol = "$" Then
      ti = P.TermList.Count
    Else
      ti = IndexInCollection(Token.Symbol, P.TermList) - 1
    End If
    
    'テーブルMを参照
    prod = P.Table(ni, ti)
    
    'テーブルが見つからない場合
    If prod.NonTerm = "" Then
      'エラー
      Exit Function
    End If
    
    'X := Y1 Y2 Y3・・・に対して逆向きに積む
    Call Stack.Remove(Stack.Count)  'まずXをどかす

    If prod.SymbolList(0) <> "#" Then
      'スタックに入れていく
      For j = UBound(prod.SymbolList) To 0 Step -1
        Set TmpNode2 = New TTNode
        TmpNode2.Label = prod.SymbolList(j)
        Call Stack.Add(TmpNode2)
      Next
      'この時点で出力にも積んでいく
      For j = 0 To UBound(prod.SymbolList)
        Call TmpNode.SubNode.Add(Stack(Stack.Count - j))
      Next
    End If
  End If
  
Loop While i < UBound(Tokenlist)

Set ParseTokenlist = TopNode
End Function


7. まとめ
早足ですが、構文解析のためのBNF記法およびLL(1)文法を見ていきました。
しかしまだ課題は残っています。
「これから非終端文字列Xを解析するとき、入力が記号aだったらこのルールを適用する」という情報を持つ表Mの作り方が不明です。
次回、この表Mの作り方について見ていきます。

参考文献
  • [1] A. V. エイホ, R. セシィ, J. D. ウルマン, 原田 賢一 『コンパイラI・II』サイエンス社, 1990年(amazon).
  • [2] Wikipedia:LL parser.

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"

この文字列を意味ごとに分けろ、といわれたら、空白を除くと
total」 「=」 「3.5」 「+」 「data」 の5つに分けられます。
このように文字列を意味ごとに分けることを、字句解析と呼んでいきます。

人が見ると「あ〜"total"は一まとめで1つの変数だろうなぁ」と判断できますが、コンピュータが判断する場合はちゃんと判断基準を与えなければなりません。
例えばVB.Netにおける"変数"の定義[2]を簡単にまとめると以下のとおりです。

・最初の1文字はアルファベットかアンダースコア(_)。アンダースコアの場合は2文字目が必要。
・2文字目以降はアルファベットか数字かアンダースコア。

"total = 3.5 + data"を見たら、上のルールにより確かに"total"までが一まとまりで変数を表すことがわかります。
"="はアルファベット・数字・アンダースコアのいずれでもないですしね。

この様なルールをいちいち文章で書くのは面倒です。
それに文章で書いたルールを、コンピュータで処理できるように落とし込むのも面倒です。
と言うことで、人間にもコンピュータにも、そこそこわかりやすいルールの表現として正規表現を使っていくことにします。
(今後この戯れ言でコンパイラを作成する際に使うflexと言うツールも、正規表現を利用します。)

この場合、上のルールは"([a-zA-Z]|_[a-zA-Z0-9_])[a-zA-Z0-9_]*"と表現できます。
以下の項目では、正規表現を使える方を対象とし、正規表現の処理の実装を説明していきます。


2. 正規表現を考える

Perl等では高度な機能を持つ正規表現が利用できますが、とりあえず必要最低限の機能のみを考えてみます。

まず、正規表現の定義を行いたいと思います。
再帰的な定義に慣れていない方は、後述の例を参考にしてください。

(1) E = a or b or ... or z空文字及び小文字のアルファベット1文字は、正規表現
(2) E = E' E''2つの正規表現を繋げて記述したものは、正規表現
(3) E = (E')正規表現を括弧で囲ったものは、正規表現
(4) E = E' ?正規表現に「?」をつけたものは、正規表現
(5) E = E' *正規表現に「*」をつけたものは、正規表現
(6) E = E' +正規表現に「+」をつけたものは、正規表現
(7) E = E' | E''2つ正規表現の間に「|」をつけたものは、正規表現

この定義を用いると、以下の表現は正規表現になります。
abcde「a」〜「e」の各文字は(1)より正規表現、5つの正規表現に(2)を4回適用したものも正規表現
(a|b*)?「a」「b」は(1)より正規表現、「b*」は(5)より正規表現、「a|b*」は(7)より正規表現、「(a|b*)」は(3)より正規表現、「(a|b*)?」は(4)より正規表現

一方、「?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と文字列のマッチングを行うとき、すごろくの駒が次のルールで動くことを考えてみてください。
(A) 「S」のマスからは毎回、駒が新しく生まれる。
(B) 「ε」のついている矢印については、駒は矢印の指す先のマスにも分裂する。
(C) 文字列を頭から1文字ずつ見て行き、駒がいるマスから出ている矢印についた文字と、現在見ている文字が、
 (i) 一致するなら駒は矢印の先のマスに移動する。
 (ii) 一致しない場合、その駒はそこで消える。

文字列の1文字ごとに(A)(B)(C)をこの順番で繰り返します。
この駒がゴールの「T」まで到達すればマッチング成功。

これも実例を見てみます。
「dac」と言う文字をマッチングさせてみます。
(Flashとかでアニメーションがあるとわかりやすいんですが…)

行われる処理現在駒のいるマス
まず、(A)より駒がSに生成。S
(B)より、Sの駒は1,3にも分裂。S 1 3
(C)の判定では1文字目の「d」と一致する矢印はないので、(C-ii)より、駒は消える。なし
(A)(B)をもう一度繰り返す。S 1 3
1→2の矢印にある文字「a」と2文字目の「a」は一致しているので、(C-i)より1の駒は2で移動。それ以外は(C-ii)より消える。2
(A)(B)を再度繰り返すと、2の駒は6に分裂。S 1 2 3 6
3文字目の「c」は6から出る矢印と一致、6の駒はTに到達、マッチング成功。T

処理がだいぶややこしそうに見えますが、何とかマッチングができそうなことがわかるかと思います。


4. NFAの作成ルール

NFAを作ればマッチングができるのはいいとして、どうやってNFAを作るのか?という問題が残ります。
先ほどの「(a|bc)c*」程度の例なら何とか人間でも作れそうですが、複雑な正規表現に対してNFAを作るのは大変です。

ここで、先ほどの正規表現を考えるで挙げた正規表現の定義が役にたちます。
正規表現の定義(1)-(7)に対応するNFAの作り方さえわかれば、あとはその組み合わせでどんな複雑な正規表現に対してもNFAが作れることになります。

では、それぞれに対応するNFAの作成方法を見ていきます。
(1) E = a or b or ... or z
これは簡単です。SからTに対して、文字に対応する矢印を1本引くだけ。


(2) E = E' E''
2つの正規表現が続いた場合は、NFAもそれぞれをつなげるようにしてやります。
ここでは前の正規表現E'のゴールt(E')と、後の正規表現E''のスタートs(E'')をεの矢印でつないでいます。


(3) E = (E')
これは特に何もする必要はありません。
「?」とか「|」の結合の優先順位を指定するだけのものです。
数式でも括弧は計算順序を指定するためにつけるだけで、何も計算しないのと同じです。

(4) E = E' ?
これはE'を通ってもよいし、通らなくてもいいということで、SからTへの別のバイパスを作ることになります。


(5) E = E' *
E'を何回通っても良いということで、(4)に加え、E'内部に逆方向の「ε」の矢印がつきます。


(6) E = E' +
E'は1回は通らないといけないので、(5)から(4)のバイパスを除いた形になります。


(7) E = E' | E''
E'とE''のどちらかを通ればよいので、両方に分裂するように「ε」の矢印をつけてやります。


見てみると、妙に「ε」の矢印がたくさんあります。
そこは、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++でコンパイラ作りに入ります。


参考文献
今回NFAの画像の作成には、[3]のGraphvizに含まれる"dot"を利用しました。
dot用のファイルはここ(zip形式、2KB)においておきます。
掲載している画像は、dotの生成画像から余白などを取り除いたものです。

05/08/28 VBでOOP・STL風味編−3(VBでFunctor)[★★★★◎]
ここまでVBの機能で汎用なソート関数を実現してみました。
今回はもうちょいトリッキーなことにチャレンジ。

題材自体はそれほど難しくないです。
「配列の各要素に同じ関数を適用させて、配列内の値を置き換える」
というものです。

基本的なアプローチ Binderを作ってみる
さらに既定プロパティでトリッキーに まとめ

基本的なアプローチ

これ自体は簡単で、配列Arraysの各要素に関数Fooを適用させたければ、

For i = LBound(Arrays) To UBound(Arrays)
  Arrays(i) = Foo(Arrays(i))
Next

で終了です。
配列ではなくコレクションの場合は結果の代入方法が多少変わりますが、まぁ全体の処理としては似たようなもんです。

配列の要素に関して処理を行っていくというのはよくある処理です。
なので、毎回For文を回すなんで面倒なことをしなくても配列と関数だけ与えればいいようにできないかなぁ〜とか考えたくならないでしょうか。(ならないなら今回の話は意味がない…)
前回までのソートの話を思い出すと、以下の様な関数はさらっと書けるかと思います。

1つの引数を受け取って何か処理をして返すExecというメソッドを定義したインターフェースとして、IUnaryFuncクラスというものを定義しています。

あとはこのクラスをImplementsしたクラスを作って、Algorithm_ForEachに渡せば行いたい処理が行えます。
VBではコレクションの要素を直接編集することが出来ないので、削除と追加を繰り返すことで同じような処理をしています。

(IUnaryFunc.cls)
'1つの引数を受け取ってなんかしらするクラスのインターフェース

'単なるインターフェースなので中身は必要なし

Public Function Exec(ByVal v As Variant) As Variant
End Function

(別のbasファイル) Sub Algorithm_ForEach(dat As Collection, Unary As IUnaryFunc) Dim i& '直接コレクションのデータを書き換えることが出来ないので '操作終了後は最後に回して先頭を削除 For i = 1 To dat.Count Call dat.Add(Unary.Exec(dat(1))) Call dat.Remove(1) Next End Sub

例として入力値を2乗して返すようなSqrというクラスと、その使用例を作ってみます。

(sqr.cls)
Implements IUnaryFunc
'値を2乗するクラス

Private Function IUnaryFunc_Exec(ByVal v As Variant) As Variant
  IUnaryFunc_Exec = v * v
End Function


(Algorithm_ForEachを使うコード) Dim vec As New Collection Dim UnaryFunc As IUnaryFunc 'SqrクラスはIUnaryFuncをimplementsしたもの Set UnaryFunc = New Sqr '適当な値を作成 Call vec.Add(1) Call vec.Add(2) Call vec.Add(3) Call vec.Add(10) Call vec.Add(100) Call vec.Add(123) Call vec.Add(1000) Call Algorithm_ForEach(vec, UnaryFunc)

下の様に数値の入ったコレクションと、IUnaryFuncインターフェースをImplementsしたSqrクラスをAlgorithm_ForEachに渡すと、コレクション内の各数値が2乗されます。
(もしコレクション内に数値ではない型のものが入っていると、Sqr.Execの段階で初めてエラーが発生する)

IUnaryFuncをImplementsしたクラスを色々作ると、それだけでコレクション内の要素に同じ処理を行うプログラムが簡単に作成できます。
Sqrクラスの様に数値だけを対象とするクラスを作ってもいいですし、文字列やクラスを対象としても大丈夫です。


Binderを作ってみる

上の方法を使う場合、異なる処理を行いたい場合には異なるクラスを作成する必要があります。
とはいえ、「1を足す」「3を足す」「100を足す」と言う処理を行いたい場合、似たような処理なのに3つもクラスを作るのは面倒です。
対処法としては、別のプロパティを作成しておいて先に足すべき数値を指定しておき、Exec内で引数と事前に設定したプロパティの数値を足すという手法があります。

以下のクラスは事前にAddValueプロパティに入れておいた値を足すクラスです。

Implements IUnaryFunc
Dim m_AddValue
'事前に設定した値を加算するクラス

Public Property Let AddValue(ByVal v As Variant)
  m_AddValue = v
End Property

Private Function IUnaryFunc_Exec(ByVal v As Variant) As Variant
  IUnaryFunc_Exec = v + m_AddValue
End Function

確かにこれでも目的は達成できるのですが、
「事前に指定した値と組み合わせて(2つの引数を取る)別の関数を呼ぶ」
という処理はよく出てきます。(上の例ではExecは関数は呼びませんが、2値の加算をしているので似たようなもの)

この事前に指定した値と組み合わせるという処理を汎用化してみます。

これまで1つの引数を取る関数Execを持つインターフェースをIUnaryFuncとして作成してきました。
同様に、2つの引数を取る関数Execを持つインターフェースIBinaryFuncを作成してみます。

'2つの引数を受け取ってなんかしらするクラスのインターフェース

'単なるインターフェースなので中身は必要なし
Public Function Exec(ByVal lhs As Variant, ByVal rhs As Variant) As Variant
End Function

このインターフェースをImplementsするクラスとして、2値の引き算を行うSubClsというクラスを作成してみます。

'2つの値を引き算するクラス
Implements IBinaryFunc

Private Function IBinaryFunc_Exec(ByVal lhs As Variant, ByVal rhs As Variant) As Variant
  IBinaryFunc_Exec = lhs - rhs
End Function

このクラスはあくまでIBinaryFuncをImplementsしたものであるため、Algorithm_ForEachにはそのまま適用することは出来ません。
そこで、IBinaryFuncと値をひとつ与えて、IUnaryFuncを生成するような仕組み(Binder)を作成します。
(関数型言語を触ったことがある人だと、カリー化みたいな物とか言った方がわかりやすいかも)

IBinaryFunc.Execは2つの引数を取るので、どちらに事前に準備した値を代入して、どちらにコレクション内の値を代入するかを選択する必要があります。
ここでは2種類のパターンを別々のクラスで作成してみます。
(プログラム中にSubstitute_Variant(a,b)という関数がありますが、これは単にa=bという代入を行うだけの関数です。bがクラスだったらsetをつけて代入をします。詳細は前回分のおまけ:型によらない代入を参照。)

(事前に指定した値を第1引数に、コレクション内の値を第2引数に代入するBind1stクラス)

Implements IUnaryFunc
Dim BinFunc As IBinaryFunc
Dim rhs As Variant

Public Function Init(bf As IBinaryFunc, r As Variant) As Bind1st

  'あらかじめ関数および値を受け取って保存しておく
  Set BinFunc = bf '関数を保存しておく
  Call Substitute_Variant(rhs, r) '第2引数を保存しておく
  Set Init = Me '自分自身の参照を返す

End Function

Private Function IUnaryFunc_Exec(ByVal v As Variant) As Variant

  'Initで渡された引数は第2引数にする
  Call Substitute_Variant(IUnaryFunc_Exec, BinFunc.Exec(v, rhs))

End Function


(事前に指定した値を第2引数に、コレクション内の値を第1引数に代入するBind2ndクラス) Implements IUnaryFunc Dim BinFunc As IBinaryFunc Dim lhs As Variant Public Function Init(bf As IBinaryFunc, l As Variant) As Bind2nd 'あらかじめ関数および値を受け取って保存しておく Set BinFunc = bf '関数を保存しておく Call Substitute_Variant(lhs, l) '第1引数を保存しておく Set Init = Me '自分自身の参照を返す End Function Private Function IUnaryFunc_Exec(ByVal v As Variant) As Variant 'Initで渡された引数は第1引数にする Call Substitute_Variant(IUnaryFunc_Exec, BinFunc.Exec(lhs, v)) End Function

どちらのクラスも、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から引く、すなわち正負の符号を反転する処理を行ってくれます。

Dim UnaryFunc(1) As IUnaryFunc
Dim Bin1 As New Bind1st
Dim Bin2 As New Bind2nd

'10引く関数
Set UnaryFunc(0) = Bin1.Init(New SubCls, 10)

'0から引く、すなわちマイナスにする関数
Set UnaryFunc(1) = Bin2.Init(New SubCls, 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の言語仕様として以下の様なものがあります。
  • 引数なしの関数は( )をつけることなく呼び出すことが出来る。
  • クラスに対して既定プロパティを指定しておくと、クラス変数を参照した際に既定プロパティの値を返すことが出来る。
これを利用すると、今までのコードを修正することなくこっそり値を修正するようなクラスをBinderに渡すことが出来ます。
例えば、参照するたびに1ずつ大きな値を返すクラスSeqCountを作ってみます。

Dim cnt As Long

'呼ぶたびに1つ大きな値を返すクラス Public Property Get Counter() As Long cnt = cnt + 1 Counter = cnt End Property

このクラスはCounterプロパティを読み込むたびに1ずつ大きな値を返します。
このCounterプロパティを既定プロパティにしてみましょう。

特定のプロパティを既定プロパティにするやり方はあまり触れられることがないので説明します。(ただし、以下はVB5の場合です。VB6は未確認)
まずはカーソルがCounterプロパティのコード内にある状態にします。
ここでメニューの「ツール」→「プロシージャ属性」を選択すると、メニューが出てきます。
そこで「詳細>>」を押すと出てくる「プロシージャID」のコンボボックスから「(既定値)」を選ぶとそのプロパティは既定プロパティになります。



「このプロパティは既定プロパティである」という情報は、ソースをコードペインで見ているだけではわかりません。
しかし、作成したクラスのソースファイルを見てみると、以下の様にAttributeという項目がこっそり書かれていることがわかるはずです。

Public Property Get Counter() As Long
Attribute Counter.VB_UserMemId = 0
Attribute Counter.VB_MemberFlags = "200"
  cnt = cnt + 1
  Counter = cnt

End Property

さて、このSeqCountクラスを使って、コレクションの値から1、2、3…と値を引いていく処理を行ってみたいと思います。

Dim UnaryFunc As IUnaryFunc
Dim Bin1 As New Bind1st

Dim vec As New Collection
Dim i&, v As Variant

'1,2,3・・・を引いていく
Set UnaryFunc = Bin1.Init(New SubCls, New SeqCount)

'適当な値を作成
Call vec.Add(1)
Call vec.Add(2)
Call vec.Add(3)
Call vec.Add(10)
Call vec.Add(100)
Call vec.Add(123)
Call vec.Add(1000)

'呼び出す関数はIndexの値でクラスが切り替わる
Call Algorithm_ForEach(vec, UnaryFunc)

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 - rhs
Bind1stクラスは第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回分全体をまとめときます。

  • VBはオブジェクト指向プログラムを組むには少々力不足な言語。
    ただしImplementsキーワードを使うとそれなりに面白いことが出来る。

  • VBにあるVariant型とレイトバインディング機構は今回の様なことをやるには結構便利。
    ただし速度的にはC++/STLの様に最適化がかかったりしないので期待できない。

  • VBの「引数ががない関数呼び出しには括弧がいらない」「既定プロパティは"."をつけなくてもアクセスできる」の2つの言語仕様はC++等の言語にもない機能。
    トリッキーな使い方で色々出来るかも。
まぁ何にしても速度面が期待できない点で実用性は薄いですが、言語としての機能をいじくり倒すという意味ではなかなか面白い実験だったのではないでしょうかねぇ。


どうもこのサイトの内容ではCRCネタやFAT16ネタなど、資料性の高い話の方が興味を引いているらしい。
今回の様な「こんな実験やってみました」的なものは余り反応がよろしくないけど、VB好きには興味深く見てもらえたりするとうれしいなぁ。


一覧へ戻る