VBAの ParamArray
の仕組みがどうもよくわからない。
MSDNには「プロシージャが、省略可能なパラメーターとして、指定されたデータ型の要素の配列を受け取ることを示します。 ParamArray は、パラメーター リストの最後のパラメーターにのみ使用できます。」とある。
要するに可変長引数であり、受け取ったプロシージャ側からその引数はひとつの配列のように見える。配列なので LBound
や UBound
の値も取れるし、For ループで個々の要素にアクセスすることもできる。
しかしこれは 他のプロシージャに ByRef
で渡すことができない、という制約があって、下のコードだと bar1
に渡そうとするとエラーになる(「コンパイルエラー:ParamArrayの使い方が適切ではありません。」というダイアログメッセージが出る)。
ByVal
の bar2
に渡すことは普通にできる。しかし bar1
に渡すにはローカルでコピーをとって渡すしかないようだ。
Function foo(ParamArray v() As Variant)
bar1 v ' これはNG
bar2 v ' これはOK
Dim tmp As Variant : tmp = v
bar1 tmp ' ← コピーならOK
...
Sub bar1(ByRef x As Variant) ' ByRef渡し
Sub bar2(ByVal x As Variant) ' ByVal渡し
VBAHaskellで ParamArray
を使っている箇所は少ないが、そのひとつに catVs
1 がある。任意個の1次元配列をつなげて配列を返す関数で、以前は下のような実装になっていた。ふたつの配列をつなげる catV
関数を foldl1
する、という素朴なやり方だ。
'ベクトルを結合(可変長引数)
Function catVs(ParamArray vectors() As Variant) As Variant
catVs = foldl1(p_catV, VBA.Array(vectors)(0))
End Function
VBA.Array(vectors)(0)
の部分が分かりにくいと思うが、少し書き直すと結局こういうことをやっているのと同じで、引数vectorsのコピーを作っている。
Function catVs(ParamArray vectors() As Variant) As Variant
Dim tmp As Variant : tmp = vectors ' コピー
catVs = foldl1(p_catV, tmp)
End Function
これはfoldl1
関数に引数 vectors
をそのまま渡すことができないせいだが、もちろん効率は悪く、個々の配列が大きいときは無視できないコストになる。なにより無意味なコピーは無くしたいという気持ちが強い。
これには swapVariant 2 を使って変数をスワップするのが有効な手段で、最近以下のように書き換えた。
'ベクトルを結合(可変長引数)
Function catVs(ParamArray vectors() As Variant) As Variant
Dim i As Long
Dim tmp As Variant
If LBound(vectors) <= UBound(vectors) Then
ReDim tmp(LBound(vectors) To UBound(vectors))
For i = LBound(vectors) To UBound(vectors)
swapVariant vectors(i), tmp(i) ' スワップする
Next i
catVs = foldl1(p_catV, tmp) ' 実際の処理
For i = LBound(vectors) To UBound(vectors)
swapVariant vectors(i), tmp(i) ' 元に戻す
Next i
End If
End Function
もとの引数である vectors
と同じ長さの配列 tmp
を作り、個々の要素(それ自体が配列)をスワップして入れている。要素が巨大な配列だとしてもそのコストは無視できる程度だ。こうして作った tmp
は通常の配列と同様に扱うことができるので、foldl1
で処理すれば目的が達成できる。最後にもう一度スワップして元に戻してやる。
VBAHaskellで目標にしているループの排除が犠牲になっているが、これは課題としたい。
VBAHaskellの紹介 その18(reverseをfoldで実装)
VBAHaskellの紹介 その1(最初はmapF)
ソース https://github.com/mYmd/VBA