再帰処理


再帰とは

プログラミングでいう再帰とは、処理として自分自身を呼び出すことを指し、そのような呼び出し方を再帰呼び出しなどといいます。


for分を再帰呼び出しに書き換える

「ループ構文」で紹介したfor文で書いたループ処理を呼び出すコードを書いてみましょう。

Sub call_loop() 'ループ処理の呼び出し元
    Call loop_for(10) 'forを使ったループ処理を呼び出す
    Call loop_rec(10) '再帰を使ったループ処理を呼び出す
End Sub

Sub loop_for(maxnum) 'forを使ったループ処理 Dim i As Long 'カウンタ変数 For i = 1 To maxnum '1から引数の値までループ Cells(i, 1) = i '1列目にmaxnumの値を出力 Next End Sub

loop_forでは最大値(maxnum)を引数で受け取り、1列目に1から最大値までの値をそのまま出力しています。

だいたい同じ処理を再帰を使って表現すると以下のloop_recのようになります。

Sub loop_rec(maxnum) '再帰を使った処理
    If maxnum = 0 Then
        Exit Sub '終了条件を満たしたら終了
    Else
        Cells(maxnum, 2) = maxnum '2列目にmaxnumの値を出力
        Call loop_rec(maxnum - 1) '引数(maxnum)を-1して自分自身を呼び出す
    End If
End Sub

この処理の中では引数が0になる(終了条件が満たされる)まで、引数を-1したものを引数として自分自身を呼び出し続けながら引数の値を出力し、結果的には10個の数字を出力して引数が0で呼び出された時点で何もせずに処理を終了します。

ループ処理の実行結果

forを使って書いたloop_forでは一つのsubプロシージャ内だけでループしていたのに対し、loop_recが呼び出された際には終了条件に到達するまで何度もloop_recを呼び出しています。

loop_recが呼び出された後、最初に終了するのは最後に呼び出されたloop_recとなり、その後一つずつ呼び出された処理をさかのぼって終了していき、最後に終了するのが最初に呼ばれたloop_recになります。

上でだいたい同じ処理と書きましたが、loop_for処理では1から順に出力処理が行われるのに対して、loop_rec処理では10から順に出力処理が行われています。

もう一つの違いはカウンタ変数の有無です。

loop_forにはカウンタ変数が必要ですが、loop_recでは引数そのものを終了条件として判定に使用しているため、ループを制御するためのカウンタ変数を別に持つ必要がないのです。


再帰関数

先ほどはsubプロシージャを再帰で呼び出しましたが、次にfunctionプロシージャを呼び出してみます。

subが処理だとするとfunctionは関数であり、呼び出し元へ戻り値(関数の呼び出し元へ渡す値)を返すことができます。なおvbaでは関数の戻り値としてfunction自体に値をセットします。

Sub call_loop_func() '関数の呼び出し元
    Cells(11, 1) = func_loop_for(10) 'forを使った関数を呼び出す
    Cells(11, 2) = func_loop_rec(10) '再帰を使った処理を呼び出す
End Sub

Function func_loop_for(maxnum) 'forを使って合計を計算する関数 Dim i As Long 'カウンタ変数 Dim result As Long '計算結果 result = 0 '計算結果を初期化 For i = 1 To maxnum '1から引数の値までループ result = result + i '計算結果にiの値を加算 Next func_loop_for = result '計算結果を戻り値としてセット End Function
Function func_loop_rec(maxnum) '再帰を使って合計を計算する関数 If maxnum = 0 Then func_loop_rec = maxnum '現在の値(0)を戻り値としてセット Else func_loop_rec = maxnum + func_loop_rec(maxnum - 1) '現在の値と引数を-1した自分自身の和を戻り値としてセット End If End Function

func_loop_forでは、1から引数で受け取った最大値までの和を計算し、戻り値として返します。

ここではカウンタ変数の他に計算結果を格納するための変数が必要になっていることに注目してください。

一方func_loop_recは先ほどと同様、関数内に新たに宣言した変数がないため、すっきりした印象を受けます。

処理の流れとしては、受け取った引数と引数を-1して呼び出した自分自身の和(引数が0ではない場合)、又は0(引数が0の場合)を戻り値として返しています。

詳しく追うと、先ほどと同じく引数10で呼び出されたfunc_loop_recが引数を-1しながら再帰呼び出しを続け、引数が0で呼び出された場合にだけ引数の0自体を戻り値として返し、そこから順次(0)+1→1、(1)+2→3、(3)+3→6、(6)+4→10、(10)+5→15、(15)+6→21、(21)+7→28、(28)+8→36、(36)+9→45、(45)+10→55、という風に戻り値を返していき、最終的に1から10の和である55を得ています。

このように処理を順に積み上げていき、最後に積み上げた処理(一番上)から処理を行い、終わるとその下に積まれた処理を行って終わるとその下の処理を行って積み上げられた処理がなくなるまで処理を行う……という仕組みはスタックと呼ばれます。

スタックは最初に入れたものを最後に取り出すように振る舞うことから、先入れ後出し(後入れ先出し)LIFO(Last In, First Out)と呼ばれます。

再帰処理を使った場合はループ回数をカウンタ変数で制御する必要がありませんが、スタックに積んだ順番で処理が実行される順番と回数を制御しています。


それぞれのエラー

func_loop_forの場合、for文を使用しているため、引数が1~引数の範囲になければ処理は実行されません。

ただし、for文の内部でiを-1したような場合には無限ループが発生しエクセルが操作ができなくなるため注意が必要です。

一方、func_loop_recに負の引数を渡した場合や、そもそも終了条件を書き忘れた場合、膨大な回数の再帰呼び出しが行われる(スタックに処理が積まれる)ことになります。

スタックを積み重ねすぎるとスタックが不足してしまいますので、この場合も意図した処理を続けることができなくなります。

上のコードで「Call func_loop_rec(-1)」として再帰関数を呼び出した場合は以下のようになります。

スタック不足エラー

この場合は-1という意図しない引数を与えたことでエラーが発生しましたが、スタックの仕組み上意図した引数を与えた場合であってもスタック不足が発生する場合はあるということに注意が必要です。


ループと再帰の使い分け

再帰を使う意義についてはぴんと来ないかもしれませんが、こちら(再帰呼び出し、よく使う?使わない?)も参考にしてみてください。

VBAで再帰を使わなければいけないケースは少ないかもしれませんが、再帰の考え方を知っておけば、繰り返し処理を行う場合にループと再帰という2つの選択肢を考えることができます。

将来的にカウンタ変数を利用したループそのものを排除しようとする設計思想のプログラミング言語を使わなければならない状況に出会うことも考えられますし、問題に対処するにあたり複数のアプローチを考えられることはプログラマーが作業を進めるうえで不利に働くことはないはずです。