再帰は、関数型言語やKotlinで見られる便利なテクニックです。しかし、通常の再帰はスタックオーバーフロー(StackOverflowError)を引き起こすことがよくあります。これに対処するために、多くの言語で尾再帰最適化(tail call optimization)が導入されています。Kotlinでは、tailrecキーワードを使用して明示的に実装されています。
通常の再帰は、各呼び出しのたびに新しいスタックフレームを作成します。再帰の深さが大きいと、StackOverflowErrorを引き起こします。尾再帰の自動最適化は常に可能ではなく、JVMは他の言語(Scala、Erlangなど)のコンパイラのようにはサポートしていません。
Kotlinでは、関数にtailrecキーワードを付けることができます。関数が実際に尾再帰である場合("tail call" — 自分自身を呼び出すことが最終的な操作である場合)、コンパイラはこれをループに置き換え、スタックオーバーフローを回避します。
コードの例:
tailrec fun factorial(n: Int, acc: Int = 1): Int = if (n == 0) acc else factorial(n - 1, acc * n) println(factorial(5)) // 120
主な特徴:
tailrecがあるが、呼び出しが尾位置にない場合はどうなりますか?
コンパイラはエラーを出し、最適化を適用しません。
tailrec fun sum(n: Int, acc: Int = 0): Int { println(n) // これ以降は最適化できません return if (n == 0) acc else sum(n - 1, acc + n) } // コンパイルエラー: 呼び出しが尾位置にありません
open関数や抽象メソッドでtailrecを使用できますか?
いいえ、キーワードはfinal関数に対してのみ機能します。
open class Base { // エラー: // tailrec open fun test() {} }
通常の再帰とtailrecの間にパフォーマンスの違いはありますか?
はい、tailrecは関数をループに変換し、スタックコストを削減し、StackOverflowErrorを防ぎます。
開発者は、最適化が適用できるかどうかを考慮せずにすべての再帰関数にtailrecを付けます。呼び出しが必ずしも尾位置にあるわけではなく、プロジェクトはコンパイルされず、再帰が効果的に機能しません。
利点:
開発者はロジックを理解し、ツリー探索や階乗のような関数で尾再帰を適用します。tailrecは実際に適用可能な場所で使用され、スタックオーバーフローなしに効率的にコンパイルされたループが生まれます。
利点: