カレンダーのコードを書いていてGroovyの末尾再帰の最適化が気になって調べてみた。
参考にしたページ > http://npnl.hatenablog.jp/entry/20110630/1309447996
サンプルとして、1から10の整数が格納されているリストから、それぞれ2個ずつさらにリストに小分けにして返す処理。
def split = 2
// 普通の再帰
def clj1 = { List list ->
if(!list) {return []}
else [list.take(split)] + call(list.drop(split))
}
// trampolineを使って末尾再帰最適化!
def clj2 = { List list, List result = [] ->
if(!list) {return result}
else trampoline(list.drop(split), result + [list.take(split)])
}.trampoline()
def list = (1..10).toList()
assert clj1(list) == [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
assert clj2(list) == [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
assert clj1.call(list) == [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
assert clj2.call(list) == [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
// もし以下のような大きい値を使った場合、通常の再帰処理だとStackOverFlowとなる。
// list = (1..10000).toList()
// clj1(list)
Scalaで末尾再帰を勉強してたときは、再帰が進むにつれて、処理される値がドンドン減っていって、最終的に処理結果が段々と返ってくる、言うなれば小さくなっていってまた大きくなりながら戻ってくる、っていうイメージだった。
でもGroovyのtrampoline場合、処理結果を毎回自分自身に渡さないといけないから、逆に処理される値がドンドン増えていって、最後に一気にズドーン!と値が帰ってくるイメージ。
どちらも末尾再帰の最適化は実行されるはずなんだけど受けるイメージが真逆・・・
Closure#trampoline()には引数を取るtrampolineと取らないtrampolineがあって、それぞれ役割が違うみたい。
引数を取るtrampolineの方が実際に最適化されるロジック部分で、引数を取らないtrampolineが、このClosureは末尾再帰の最適化をやりますよーというイメージ。だと思う・・・
引数なしのtrampolineの戻り値はClosureのインスタンスなので、普通に呼び出してあげればOK。