t-hom’s diary

主にVBAネタを扱っているブログです。

VBAで関数型プログラマーの思考法を学ぶ

これは私に関数型言語の素晴らしさを教えてくれた本である。

プログラミングGauche

プログラミングGauche

プログラミングGauche(ゴーシュ)。

一度売ってしまったのだけど、どうしてもその本のコラムLisp脳」の謎に迫る-Schemeプログラマの発想がもう一度読みたくなって先日ついに買い戻してしまった。

そのコラムは手続き型の思考と関数型の思考の違いについて説明したもので、初めて読んだときはあまりに考え方が違うので衝撃を受けた。

これはGaucheという言語の本なので、コラムのコードも当然Gaucheで書かれているのだが、今回はこのコラムの内容をVBAのコードに置き換えて紹介ようと思う。
厳密にいうとGaucheは処理系の名称なので、言語名はSchemeだけど、どういうことか説明しだすとややこしいので割愛。

さて、その前に関数型言語について。

関数型言語とは

関数型言語というのは、ざっくりいうと関数を値として扱うことができる言語である。値であるから、変数に入れたり、ほかの関数の引数にしたり、戻り値にしたりすることができる。

関数というとExcel関数をイメージしてしまうかもしれないけど、ここではひとまずVBAのプロシージャに相当するものと考えてほしい。
つまり、プロシージャを変数に入れたり、他のプロシージャに引き渡したり、あるプロシージャの戻り値としてプロシージャを返したりできるのだ。

ちなみにVBA関数型言語ではないので、こんなことはできない。

コラムの内容

このコラムではコードの題材はとしてFizzBuzzを扱っている。そこでまず、FizzBuzzのルールを説明する。

FizzBuzzのルール

  • 1から100まで順に出力せよ
  • ただし、その数が3で割り切れる場合は数字の代わりにFizzと出力する
  • また、その数が5で割り切れる場合は数字の代わりにBuzzと出力する
  • さらに、その数が3と5と両方で割り切れる場合は数字の代わりにFizzBuzzと出力する

手続き型の思考

手続き型の思考では、与えられた課題をどのように実現するか、つまりHowを考え、その通りにプログラムを作成する。

  1. まずForループで変数 i を1~100まで順番に増やす。(How)
  2. ループの中で、3で割り切れるか、5で割り切れるか、3と5で割り切れるかをチェックし、それぞれ対応する結果を出力する。

図で書くと、このようなフローチャートのイメージになる。
f:id:t-hom:20170905113229p:plain

関数型言語を知らない一般的なプログラマーは、概ねこのように考えるだろう。

関数型の思考

関数型の思考では、与えられた課題をクリアするのに何が必要か、つまりWhatを中心に考える。

  1. とりあえず1から100まで入ったリストを用意する。
  2. 次に数値を与えると条件にしたがってFizz、Buzz、FizzBuzz、あるいは数値そのままを返す関数を用意する。
  3. リストに関数をマッピングさせて、結果リストを得る。
  4. 結果リストを出力する。

VBAで関数型の思考法をコードにしてみる

先ほどの関数型の考え方を図にすると、このようなイメージになる。
f:id:t-hom:20170904223107p:plain

左上の1~100と書かれているものは、1~100が格納されたリストである。今回はVBAプログラマ向けの説明なので、1~100が配列に順番に格納されているイメージで良いかと思う。関数型言語にはこのリストを瞬時に作るIota(イオタ)関数というものがある。

VBAに同等のものは存在しないので、自分で作ってみた。

Function Iota(n)
    Dim ret: ReDim ret(1 To n)
    Dim i
    For i = 1 To n
        ret(i) = i
    Next
    Iota = ret
End Function

次にFizzBuzz関数と書かれているものは、数値をひとつだけ受け取って、結果(Fizz、Buzz、FizzBuzz、元の数値のいずれか)を返す関数だ。

VBAではこのようになる。これは関数型言語でも自分でコーディングする部分だ。

Function FizzBuzz(n)
    Dim ret
    Select Case 0
    Case n Mod 15: ret = "FizzBuzz"
    Case n Mod 5: ret = "Buzz"
    Case n Mod 3: ret = "Fizz"
    Case Else: ret = CStr(n)
    End Select
    FizzBuzz = ret
End Function

さて、1つだけ数値を受け取るFizzBuzz関数に、複数の数値が集合した配列をそのまま渡すことはできない。
ここで登場するのがMapという関数。関数型言語には大抵、Map関数があり、リストと関数を引数として渡すと、リストのそれぞれの要素に関数を適用して、戻り値として新しいリストを返してくれる。

※ちなみにMapは一般的には地図と訳すけど、ここでは「対応付ける」という意味。

VBAにはMap関数がないので作ることになるが、VBA関数型言語ではないため関数を引数として渡すことができない。
そこで関数名で実行できるApplication.Runを利用した擬似Map関数を作ってみた。これで関数名を文字列として渡すことができる。

Function Map(function_name As String, list)
    Dim ret: ReDim ret(LBound(list) To UBound(list))
    Dim i
    For i = LBound(list) To UBound(list)
        ret(i) = Application.Run(function_name, list(i))
    Next
    Map = ret
End Function

これで結果配列を得られるようになったので、あとは出力だけ。
関数型言語にはリストをそのまま出力する関数が備わっているが、VBAには無いので配列をすべて出力するプロシージャを新たに作る。

Sub PrintList(list)
    Dim i
    For i = LBound(list) To UBound(list)
        Debug.Print list(i)
    Next
    Debug.Print
End Sub

そしてすべてをつなぐメインコードはこうなる。

Sub Main()
    Call PrintList(Map("FizzBuzz", Iota(100)))
End Sub

Iotaで作られた配列にFizzBuzz関数がマップされ、その結果をプリントするというコードになっている。

今回はVBAなので、Iota関数、Map関数、PrintListプロシージャでそれぞれループを使用しているが、関数型言語ではこれらの関数はあらかじめ用意されているので実質ループは必要ない。
材料である1~100のリストはIotaでポンと作れるし、そこに自作の関数をMapさせてはい出来上がりという世界だ。関数型言語なら、実際のコーディングはFizzBuzz関数と、メインコードのみで済む。

まとめ

Schemeを学ぶとプログラミングの筋がよくなるといわれている。そこで今回は私が特に感銘を受けたプログラミングGaucheのコラムをVBAコードを使って紹介した。
自分なりの言葉で説明したので受ける印象はまたちょっと違うかもしれないけれど、この記事をきっかけに関数型言語にも興味を持っていただけたらと思う。
手続き型プログラマーにとっては、非常に取っつきにくい言語であるが、だからこそ学びも多い。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。