tail.の復活

昨日のエントリで、いらない子扱いされてしまって、枕に涙を塗らしていた*1tail.プレフィクス。F♯のコンパイラオプションでは明示しなければ使ったもらえず、以下のようなコードの場合でも、スタックフレームを1個除去できるかどうかだけで、ほぼあんたの居場所ね−からと言われかねない状況だった。

let tailCallSum n=
    let rec sum accum n=if n=0L then accum else sum (n+accum) (n-1L)
    sum 0L n;;

printfn "%d" (tailCallSum 100);;

この場合、再帰するのは内側の”sum”であり、これを、C♯風に無理矢理かくってーと以下のような感じ*2

using System;

static class Program
{

	static void Main()
	{
		//やってみた感じ、tail.が適用出来そうな場合でも、
		//こっちの呼び出しには、tail.は付かない模様。
		Console.WriteLine(tailCallSum(100L));
	}

	static long tailCallSum(long n)
	{
		//この呼び出しが、tail.になるかどうかだけ。
		return sum(0, n);
	}

	static long sum(long accum, long n)
	{
		//引数を使っちまってますが、その辺はご愛敬。
		//モノの見事にループ展開されている。
		while (n != 0L)
		{
			accum += n--;
		}
		return accum;
	}
}

これからわかるように、外側の"tailCallSum"が内側の"sum"を呼び出すときに、tail.が付くかどうかと言うだけで、sumの内部は、昨日と同様なループ展開がされていると。

こうなってしまうと、tail.のありがたみはほぼほぼ無いと言えなくも無い。

復活の狼煙

このまま、本当にいらない子になってしまうのか?と言うと、さに非ず。相互再帰の場合、生きてくるコトがある。

相当に作為的だけど、この関数を相互再帰で書き直してみると以下の通り。

let rec mutalSumA accum n=if n=0L then accum else mutalSumB (n+accum) (n-1L)
and     mutalSumB accum n=if n=0L then accum else mutalSumA (n+accum) (n-1L);;

printfn "%d" mutalSumA 0 100L;;
printfn "%d" mutalSumB 0 100L;;

中身は、ご覧の通り全く同じコトをしてるmutalSumAとmutalSumBを相互的に呼び出している。で、このコンパイル結果を先ほどの通り、意訳してC#で書くと以下の通り。

using System;

static class Program
{

	static void Main()
	{
		//やってみた感じ、tail.が適用出来そうな場合でも、
		//こっちの呼び出しには、tail.は付かない模様。
		Console.WriteLine(mutalSumA(0L, 100L));
		Console.WriteLine(mutalSumB(0L, 100L));
	}

	static long mutalSumA(long accum,long n)
	{
		if (n == 0) return accum;
		return mutalSumB(accum + n, --n);
	}

	static long mutalSumB(long accum, long n)
	{
		if (n == 0) return accum;
		return mutalSumA(accum + n, --n);

	}


}

先の例とは異なり、ループ展開されていない。

と言うことは・・・*3

当然、スタックオーバーフローを起こす程度にでかい数食わせてみたくなる。

あたしの環境*4だと、概ね100,000食わせると、落ちてくれることがわかってるので,コンパイラオプションの“tail呼び出しの生成”の有無で実行結果に差が出るか、調べてみた。

let rec mutalSumA accum n=if n=0L then accum else mutalSumB (n+accum) (n-1L)
and     mutalSumB accum n=if n=0L then accum else mutalSumA (n+accum) (n-1L);;

printfn "%d" mutalSumA 0 100000L;;
printfn "%d" mutalSumB 0 100000L;;

その結果は、予想通り、“tail呼び出しの生成”がチェックされている場合は、何の問題も無く、実行された。けどチェックを外した場合、StackOverFlowExpceptionが発生してこけた。

tail.の使いどころ(と言うか、tail呼び出しの生成をどのようなときチェックすべきか)

今回は、相当作為的じゃあるけど、

と言うシナリオでは、tail.は必須となり、そこに居場所があるってコトがわかった。ただ、実際例として、そんなシナリオが存在し得るのか?と言われると、あくまで今回検証目的で確認しただけなので、何とも言えないけど、コンパイラオプションにこんな項目がある以上、あり得るのかもしれないかなぁと言うのが今のところの私感。

と同時に、デフォルトでtail.を生成させない当たり、やっぱり看過できない程度のパフォーマンスインパクトを持っているのかな?とも考えた。

*1:かどうかはわからないw

*2:ILのデコンパイル結果じゃなくて、若干意訳してますご了承の程

*3:当然、Lには意味があるw

*4:Win7/Pro 64bit X86デバッグビルド

*5:StackOverFlowExpceptionが発生するスケールで。