階乗を末尾呼び出しに変換する例として、次のことがよく見られます。
int factorial(int n, int acc=1) {
if (n <= 1) return acc;
else return factorial(n-1, n*acc);
}
乗算は結合法則と可換法則の両方である必要があるため、これは完全には正しくありません。(乗算は結合法則と可換法則ですが、上記はこれらの制約を満たさない他の演算のモデルとしては機能しません。)より良い解決策は次のとおりです。
int factorial(int n, int k=1, int acc=1) {
if (n == 0) return acc;
else return factorial(n-1, k+1, acc*k);
}
これは、フィボナッチ変換のモデルとしても機能します。
int fibonacci(int n, int a=1, int b=0) {
if (n == 0) return a;
else return fibonacci(n-1, a+b, a);
}
これらは、呼び出しスタックで保留中の継続をキューに入れるのではなく、最初からシーケンスを計算することに注意してください。したがって、それらは、再帰的なソリューションというよりも、構造的に反復的なソリューションに似ています。ただし、反復プログラムとは異なり、変数を変更することはありません。すべてのバインディングは一定です。これは面白くて便利なプロパティです。これらの単純なケースでは、大きな違いはありませんが、再割り当てなしでコードを記述すると、一部のコンパイラの最適化が容易になります。
とにかく、最後のものは再帰関数のモデルを提供します。フィボナッチ数列のように、関連する過去の値を保持する必要がありますが、2つではなく3つが必要です。
int mouse(int n, int a=1, int b=1, int c=1) {
if (n <=2 ) return a;
else return mouse(n-1, a*c+1, a, b);
}
補遺
コメントでは、2つの質問が提起されました。ここでそれら(およびもう1つ)に答えようとします。
まず、(関数呼び出しの概念を持たない基盤となるマシンアーキテクチャを考慮して)、関数呼び出しをgoto(おそらく非制限の中間ストレージを使用)と言い換えることができることを明確にする必要があります。さらに、どのgotoも末尾呼び出しとして表現できます。したがって、再帰を末尾再帰として書き直すことは可能です(ただし、必ずしもきれいである必要はありません)。
通常のメカニズムは「継続渡しスタイル」です。これは、関数を呼び出すたびに、代わりに現在の関数の残りを新しい関数(「継続」)としてパッケージ化し、それを渡すという派手な言い方です。呼び出された関数への継続。その後、すべての関数は引数として継続を受け取るため、受け取った継続の呼び出しで作成した継続を終了する必要があります。
おそらく頭を回転させるのに十分なので、別の言い方をします。引数と戻り位置をスタックにプッシュして関数(後で戻る)を呼び出す代わりに、引数と継続位置をスタックにプッシュします。そして、関数に移動します。この関数は、後で継続位置に移動します。つまり、スタックを明示的なパラメータにするだけで、戻る必要はありません。このスタイルのプログラミングは、イベント駆動型コード(Python Twistedを参照)では一般的であり、書き込み(および読み取り)するのは非常に困難です。したがって、コンパイラーにこの変換を実行させることを強くお勧めします。
@xxmouseは、私が漸化式を帽子から引き抜くことを提案し、それがどのように導き出されたかを尋ねました。これは単に元の再帰ですが、単一のタプルの変換として再定式化されています。
fn = fn-1*fn-3 + 1
=>
Fn = <Fn-11*Fn-13+1, Fn-11, Fn-12>
それがもっと明確かどうかはわかりませんが、私ができる最善のことです。少し単純なケースについては、フィボナッチの例を見てください。
@j_random_hackerは、この変換の制限を尋ねます。k
これは、各要素が前の要素の式で表すことができる再帰シーケンスで機能します。ここk
で、は定数です。末尾呼び出しの再帰を生成する方法は他にもあります。例えば:
// For didactic purposes only
bool is_odd(int n) { return n%2 == 1; }
int power(int x, int n, int acc=1) {
if (n == 0) return acc;
else if (is_odd(n)) return power(x, n-1, acc*x);
else return power(x*x, n/2, acc);
}
上記は、通常の非末尾呼び出し再帰と同じではありません。これは、異なる(ただし同等で同じ長さの)乗算シーケンスを実行します。
int squared(n) { return n * n; }
int power(int x, int n) {
if (n == 0) return 1;
else if (is_odd(n)) return x * power(x, n-1));
else return squared(power(x, n/2));
}
次のテストを行ってくれたAlexeyFrunzeに感謝します。出力(ideone):
mouse(0) = 1
mouse(1) = 1
mouse(2) = 1
mouse(3) = 2
mouse(4) = 3
mouse(5) = 4
mouse(6) = 9
mouse(7) = 28
mouse(8) = 113
mouse(9) = 1018