Kotlin Tip #35: Use Tailrec for Efficient Recursion — 100 Kotlin Tips in 100 Days

Raphael De Lio
Kotlin with Raphael De Lio
4 min readMar 16, 2024

--

Twitter | LinkedIn | YouTube | Instagram
Tip #34: Prefer Inline Classes for Wrapping Primitive Types

Recursion is a fundamental concept in computer programming, allowing functions to call themselves to solve problems. However, traditional recursion can lead to performance issues, such as stack overflow errors, especially with deep recursion levels. Kotlin addresses this concern with a feature known as tail recursion optimization, enabled by the tailrec modifier.

To understand how Kotlin can optimize tail-recursive functions, we first need to understand the difference between a tail-recursive and a non-tail-recursive function.

In non-tail recursion, the function performs some operation after the recursive call. Here’s an example with a simple recursive function to calculate the sum of numbers up to n:

fun sumUpTo(n: Int): Int {
if (n == 1) return 1
// Recursive call is not the last operation because we add 'n' after it returns.
return n + sumUpTo(n - 1)
}

In sumUpTo(n - 1), the recursive call is made, but once it returns, we add n to the result. This means the current call must wait for the recursive call to complete before it can proceed.

In tail recursion, the function’s last operation is the recursive call. There are no steps after the recursive call, allowing the function to pass its current state through parameters without keeping any additional information on the call stack. Here’s how we can rewrite the above example to be tail-recursive in Kotlin:

fun sumUpToTailRec(n: Int, accumulator: Int = 0): Int {
if (n == 1) return accumulator + 1
// Recursive call is the last operation, and we carry the current state through 'accumulator'.
return sumUpToTailRec(n - 1, accumulator + n)
}

In this tail-recursive version, the last thing the function does is call itself (sumUpToTailRec(n - 1, accumulator + n)), with no additional operations after that. The current state is passed through the accumulator parameter, which accumulates the sum as the recursion unfolds.

This characteristic allows the Kotlin compiler to optimize the recursion, transforming it into a loop under the hood. The benefit? Significantly reduced risk of stack overflow errors and improved performance.

To take advantage of this optimization, prepend your recursive function definition with the tailrec keyword. Note, however, that not all recursive functions can be marked as tail recursive. The recursive call must be in tail position, meaning it must be the last action the function takes.

Here’s a simple example to illustrate:

tailrec fun countDown(n: Int) {
if (n > 0) {
println(n)
countDown(n - 1)
}
}

In this example, countDown is a tail-recursive function. The use of an accumulator parameter allows the recursive call to be the last operation, making the function eligible for the tailrec optimization.

When you mark a recursive function with the tailrec modifier, you're giving the Kotlin compiler permission to optimize the function if it fits the criteria for tail recursion (i.e., the last operation is a recursive call and there's no additional processing after that call). Here's what happens next:

  1. Detection: The compiler first verifies that the function is truly tail-recursive. It checks if the last action of the function is a call to itself and that there are no additional computations after this call.
  2. Transformation: If the function meets the criteria, the compiler transforms the recursive function into a loop during the compilation process. This means that instead of the function calling itself and creating a new stack frame for each call, the compiler generates code that uses a loop to repeat the function’s body, effectively eliminating the need for multiple stack frames.
  3. Execution: As a result, the optimized function uses a constant amount of stack memory, regardless of the recursion depth. This prevents stack overflow errors that can occur with deep recursive calls and improves performance by reducing the overhead associated with function calls.

The countDown tail-recursive function might be transformed by the Kotlin compiler into something conceptually similar to this loop (in Kotlin, not bytecode, for clarity):

fun countDown(n: Int) {
var counter = n
while (counter > 0) {
println(counter)
counter--
}
}

The tailrec modifier in Kotlin is a powerful tool for optimizing recursive functions, ensuring they are both efficient and safe from stack overflow errors. By understanding and applying tail recursion, you can write more efficient and reliable Kotlin code, further enhancing the maintainability and performance of your applications.

I hope you have enjoyed this tip of our series! Don’t forget to subscribe and stay tuned for more Kotlin tips!

Stay curious!

Tip #36: Use filter and filterNot to Collection filtering

Contribute

Writing takes time and effort. I love writing and sharing knowledge, but I also have bills to pay. If you like my work, please, consider donating through Buy Me a Coffee: https://www.buymeacoffee.com/RaphaelDeLio

Or by sending me BitCoin: 1HjG7pmghg3Z8RATH4aiUWr156BGafJ6Zw

Follow Me on Social Media

Stay connected and dive deeper into the world of Kotlin with me! Follow my journey across all major social platforms for exclusive content, tips, and discussions.

Twitter | LinkedIn | YouTube | Instagram

--

--