■
t-homさんが直近の記事でByRef As Variantで受けたときにアドレスが変わる事象について仮説を書いている。
型一致参照である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 とは?
(自分用メモ)
VBAでByRef 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パリエーションで、リストの先頭の値を初期値として使って左から順に関数を適用していくものだ。
意味的にはこういう感じになる。
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 で受け取った配列はこうするしかないのだ。この事情は
に書いた。
VBAHaskellで順列の数え上げ
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要素を付け加える、というのをくりかえす。
配列の先頭に要素を付け加える関数は cons
なので、その第1引数を ar(i)
に束縛して配列にマップしてやればいい。式 p_cons(ar(i))
がそれである。
一番外側にある catV
関数は配列の結合で、先頭 "a" のグループ、先頭 "b" のグループ・・・先頭 "e" のグループを結合してひとつの配列にしている。
ただしこの関数は実用性がない。手元のマシンで要素数7のとき(順列の数 = 5040)かかった時間は約1秒、要素数8のとき(順列の数 = 40320)かかった時間は約9秒だ。
VBA 配列でつくったテーブルにSQLっぽいことしてみる(パクリ)
CallMeKoheiさんのブログ記事「VBA 配列でつくったテーブルにSQLっぽいことしてみる(その2)」をパクってみるという、はなし。
上のようなジャグ配列 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
として追加した。