1. Introduction
Some algorithms work best when implemented in a recursive manner – where computation is based on a simpler form of the same computation.
In most programming languages, there is a risk of a stack overflow associated with recursion. There is a limit on the number of nested method calls that can be made in one go, without returning.
If this is an issue, the algorithm can be re-written in an imperative manner, using a traditional loop instead. Tail recursion is a technique where the compiler can rewrite a recursive method in an imperative manner, assuming that certain rules are met.
2. Rules for Tail Recursion in Kotlin
To implement a function in Kotlin using tail recursion, there is one rule to follow: the recursive call must be the very last call of the method.
This rule is not as simple to follow as it seems. For example, taking the Factorial example, this would be implemented as:
fun recursiveFactorial(n: Long) : Long {
return if (n <= 1) {
n
} else {
n * recursiveFactorial(n - 1)
}
}
This works perfectly well. However, it is not eligible for tail recursion.
Broken down, the above function does the following:
- If n is <= 1 then return n
- Calculate accum = recursiveFactorial(n – 1)
- return n * accum
Written like that, you can see that the recursive call is not the last call in the function.
3. Implementing Factorial as Tail Recursion
Instead, to implement a factorial function using tail recursion we need to re-work it to change where the calculation is performed. We need to ensure that the multiplication is done before the recursive call, not after. This is easiest done by passing the partial result in as a parameter:
fun factorial(n: Long, accum: Long = 1): Long {
val soFar = n * accum
return if (n <= 1) {
soFar
} else {
factorial(n - 1, soFar)
}
}
This can be broken down into the following:
- Calculate soFar = n * accum
- If n <= 1 then return soFar
- Call the factorial function, passing in n – 1 and soFar
This time, the recursive call is the last step in our process.
Once we have a recursive function that is in the correct form, we instruct Kotlin to consider it for tail recursion by use of the tailrec keyword. This informs the compiler that it is allowed to re-write the function as a loop if it can do so. This keyword applies to the function itself, so becomes:
tailrec fun factorial(n: Long, accum: Long = 1): Long
4. Compilation Output of Factorial
The goal of this is to write recursive code that gets run in an imperative manner, to avoid stack overflow issues. If we decompile the above function, we can see that the result produced by the compiler is indeed imperative:
public final long factorial(long n, long accum) {
while(n > (long) 1) {
long var10000 = n - (long)1;
accum = n * accum;
n = var10000;
}
return n * accum;
}
We can immediately see how this works, and observe that there is absolutely no risk of a stack overflow in this code.
5. Performance Improvements
We can occasionally see performance improvements using this optimization, as well as safety gains. These benefits depend on some other factors – such as how deep the recursion is, and how complicated the calculations are.
The improvements come from the fact that method calls are more expensive than simply looping.
Using our factorial example again, we can measure how long it takes to execute and compare:
- Calculating factorial(50) 1,000,000 times without tail recursion takes ~70ms
- Calculating factorial(50) 1,000,000 times with tail recursion takes ~45ms
Using the naive benchmark, we got a speedup of 36%, which is significant just for allowing the compiler to re-work our implementation.
Note that these measurements are from very simple benchmarking of a simple function. Actual performance changes will vary based on circumstances, and should always be measured before making any decisions
6. Summary
In some scenarios, tail recursion can give some important benefits to our code – both regarding safety and performance. Kotlin’s compiler can do this automatically – we just need to use the right keyword.