6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Visual BasicAdvent Calendar 2019

Day 24

VBAの関数型風プログラミング用のライブラリを書いた

Last updated at Posted at 2019-12-24

ライブラリを触ってみようという方へ

VBA-Functional という関数型プログラミング用のライブラリを github にあげたのですが、説明分をほとんど書けていません。なので興味を持ってくださる方は、modTestモジュール内の関数を直接触ってもらったほうが雰囲気がわかると思います。全体的にToy Programmingっぽいですが、modUtilにあるprintAryやprintTimeあたりが実用性が高いと思うので、試してもらえるとうれしいです。

このライブラリには関数プログラミングと配列の操作の機能がありますが、今日は関数プログラミング的な部分を書こうと思います。

ライブラリの関数の命名法

まず関数名は語尾で区分けをしているので表にしておきます。
関数オブジェクトというのは僕が勝手に作った言葉なので、後で説明します。

語尾 種類
~Ary  配列用関数
~A   関数型プログラミング用関数
~F   特に関数オブジェクトを引数とする関数
~Fs  特に関数オブジェクトの配列を引数とする関数

定番関数のmapAを中心とした説明

以下[ ]は配列、" "は文字列を意味します。

関数型風なプログラミングの実現には、ExcelのメソッドApplication.Runが活躍しています。これは、他のマクロを呼び出すメソッドで 
Application.Run("fn0",arg1,arg2,arg3)->fn0(arg1,arg2,arg3)
のようにマクロ名と30個までのパラメータを引数をとります。マクロは関数のようなものだから、関数を引数にとる高階関数とみなせます。
関数型プログラミングの定番であるmap,filter,foldLeft,reduceLeftもApplication.Runを利用して、mapA,filterA,foldA,reduceAという名前で定義しました。
addOneを数を一つ足す関数とすると、mapAは
mapA("addOne",[1,2,3])->[addOne(1),addOne(2),add(3)]->[2,3,4]
のように計算します。
mapは通常、一変数の関数と配列を引数にとりますが、、mapAは、さらにパラメータをとれるように拡張しています。また、演算子やVBAの組込みの関数は、マクロではないため、そのままではmapAに使うことができません。そこで

 Function calc_(num1, num2, symbol As String)
    Dim ret
    Select Case symbol
        Case "+": ret = num1 + num2
        Case "-": ret = num1 - num2
        Case "*": ret = num1 * num2
        Case "/": ret = num1 / num2
        Case "\": ret = num1 \ num2
        Case "%": ret = num1 Mod num2
        Case "^": ret = num1 ^ num2
        Case Else
    End Select
    calc_ = ret
    End Function

のように、通常では関数名に使えない記号をパラメータにとって計算できるcalc_というマクロを作りました。
これを使うと、
mapA("calc_",[1,2,3],1,"+")->[2,3,4]
といった計算がaddOneのようなその場しのぎの関数を作らなくてもいいので、いろいろ
気楽に試してもらえると思います。
filterA,foldA,reduceAも同様な拡張をしています。
また今のところfoldRiget,reduceRightには対応していません。
配列の関数にrevAryを用意しているので、組み合わせて使ってください。

部分適用と関数の合成の実現

既存の関数から新しい関数を作る部分適用や関数の合成も実現したいところです。
まず、先頭が関数名の配列を計算するevalA定義しました。
evalA(["fn0",arg1,arg2,arg3])->fn0(arg1,arg2,arg3)
配列の要素を順にApplication.Runに渡しているだけですが、関数名を先頭とする配列を操作することが、関数を操作することになるので、部分適用や関数の合成を考えやすくなります。

2)       [[2,1],["calc_",Null,Null,"/"]
3)       [[2]["calc_",1,Null,"/"]]
``` 
1式は通常の割り算、2式は1番目と2番目の引数を入れ替えたもの、3式は1番目の引数に1を代入して1変数関数にしたもの表しています。こういった配列を僕の勝手な用語で関数オブジェクトと呼んでいます。

また 2式 はArray関数を使って書くと
```Array(Array(2,1),Array("calc_",Null,Null,"/"))```
のように結構大変になるので
```mkF(2,1,"calc_",Null,Null,"/")```
のようにisnumeric関数を使って関数名の前にある数字を識別して関数オブジェクトを作る関数mkFも定義しました。
ここまで準備できれば、二つの数の組[2,4]を2式に関数オブジェクト前半の示す数字の位置に従って後半の未完成な式に代入し、その数列をevalAすれば計算できます。
実際にそれを行う関数をapplyFとして定義しました。
```applyF(Array(2,4),mkF(2,1,"calc_",Null,Null,"/"),true) -> 2```
(式の最後のtrueは配列をばらして2か所に代入するように指示するオプションです)


また同様に
```4)    
 [
          [[1],["calc_",Null,1,"+"]],
          [[1],["calc_",Null,2,"*"]]
          ]```
4式は、1を足す関数と2をかける関数を並べた配列で、この二つの関数を合成した関数を表しています。この計算は順次計算結果を次の関数オブジェクト代入することで得られるので、applyFsを定義することができます。
```applyFs(2,Array(mkF(1,"calc_",Null,1,"+"),mkF(1,"calc_",Null,2,"*")),false) -> 6```

こんな感じで原始的ですが、関数オブジェクトを使って部分適用や関数の合成を実現しています。また、filterA,mapA,foldA,reduceAはパラメータをとれるように拡張しているので、applyF,applyFsとともに部分適用関数、合成関数に対して使うことができます。

やっていることのわりに説明が長くなってしまいました。いろいろ遊んでもらえると嬉しいです。




6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?