(s)で動作する関数を作成する場合Stream
、再帰の概念は異なります。最初の単純な意味は、コンパイラレベルでは再帰的ではありません。テールが即座に評価されない場合、関数はすぐに返されますが、返されるストリームは再帰的です。
final def simpleRec[A](as: Stream[A]): Stream[B] =
if (a.isEmpty) Stream.empty
else someB(a.head) #:: simpleRec(a.tail)
上記の再帰の概念は問題を引き起こしません。2つ目は、コンパイラレベルで本当に末尾再帰です。
@tailrec
final def rec[A](as: Stream[A]): Stream[B] =
if (a.isEmpty) Stream.empty // A) degenerated
else if (someCond) rec(a.tail) // B) tail recursion
else someB(a.head) #:: rec(a.tail) // C) degenerated
ここでの問題C)
は、実際の呼び出しが実行されていない場合でも、ケースが非tailrec呼び出しとしてコンパイラーによって検出されることです。これは、ストリームテールをヘルパー関数に分解することで回避できます。
@tailrec
final def rec[A](as: Stream[A]): Stream[B] =
if (a.isEmpty) Stream.empty
else if (someCond) rec(a.tail) // B)
else someB(a.head) #:: recHelp(a.tail)
@tailrec
final def recHelp[A](as: Stream[A]): Stream[B] =
rec(as)
コンパイル中、このアプローチは最終的にメモリリークを引き起こします。末尾再帰rec
は最終的に関数から呼び出されるため、recHelp
関数のスタックフレームはスチームヘッドへの参照を保持し、呼び出しが戻るrecHelp
までストリームをガベージコレクションさせません。rec
再帰ステップ)への呼び出しの数に応じてB)
。
ヘルプレスの場合でも、コンパイラが@tailrecを許可した場合、レイジーストリームテールが実際にはストリームヘッドへの参照を保持する匿名オブジェクトを作成するため、メモリリークが存在する可能性があることに注意してください。