読者です 読者をやめる 読者になる 読者になる

紹介記事の補足

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

に書いた。