VBAHaskellでの関数定義

  • 1
    いいね
  • 0
    コメント

はじめに

これは、Visual Basic Advent Calendar 2016の16日目の記事です。

これまで2回Advent Calendar 2016に紹介記事を書かせて頂きましたが、2回ともデモっぽい内容だったので、今回はちゃんとVBAHaskellにおける関数の作り方について書きたいと思います。

関数にできるもの

VBAHaskellでは、関数本体とそれとペアになる関数オブジェクトを定義して、ゆるふわな関数型っぽいプログラムを書くことができます。しかしその使い方ができる関数のパターンは一つだけです。

' VBAHaskellで基本的に定義できる たったひとつの関数パターン
Function f(ByRef a As Variant, ByRef b As Variant) As Variant
' ただし、第二引数は Optional にすることはできる
Function g(ByRef a As Variant, Optional ByRef b As Variant) As Variant

これだけですが、以下の3つの方法で用途を広げられます。

配列関数の引数にする

plusと、そのペアであるp_plusは単に足し算するだけの関数ですが、mapFzipWithfold/scan系の関数の引数にすれば大量のデータを処理できます。

' ふたつの配列の各要素を加える
printM zipWith(p_plus, iota(0, 9), iota(30000, 30009))
  30000  30002  30004  30006  30008  30010  30012  30014  30016  30018

zipWithは二つの配列から新たに配列を作成する関数ですが、p_plusがその第1引数になっています。API内でループして0+30000, 1+30001, ..., 9+30009という配列を作ってます。引き算にしたければp_minusを、掛け算にしたければp_multを指定します。

' 自然数を順次足していく
printM scanl1(p_plus, iota(0, 100))
  0  1  3  6  10  15  21  28  36  45  55  66  78  91  105  120  136  153  171  190  210  231  253  276  300  325  351  378  406  435  465  496  528  561  595  630  666  703  741  780  820  861  903  946  990  1035  1081  1128  1176  1225  1275  1326  1378  1431  1485  1540  1596  1653  1711  1770  1830  1891  1953  2016  2080  2145  2211  2278  2346  2415  2485  2556  2628  2701  2775  2850  2926  3003  3081  3160  3240  3321  3403  3486  3570  3655  3741  3828  3916  4005  4095  4186  4278  4371  4465  4560  4656  4753  4851  4950  5050

同じくp_plusを使ってますが、端からどんどん処理して履歴を出すscanl1に使うと上のようになります。

関数を合成する

lessp_lessは大小関係 "<" 表しますが、文字列の長さを求めるp_lenと組み合わせることによって、配列を「文字列の長さ順」にソートすることができます。

' String の配列
s = Array("Zbkn", "Afkbpirthrit", "123", "Kdgndi")
' 文字列の長さ順にしたときの順位 ord を出力する
ord = sortIndex_pred(s, p_less(p_len, p_len))
printM ord
  2  0  3  1
' Ord 順に並べる
printM  subV(s, ord)
  123  Zbkn  Kdgndi  Afkbpirthrit

上の例では、p_less(p_len, p_len) がソートに使う比較関数となり、デフォルト引数が隠れているので実際にはこんな構造です。
  p_less(p_len( _ :sunny: _ ), p_len( _ :stuck_out_tongue: _ ))
これにふたつの文字列s1s2を代入すると Len(s1) < Len(s2) となって、文字列長の比較になります。個々の関数は単機能でも合成すれば複雑なものを作れるので、目的ごとにいちいち Function プロシージャを書いて関数を定義せずにすみます。たとえば文字列長で降順ソートしたければp_greaterを使えばいいです。

' String の配列
s = Array("Zbkn", "Afkbpirthrit", "123", "Kdgndi")
' 文字列の長さ順(降順)にしたときの順位 ord を出力する
ord = sortIndex_pred(s, p_greater(p_len, p_len))
printM ord
  1  3  0  2
' Ord 順に並べる
printM  subV(s, ord)
  Afkbpirthrit  Kdgndi  Zbkn  123

引数、戻り値を配列にする

自分自身が配列を引数にとることで、多数の要素を相手にした処理をすることができます。戻り値を配列にするのも同様です。
たとえばfilterRという関数は、第1引数に対象配列、第2引数に<0/1>フラグからなる配列を指定することによって、配列をフィルタリングをします。

'ベクトル・配列の(行の)フィルタリング
'Flgは 0/1
Public Function filterR(ByRef data As Variant, ByRef flg As Variant) As Variant

下は整数列から3の倍数だけ抽出する単純な例ですが、フラグを作るのに関数合成を使っています。

a = iota(0, 30)               ' 0から30までの整数列
f = p_equal(p_mod(, 3), 0)    ' (x Mod 3) = 0 を表す関数
printM filterR(a, mapF(f, a))
  0  3  6  9  12  15  18  21  24  27  30

関数オブジェクトの作り方

Optional引数のないいちばん身近な例は足し算です。
この関数は、Haskell_2_stdFunモジュール(Github、もしくはここ)にあります。

'加算
Function plus(ByRef a As Variant, ByRef b As Variant) As Variant
    plus = a + b
End Function

    Function p_plus(Optional ByRef firstParam As Variant, Optional ByRef secondParam As Variant) As Variant
        p_plus = make_funPointer(AddressOf plus, firstParam, secondParam)
    End Function

Function plus は最初に述べたパターンを守って普通に定義したVBA関数です。
p_plusの方はplusのペアであることを明確にするためにp_plusという名前にしていますが、AddressOfの引数がplusになっていることが本質です。
自分で新しく関数とその関数オブジェクトを作成するときは、これをコピペして関数名だけ変えればOKです。make_funPointerがすべての仕事をやっています。

第二引数がOptionalな場合は、それが全くのダミーであるかそうでないかでパターンが分かれます。
次の指数関数 expN は組み込みのexpそのまんまですが、第二引数は名前の通りダミーで、あってもなくても無影響です。IsMissingの判定もしていません。この場合は、最初のパターンと同じで、make_funPointerで関数オブジェクトを作ります。

'指数関数
Function expN(ByRef a As Variant, ByRef dummy As Variant) As Variant
    expN = Exp(a)
End Function
    Function p_exp(Optional ByRef firstParam As Variant, Optional ByRef secondParam As Variant) As Variant
        p_exp = make_funPointer(AddressOf expN, firstParam, secondParam)
    End Function

対数関数の場合は第二引数がなければ自然対数、あればその数を底にするため、IsMissingで判定をしています。こういう関数に対してはmake_funPointerの代わりにmake_funPointer_with_2nd_Defaultを使います。

'対数関数
Function logN(ByRef a As Variant, Optional ByRef base As Variant) As Variant
    If IsMissing(base) Then
        logN = Log(a)
    Else
        logN = Log(a) / Log(base)
    End If
End Function
    Function p_log(Optional ByRef firstParam As Variant, Optional ByRef secondParam As Variant) As Variant
        p_log = make_funPointer_with_2nd_Default(AddressOf logN, firstParam, secondParam)
    End Function

make_funPointermake_funPointer_with_2nd_DefaultがやっていることはAPIとの絡みもあるので複雑ですが、どちらを使うかの基準は上に述べた通り単純なので、p_plusp_logのどちらかをコピペするという方針で十分です。


リンク

Qiita VBAHaskellの紹介 その1 (最初はmapF)
ソースコード(Github)
VBAHaskellの関数リファレンス
dllバイナリ:
https://github.com/mYmd/VBA/blob/master/bin/mapM (32bit-Office用)
https://github.com/mYmd/VBA/blob/master/bin/mapM64 (64bit-Officey用)
VBAコード添付済みExcelブック
VBAHaskellほぼ全部入り.xlsm

この投稿は Visual Basic Advent Calendar 201616日目の記事です。