Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
131: Tailrec Functions
You've likely encountered recursion before—it's a clean, elegant way to solve problems that have a naturally repetitive, nested structure. But there is a hidden danger in the way most of us first learn to write recursive functions. If your recursion goes too deep, you'll hit a StackOverflowError, and your application will crash. It's a rite of passage for every developer, but in a production environment, it's a bug we can't afford.
The Stack Overflow Trap
Let's look at a common way to calculate the sum of all numbers from 1 up to n. I'll write this the "naive" way first:
fun sumRange(n: Int): Int {
if (n <= 0) return 0
return n + sumRange(n - 1)
}
At first glance, this is perfect. It's readable and mathematically sound. But here is the problem: the computer cannot finish the addition (n + ...) until the recursive call to sumRange(n - 1) returns a value. This means the runtime has to "remember" every single call on a call stack. If you pass in 10, 100, or even 1,000, it works fine. But try passing in 100,000, and the stack will simply run out of space. You've asked the CPU to remember 100,000 pending additions, and it just can't do it.
Moving the Work to the Argument
To fix this, we need to change when the work happens. Instead of waiting for the recursive call to finish before adding, we can pass the current total along as an argument. This is called using an accumulator.
fun sumRangeTail(n: Int, accumulator: Int = 0): Int {
if (n <= 0) return accumulator
return sumRangeTail(n - 1, accumulator + n)
}
Notice the difference? The very last thing this function does is call itself. There is no pending addition waiting to happen after the call returns. In theory, the compiler doesn't need to keep the current stack frame alive because there's nothing left to do here once the next call starts. However, in standard JVM bytecode, this is still a recursive call, and you'll still eventually hit that stack limit—just perhaps a bit later.
Letting the Compiler Do the Heavy Lifting
This is where Kotlin's tailrec modifier comes in. By adding this keyword, you're telling the compiler: "I've written this function so that the recursive call is the absolute last operation. Please optimize this for me."
tailrec fun sumRangeOptimized(n: Int, accumulator: Int = 0): Int {
if (n <= 0) return accumulator
return sumRangeOptimized(n - 1, accumulator + n)
}
When you add tailrec, the Kotlin compiler actually rewrites your code under the hood. It transforms that recursion into a while loop in the final bytecode. You get the clean, declarative look of recursion in your source code, but the performance and safety of a loop at runtime. Now you can pass 1,000,000 into sumRangeOptimized, and it will return the result instantly without ever touching the stack limit.
Just a heads-up: tailrec only works if the recursive call is the final action. If you try to wrap the call in another operation—like 1 + sumRangeOptimized(...)—the compiler will give you a warning and ignore the modifier, because the "tail" is no longer just the function call. It's an all-or-nothing optimization.
📋 Practical Task
Optimizing a Recursive Factorial Calculator
You have been handed a legacy piece of code that calculates the factorial of a number. It currently uses the naive recursive approach, which causes the system to crash when calculating factorials for large numbers (though the result will overflow a Long eventually, we want to avoid the StackOverflowError first).
Your Task: Rewrite the following function using the accumulator pattern and the tailrec modifier to ensure it can handle deep recursion without crashing the stack.
fun factorial(n: Long): Long {
if (n <= 1) return 1
return n * factorial(n - 1)
}There are no comments for now.