13

Suppose I write code like this:

tailrec fun odd(n: Int): Boolean =
        if (n == 0) false
        else even(n - 1)

tailrec fun even(n: Int): Boolean =
        if (n == 0) true
        else odd(n - 1)

fun main(args:Array<String>) {
    // :( java.lang.StackOverflowError
    System.out.println(even(99999))
}

How do I get Kotlin to optimize these mutually recursive functions, so that I can run main without throwing a StackOverflowError? The tailrec keyword works for single-function recursion, but nothing more complicated. I also see a warning that no tail-calls are found where the tailrec keyword is used. Perhaps this is too hard for compilers?

denine99
  • 345
  • 1
  • 7
  • You can add a feature request to https://youtrack.jetbrains.com for the feature of "mutual tail recursion", that is the best bet if you want it added to Kotlin. Also search there first, in case it is already requested or planned. – Jayson Minard Mar 01 '16 at 12:25
  • 1
    I created a Kotlin issue here: https://youtrack.jetbrains.com/issue/KT-11307 – denine99 Mar 05 '16 at 16:51

3 Answers3

9

What you are looking for are "proper tail calls". The JVM does not support those, so you need trampolines.

A proper tail call cleans up the memory of its own function (parameters, local variables) before jumping (instead of calling) to the tail called function. That way the tail called function can return directly to its caller-caller-function. Infinite mutual recursion is possible. (In functional languages this is one of the most important features.)

To allow proper tail calls in assembler you would need a command to jump (goto) to a routine/method that is referred to via pointer. OOP needs calls (stores location to jump back to on the stack and then jumps) to a routine/method that is referred to via pointer.

You can emulate proper tail calls with the trampoline design pattern, maybe there is some support via library. The trampoline is a while loop that calls a function which returns a reference to the next function which returns a reference to the next...

comonad
  • 5,134
  • 2
  • 33
  • 31
  • 1
    Cool, it sounds like we could support this in the JVM by writing a trampoline method which calls method references with given arguments. The `even` and `odd` functions should be modified to return method references plus the next argument. We make the first call by calling the trampoline method with a reference to the `even` function and argument `99999`. – denine99 Jun 20 '17 at 16:38
4

By wikipedia https://en.wikipedia.org/wiki/Tail_call :

a tail call is a subroutine call performed as the final action of a procedure. If a tail call might lead to the same subroutine being called again later in the call chain, the subroutine is said to be tail-recursive

So your case is not a tail recursion by definition. That't what the warning says.

Currently there is no way compiler will optimise that, mostly because it is a very rare situation. But I am not sure that even Haskel optimises that away.

voddan
  • 31,956
  • 8
  • 77
  • 87
  • 4
    From the same page (slightly edited): "functional languages [like Kotlin] that target the JVM [tend to] implement direct [or self] tail recursion, but not mutual tail recursion." I can assure you that Haskell supports mutual tail recursion. – Dan D. Mar 01 '16 at 06:16
  • It does? Cool! I thought so, just because it is Haskell, it would. Thanks for the tip. – voddan Mar 01 '16 at 06:23
  • Could you clarify further? In the even/odd example, the final action of both even and off is a subroutine call, and we see that the same subroutine is called later in the call chain. Therefore by the definition both functions are tail-recursive. – denine99 Mar 01 '16 at 18:59
  • The relevant Wiki section is here: https://en.wikipedia.org/wiki/Mutual_recursion#Computer_functions – denine99 Mar 01 '16 at 21:16
  • "Proper Tail Calls" (like in Haskell) are not an optimization; they are a needed guarantee to allow infinite recursion on which the Haskell run time system depends on. (Imagine the first command tail-calls a function given via parameter, which is the second command with its parameter pointing to the third.) Inlining of a function would be an optimization. – comonad Jun 19 '17 at 09:14
3

Here is an implementation of @comonad's trampoline suggestion. It works!

import kotlin.reflect.KFunction

typealias Result = Pair<KFunction<*>?, Any?>
typealias Func = KFunction<Result>

tailrec fun trampoline(f: Func, arg: Any?): Any? {
    val (f2,arg2) = f.call(arg)
    @Suppress("UNCHECKED_CAST")
    return if (f2 == null) arg2 
        else trampoline(f2 as Func, arg2)
}

fun odd(n: Int): Result =
        if (n == 0) null to false
        else ::even to n-1

fun even(n: Int): Result =
        if (n == 0) null to true
        else ::odd to n-1

fun main(args:Array<String>) {
    System.out.println(trampoline(::even, 9999999))
}
denine99
  • 345
  • 1
  • 7