以降で示すコードの全体は tailrec.dats から入手できます。
fun foo (x: int): int =
if x > 0 then bar(x) else baz(x)+1
ところで、たとえ baz(x)+1 を baz(x)+0 に変えたとしても、baz(x)+0 をなんとかして
baz(x) に翻訳することなしに、[baz] の呼び出しはまだ末尾呼出とは見なせません。
もし末尾呼出で呼び出す関数が自分自身を呼び出しているなら、この末尾呼出はしばしば末尾再帰呼出と呼ばれます。 関数本体内のあらゆる再帰呼出が末尾呼出であれば、その関数は末尾再帰です。 末尾再帰関数は反復関数と呼ばれることもあることに注意してください。 次の例では、 外側の [f91] 呼び出しは末尾再帰ですが、内側の呼び出しはそうではありません:
fun f91 (x: int): int =
if x >= 101 then x-10 else f91(f91(x+11))
定義から、[f91] は末尾再帰関数ではありません。
fun tally (n: int): int =
if n > 0 then n + tally (n-1) else 0
[tally] が末尾再帰でないことは明確です。
もしこの [tally] の実装をC言語に変換すると、本質的に次のようなコードが得られるはずです:
int tally (int n) { return (n > 0) ? n + tally (n-1) : 0 ; }C言語における [tally] のこの実装は少し独特で、一般的な実装は for ループに基づくものになるはずです:
int tally2 (int n) { int i ; int res = 0 ; for (i = n ; i > 0 ; i--) res += i ; return res ; }整数の加算の 結合法則 を使えば、[tally] と [tally2] が等価であることは指摘するべきでしょう。 もし加算を減算で置換すると、それは結合法則を満たしません。 そのため2つの実装はもはや等価ではなくなってしまいます。
C言語における [tally2] の上記の実装は、容易に次のような ATS コードに変換できます:
fun tally2 (n: int): int = let // fun loop (n: int, res: int): int = if n > 0 then loop (n-1, res+n) else res // in loop (n, 0) end // end of [tally2]内部関数 [loop] が末尾再帰であることに注意してください。 ATS (ATS/Postiats) コンパイラは、[loop] を上記で示した for ループ相当のC言語コードに本質的にコンパイルします。
一般的に、再帰関数を末尾再帰関数に変換は、その関数を実行中に生成されるスタックをエンコードする効率の良い方法を見つけられるかで決まります。 例えば、[tally] を 100 に対して呼び出したと仮定しましょう; この呼び出しは 99 に対する再帰呼出を生成し、そして 98 に対する再帰呼出、などと続きます; [tally] を 50 に対して呼び出した時、そのコールスタックは本質的に次のような評価コンテキストを表わします:
100 + (99 + (98 + (... + (51 + []) ...)))このとき記号 [] は tally(50) の返値で置き換えられます。 整数の加算は結合法則を満たすので、この評価コンテキストを表現するのに和 (100+99+98+...+51) を使うことができます。 これは [tally2] の本体内に内部関数 [loop] を実装する際のアイデアそのままです。
fun fib (n: int): int =
if n >= 2 then fib(n-1) + fib(n-2) else n
[fib] 本体内の2つの再帰呼出のどちらも末尾再帰でないことは明白です。
フィボナッチ数列を計算する別の関数 [fib2] を次に示します:
fun fib2 (n: int): int = let // fun loop (i: int, f0: int, f1: int): int = if i < n then loop (i+1, f1, f0+f1) else f0 // in loop (0, 0, 1) end // end of [fib2][fib2] の本体の内部関数 [loop] が末尾再帰であることは明らかです。 fib2(100) の評価を想像してみましょう。 この評価は loop(0, 0, 1) 呼び出しを生成し、その後 loop(1, 1, 1) 呼び出しを、さらにその後 loop(2, 1, 2) 呼び出しを、などと続きます。 もしこの呼び出し列を loop(i, f0, f1) とすれば、f0 は i 番目のフィボナッチ数、f1 は i+1 番目のフィボナッチ数になります。 これは、この列の最後の呼び出しの返値が 100 番目のフィボナッチ数 (i が 100 に到達した地点) になることを意味しています。 この引数は、[fib] と [fib2] は等価であることを納得させるに十分な情報でしょう。 つまり与えられた整数に適用した時、それらは同じ結果を返すのです。
[fib] を [fib2] に変換する背後にあるアイデアは、n >=2 の n 番目のフィボナッチ数を計算するために2つ前に計算されたフィボナッチ数のみを保持する必要がある、という単純な観察によっています。
fun {a:t@ype} list_append {m,n:nat} ( xs: list (a, m) , ys: list (a, n) ) : list (a, m+n) = ( case+ xs of | list_cons (x, xs) => list_cons (x, list_append (xs, ys)) | list_nil ((*void*)) => ys ) // end of [list_append][list_append] に割り当てられた型は、この関数が長さがそれぞれ m と n の2つのリスト xs と ys に適用されると、長さ m+n のリストを返すことを意味しています。 これが入力のリストを修正しない、いわゆる関数的なリストの連結であることに注意してください。 [list_append] 内の再帰呼出が末尾呼出でないので、この関数は末尾再帰ではないことは明確です。 もし [list_append] 呼び出しの最初の引数が長い (例: 100万要素を含むなど) 場合には、この呼び出し実行がコールスタックを溢れさせてクラッシュを引き起こす可能性が高くなります。
[list_append] を末尾再帰実装にするためには、リストの結合に関するいくつかの知識が必要です。 list_cons(x, xs) の評価は本質的に2つのステップです; はじめにリストノードを格納するために必要なメモリを確保し; それからこのノードを x と xs で初期化します。 ATS では、これら2つのステップは形式的に分離することができます。 この分離は、次のコードで示すような、リスト連結の適切な末尾再帰実装を得るための鍵になります:
fun {a:t@ype} list_append2 {m,n:nat} ( xs: list (a, m) , ys: list (a, n) ) : list (a, m+n) = let // fun loop{m:nat} ( xs: list (a, m) , ys: list (a, n) , res: &ptr? >> list (a, m+n) ) : void = ( case+ xs of | list_cons (x, xs1) => let val () = // allocate a list node with res := list_cons{a}{0}(x, _) // uninitialized tail val+list_cons (_, res1) = res // [res1] points to the tail // put into [res1] the concatenation val () = loop (xs1, ys, res1) // of [xs1] and [ys] in fold@(res) // folding translates into a no-op at run-time end // end of [list_cons] | list_nil ((*void*)) => res := ys ) // var res: ptr val () = loop (xs, ys, res) // in res end // end of [list_append2]xs, ys, res が与えられたとき、内部関数 [loop] は res の中に xs と ys の連結を配置します。 [loop] の本体では、式 fold@(res) が末尾呼出 [loop] よりも後ろにあるように見えます。 けれども、型検査の目的で fold@(res) は単に使われて型検査後では削除されてしまうため、この呼び出しは末尾呼出であると考えられます。 したがって [loop] は末尾再帰関数です。
ところで、この [loop] の実装に見られる再帰のスタイルはしばしば 末尾再帰を法とするアロケーション (tail-recursion modulo allocation) と呼ばれます。 これは一般に弱い型の言語 (例: LISP や C言語) 見られますが、(適切に型を付けることが非常に困難なため) 型付き言語ではあまり見られません。
fun isevn (n: int): bool =
if n > 0 then isodd (n-1) else true
and isodd (n: int): bool =
if n > 0 then isevn (n-1) else false
[isevn] 本体の [isodd] 呼び出しと [isadd] 本体の [isevn] 呼び出しは両方とも末尾呼出です。
これらはまた相互末尾呼出なので、これらは相互末尾再帰呼出と呼ばれます。
[isevn] と [isadd] のこの実装をコンパイルすると、 ATS コンパイラ (ATS/Postiats) は [isevn] と [isodd] を別々に扱います。 したがって、[isevn] ([isodd]) 本体内の [isodd] ([isevn]) 呼び出しをローカルジャンプに変換できません。 2つの関数を混合してコンパイルする必要があることをコンパイラに指示するために、キーワード [fun] を別のキーワード [fnx] で置き換えなければなりません:
fnx isevn (n: int): bool =
if n > 0 then isodd (n-1) else true
and isodd (n: int): bool =
if n > 0 then isevn (n-1) else false
上記のコードをコンパイルすると、ATS コンパイラは [isodd] 本体のコピーを [isevn] 本体内に埋め込みます。
これで、これら2つの関数の本体内の相互末尾再帰呼出はローカルジャンプに変換できるようになります。
また、キーワード [fnx] に続いて相互的に定義された関数列の最初の関数だけ、後のコードで使用できることに注意してください。
上記の場合、関数 [isevn] は後に使用することができますが、関数 [isodd] はそうではありません。