t-homさんが直近の記事でByRef As Variantで受けたときにアドレスが変わる事象について仮説を書いている。

thom.hateblo.jp

型一致参照であるShowPtr関数とVariant参照であるShowPtr2関数を作って比較し

ShowPtr2でVarPtr(IntArg)としたときは、Variant変数自体のアドレス1373176が表示されるが、Variant変数は内部で137362へのポインタを保持しているということだろう。

と結論付けているが、おおむね正しいと思う。


ただ、ByRef x As Variant としたときの x はいったい何者なのかと考えるとちょっと気持ち悪くて、引数の型によって参照だったり実体だったりするのではなくて、そもそも別々のものだと考えたほうがわかりやすいと感じた。
型一致参照であるShowPtr関数の挙動は明らかなので、ShowPtr2関数については疑似的にこういうふうに解釈すればいいんだと思う。

#include <iostream>

struct Variant {
    void* p;
    Variant() : p(nullptr)   { }
    //いろいろ省略
    template <typename T>   // 他の型で初期化するコンストラクタ
        Variant(T& t) : p(static_cast<void*>(&t))   { }
};

//Byref As Variant相当 実引数がVariantの場合 → 本当の参照
void ShowPtr2(Variant& v)
{
    std::cout << "varArg:" << &v <<std::endl;
}

//Byref As Variant相当 実引数がVariant以外の場合
template <typename T>
void ShowPtr2(T&& x)
{
    Variant v(x);    //→ 新たにVariant変数を作成
    std::cout << "intArg:" << &v <<std::endl;
}
 
int main()
{
    Variant varArg;
    int intArg = 0;
    std::cout << "-----------初期アドレス------------" <<std::endl;
    std::cout << "varArg:" << &varArg <<std::endl;
    std::cout << "intArg:" << &intArg <<std::endl;
    std::cout << "-----------Variant参照------------" <<std::endl;
    ShowPtr2(varArg);   // この ShowPtr2 と
    ShowPtr2(intArg);   // この ShowPtr2 は別物
}
-----------初期アドレス------------
varArg:0x7ffe5587a6e8
intArg:0x7ffe5587a6e4
-----------Variant参照------------
varArg:0x7ffe5587a6e8
intArg:0x7ffe5587a690

ByRef As Variant とは?

(自分用メモ)

VBAByRef x As Longと宣言された仮引数にLong型以外の実引数を渡すと、「ByRef引数の型が一致しません。」というエラーになる。しかしByRef x As Variantという宣言だったら Variant型以外の実引数、たとえばLong型でも渡すことができる。Variantなんだから当然と思われるかもしれないが、変数のアドレスを確認してみると、Variantを渡したときと他の型を渡した時では状況が違う。

Sub addressCheck()
    Dim a As Long: a = 2
    Dim v As Variant: v = 5
    Debug.Print "VarPtr(a) ="; VarPtr(a)
    Debug.Print "VarPtr(v) ="; VarPtr(v)
    Debug.Print "a ="; a; "v ="; v
    addressCheck_sub a    ' 引数のアドレスを出力して書き換える
    addressCheck_sub v    ' 引数のアドレスを出力して書き換える
    Debug.Print "a ="; a; "v ="; v
End Sub

' 引数のアドレスを出力して書き換える
Private Sub addressCheck_sub(ByRef x As Variant)
    Debug.Print "VarPtr(x) ="; VarPtr(x)
    x = 10
End Sub

この Sub addressCheck() を実行すると次のように出力された。

VarPtr(a) = 93921072 
VarPtr(v) = 93921048 
a = 2 v = 5 
VarPtr(x) = 93921024  'a と違うアドレス
VarPtr(x) = 93921048  'v と同じアドレス
a = 10 v = 10         'どちらも書き換えはできている...

Variant を渡したときアドレスは一致するので x に代入することは v に代入することと同じである。Long a を渡したとき、それを受ける引数 x のアドレスは a とは異なるにもかかわらず、x に代入した結果が a にも反映された。

引数 実引数 アドレス比較 代入結果
ByRef x As Variant a As Long 不一致 変更される
ByRef x As Variant v As Variant 一致 変更される

つまり、実引数の型によって x の状態が次のようになっている。(比喩的な表現が混じっている)

引数 実引数 アドレス比較 x の状態 ( x の内部 ) x = 10 の意味
ByRef x As Variant a As Long 不一致 aへのポインタ 0 != (VT_BYREF & x.vt) 間接代入
ByRef x As Variant v As Variant 一致 vの参照 0 == (VT_BYREF & x.vt) 直接代入

代入の意味が上と下では違っているが、普通にVBAを使っている分にはその違いを意識することはない。しかし VBAHaskell の swapVariant 関数のようにメモリ上のベタコピーを行うと、その違いが浮き彫りになる。

Sub swapTest()
    Dim a As Long, b As Long
    a = 1&: b = 2&
    swapVariant a, b
    Debug.Print a; b
    
    Dim v As Variant, w As Variant
    v = 1&: w = 2&
    swapVariant v, w
    Debug.Print v; w
End Sub

この Sub swapTest() を実行すると次のように出力される。

 1  2 
 2  1 

ふたつの変数をスワップしたはずなのに、 Long a と Long b の組み合わせのときはスワップされていない。swapVariant のシグネチャFunction swapVariant(ByRef x As Variant, ByRef y As Variant) As Long であり、x や y の指す先が変わっただけなので a にも b にも影響は及ばない。

スワップ スワップ 結果
x は a へのポインタ x は b へのポインタ aは1のまま
y は b へのポインタ y は a へのポインタ bは2のまま

Variant を渡したときはスワップの対象は変数自身なのでこうなる。

スワップ スワップ 結果
x = v = 1 x = v = 2 vは1から2に変化
y = w = 2 y = w = 1 wは2から1に変化

スワップと代入を伴うパターンはこうなる。

Sub swapTest2()
    Dim a As Long, b As Long
    a = 1&: b = 2&
    swapTest_sub a, b
    Debug.Print a; b
    
    Dim v As Variant, w As Variant
    v = 1&: w = 2&
    swapTest_sub v, w
    Debug.Print v; w
End Sub

Private Sub swapTest_sub(ByRef x As Variant, ByRef y As Variant)
    swapVariant x, y
    x = 111
    y = 999
End Sub
 999  111 
 111  999 

紹介記事の補足

t-homさんがVBAHaskellの紹介記事を書いてくれた。 thom.hateblo.jp
こんなことは僕の人生初なので、とてもうれしい。

ところでそこにapplyFun2by2関数が取り上げられていて少し驚いた。マイナーな関数だと思っていたからだ。t-homさんにはQiitaでも「before-afterで見せるとわかりやすいのでは?」という旨のコメントをいただいているので、さっそくこれをビフォー/アフターしてみたい。


まず、紹介されていたコードはこうだ。数値の列に演算を施すときに、演算そのものも引数として指定できるよ、という例になっている。

Sub TestVBAHaskell版()
    Call 計算(p_plus, 1000, 20, 3, 9)
    Call 計算(p_minus, 1000, 20, 3, 9)
    Call 計算(p_mult, 1000, 20, 3, 9)
    Call 計算(p_divide, 1000, 20, 3, 9)
End Sub

Sub 計算(Ope As Variant, ParamArray x())
    Dim total As Variant
    Dim i As Long
    total = x(0)
    For i = 1 To UBound(x)
        total = applyFun2by2(Array(total, x(i)), Ope)    ' ← ここ
    Next
    Debug.Print total
End Sub

このSub TestVBAHaskell版を実行すると、
 1032
 968
 540000
 1.85185185185185
と出力される。それぞれ
 1000 + 20 + 3 + 9
 1000 - 20 - 3 - 9
 1000 * 20 * 3 * 9
 1000 / 20 / 3 / 9
の計算結果である。

'applyFun2by2'は「2変数関数に2つの要素からなる配列を渡す」ときに使う関数であり、上のapplyFun2by2(Array(total, x(i)), Ope) は、意味的には Ope(total, x(i))である。これを少し別な書き方をするとこうなる。

Sub TestVBAHaskell版2()
    Call 計算2(p_plus, 1000, 20, 3, 9)
    Call 計算2(p_minus, 1000, 20, 3, 9)
    Call 計算2(p_mult, 1000, 20, 3, 9)
    Call 計算2(p_divide, 1000, 20, 3, 9)
End Sub

Sub 計算2(Ope As Variant, ParamArray x())
    Dim total As Variant
    Dim i As Long
    total = x(0)
    For i = 1 To UBound(x)
        total = applyFun(x(i), bind1st(Ope, total))    ' ← ここ
    Next
    Debug.Print total
End Sub

bind1stは「2変数関数の1番目の引数を束縛する」関数で、Ope(total, _ )プレースホルダである _ のところに実引数が繰り返し読み込まれる挙動となる。

だが、これらはどちらもVBAHaskellらしいコードではなく、2変数関数を繰り返し適用するのはfold系の関数を使うのが一番ラクだ。ループを書かずに済ませるのがVBAHaskellのもともとの目標でもあったし。

Sub TestVBAHaskell版3()
    Call 計算3(p_plus, 1000, 20, 3, 9)
    Call 計算3(p_minus, 1000, 20, 3, 9)
    Call 計算3(p_mult, 1000, 20, 3, 9)
    Call 計算3(p_divide, 1000, 20, 3, 9)
End Sub

Sub 計算3(Ope As Variant, ParamArray x())
    Dim tmp As Variant
    tmp = x
    Debug.Print foldl1(Ope, tmp)    ' ← ここ
End Sub

foldl1「たたみ込み関数」というものの1パリエーションで、リストの先頭の値を初期値として使って左から順に関数を適用していくものだ。

qiita.com

意味的にはこういう感じになる。
foldl1(p_plus, Array(1,2,3,4,5)) → 1 + 2 + 3 + 4 + 5
foldl1(p_Func, Array(1,2,3,4,5)) → Func(Func(Func(Func(1, 2), 3), 4,) 5)

なお、上のコードで tmp = x と、わざわざコピーを作っているのにはわけがあって、ParamArray で受け取った配列はこうするしかないのだ。この事情は

qiita.com

に書いた。

VBAHaskellで順列の数え上げ

n 個の元から k-個を選んで得られる順列の総数は下の式で計算できる。

\frac{n!}{(n-k)!}

VBAの適当な配列から、このすべての並びを列挙する関数 nPk をVBAHaskellで書いてみる。
(CallMeKohei さんのつぶやきからネタをパクりましたm(_ _ )m)

関数 nPk に任意の配列と取り出す要素数(省略可)を与えると、取りうる全ての並びからなる配列を返す。それぞれの要素それ自体もまた配列なので、返り値はジャグ配列ということになる。5つの文字からなる配列 Array("a", "b", "c", "d", "e") で結果を確認すると次のようになる。

m = nPk(Array("a", "b", "c", "d", "e"))    ' 全体の順列
?sizeof(m)
 120 
printM unzip(m,2)
  a  b  c  d  e
  a  b  c  e  d
 ~~省略~~~
  e  d  c  a  b
  e  d  c  b  a

m = nPk(Array("a", "b", "c", "d", "e"), 2)    ' 2個だけ取り出す順列
?sizeof(m)
 20 
printM unzip(m,2)
  a  b
  a  c
 ~~省略~~~
  e  c
  e  d

与えた配列に重複する要素があった場合には取り除かなければならないが、それは呼び出し側の責任とした。
VBAHaskellを使った nPk 関数のソースコードは以下の通り。

    ' 順列の数え上げ
' ar :  任意の1次元配列  ,  k_val :  取り出す個数(省略時は配列全体)
Function nPk(ByRef ar As Variant, Optional ByRef k_val As Variant)
    If IsMissing(k_val) Then k_val = sizeof(ar)
    If sizeof(ar) < k_val Then k_val = sizeof(ar)
    If 1 < k_val Then
        Dim ret As Variant: ret = VBA.Array()
        Dim flg As Variant: flg = iota(LBound(ar), UBound(ar))
        Dim i As Long
        ' 1要素を選択 → その要素と残りの要素に分割 → 残り要素に対して再帰 → つなげる
        For i = LBound(ar) To UBound(ar) Step 1
            ret = catV(ret, mapF(p_cons(ar(i)), nPk(filterR(ar, mapF(p_notEqual(i), flg)), k_val - 1)))
        Next i
        swapVariant nPk, ret
    ElseIf 1 = k_val Then
        nPk = ar
    Else
        nPk = VBA.Array()
    End If
End Function

このプログラムにはライブラリとしてVBAHaskellが必要で、ここで使用している主な関数を表にまとめた。 qiita.com

関数 機能 配置モジュール 備考
mapF 配列の各要素に関数を適用 Haskell_1_Core
notEqual 述語 a != b Haskell_2_stdFun 関数オブジェクト p_notEqual として使用
filterR 配列要素のフィルタリング Haskell_4_vector
cons 配列先頭への要素追加 Haskell_4_vector 関数オブジェクト p_cons として使用
catV 配列の結合 Haskell_4_vector
sizeof 配列長の取得 Haskell_4_vector
iota 連続する整数列の生成 Haskell_4_vector
swapVariant Variant変数のスワップ Haskell_0_declare

これに基づいていちばん複雑な式

ret = catV(ret, mapF(p_cons(ar(i)), nPk(filterR(ar, mapF(p_notEqual(i), flg)), k_val - 1)))

の説明をする。その前の段階で flg = iota(LBound(ar), UBound(ar))としているので flg には配列のインデックスが入っている。ar = Array("a", "b", "c", "d", "e") だったら flg = Array(0, 1, 2, 3, 4) だ。ループインデックスiがカウントアップされたときの ar(i)mapF(p_notEqual(i), flg)filterR(ar, mapF(p_notEqual(i), flg)) の値は以下のように推移する。

i ar(i) mapF(p_notEqual(i), flg) filterR(ar, mapF(p_notEqual(i), flg))
0 "a" Array(0, 1, 1, 1, 1) Array("b", "c", "d", "e")
1 "b" Array(1, 0, 1, 1, 1) Array("a", "c", "d", "e")
2 "c" Array(1, 1, 0, 1, 1) Array("a", "b", "d", "e")
3 "d" Array(1, 1, 1, 0, 1) Array("a", "b", "c", "e")
4 "e" Array(1, 1, 1, 1, 0) Array("a", "b", "c", "d")

だから再帰呼び出し nPk(filterR(ar, mapF(p_notEqual(i), flg)), k_val - 1) は先頭1要素を除いた残りの配列に対する再帰になっている。返ってくる値はジャグ配列となるがそれぞれの先頭に最初選んだ1要素を付け加える、というのをくりかえす。

f:id:mmYYmmdd:20150725185822p:plain

配列の先頭に要素を付け加える関数は cons なので、その第1引数を ar(i) に束縛して配列にマップしてやればいい。式 p_cons(ar(i)) がそれである。
一番外側にある catV 関数は配列の結合で、先頭 "a" のグループ、先頭 "b" のグループ・・・先頭 "e" のグループを結合してひとつの配列にしている。

ただしこの関数は実用性がない。手元のマシンで要素数7のとき(順列の数 = 5040)かかった時間は約1秒、要素数8のとき(順列の数 = 40320)かかった時間は約9秒だ。

最近Qiitaに書いた記事へのリンク

2015/7/25

最近Qiitaに書いた記事へのリンク

qiita.com

qiita.com

最近Qiitaに書いた記事へのリンク

最近Qiitaに書いた記事へのリンク

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

VBA 配列でつくったテーブルにSQLっぽいことしてみる(パクリ)

CallMeKoheiさんのブログ記事「VBA 配列でつくったテーブルにSQLっぽいことしてみる(その2)」をパクってみるという、はなし。


    arr = Array(Array(Array("品番", "a"), Array("品名", "apple"), Array("価格", 100#)), _
                Array(Array("品番", "b"), Array("品名", "banana"), Array("価格", 200#)), _
                Array(Array("品番", "c"), Array("品名", "cccc"), Array("価格", 300#)))

    arr2 = SQL(SELECT_("品番"), FROM(arr), WHERE(f))

上のようなジャグ配列 arr をテーブルと見立てて、SQLっぽくセレクトするというわけだ。
これをVBAHaslellでサンプルプログラムを書いてみる。
まず SQL(SELECT_(*), FROM(*), WHERE(*)) の部分だが、ひとつの関数に簡略化してしまう。
select_from_where(selector, from, where) というインタフェースだ。
・ selector はカラム選択関数で、何個目のカラム名を選ぶかのインデックスを返す
・ fromは対象テーブルそのもの
・ whereはレコード選択の関数

サンプルために、
・ カラム名を配列で渡すとその位置を返す関数select_col
・ 存在しないカラムだった場合に返ってくるNullを無視する関数remain_valid
をヘルパ関数として作った。

結果としてselect_from_whereの実装はこうなった。

Function select_from_where(ByRef selector As Variant, ByRef from_ As Variant, ByRef where_ As Variant) As Variant
    Dim where_result As Variant, select_index As Variant

    If count_if(where_, from_) = 0 Then
        select_from_where = VBA.Array()
    Else
        ' WHERE でレコードをフィルタリング
        where_result = filterR(from_, mapF(where_, from_))
        ' SELECTORで各レコードの対象カラムのインデックスを抽出
        select_index = mapF(selector, where_result)
        ' インデックスからNullを削除
        select_index = mapF(p_remain_valid, select_index)
        ' 各レコードから対象カラムを抽出
        select_from_where = zipWith(p_subV, where_result, select_index)
    End If
End Function

上に書いた arr を対象テーブルとして、セレクタを、p_select_col(Array("品名", "品番", "価格")) 、WHEREは無指定(恒真関数p_true)とすると以下のように出力される。

品名 apple
品番 a
価格 100
---------------
品名 banana
品番 b
価格 200
---------------
品名 cccc
品番 c
価格 300
---------------

SELECTは単にSELECT xxxx, yyyy だけでなく、SELECT xxxx + yyyy, zzzz のようなことができた方がいいが、今回は考えていない。

サンプルプログラムは、モジュールインポート済みWORDファイル(64bit版 , 32bit版 )にSub select_from_where_Testとして追加した。