Is this definition of a tail recursive fibonacci function tail-recursive?

1k views Asked by At

I've seen around the following F# definition of a continuation-passing-style fibonacci function, that I always assumed to be tail recursive:

let fib k =
  let rec fib' k cont =
    match k with
    | 0 | 1 -> cont 1
    | k -> fib' (k-1) (fun a -> fib' (k-2) (fun b -> cont (a+b)))
  fib' k id

When trying out the equivalent code in Scala, I've made use of the existent @tailrec and was caught off-guard when the Scala compiler informed me the recursive calls are NOT in tail position:

  def fib(k: Int): Int = {
    @tailrec def go(k: Int, cont: Int => Int): Int = {
      if (k == 0 || k == 1) cont(1)
      else go(k-1, { a => go(k-2, { b => cont(a+b) })})
    }
    go(k, { x => x })
  }

I believe my Scala implementation is equivalent to the F# one, so I'm left wondering why the function is not tail recursive?

3

There are 3 answers

1
Jörg W Mittag On BEST ANSWER

The second call to go on line 4 is not in tail position, it is wrapped inside an anonymous function. (It is in tail position for that function, but not for go itself.)

For continuation passing style you need Proper Tail Calls, which Scala unfortunately doesn't have. (In order to provide PTCs on the JVM, you need to manage your own stack and not use the JVM call stack which breaks interoperability with other languages, however, interoperability is a major design goal of Scala.)

0
melps On

The JVMs support for tail call elimination is limited.

I can't speak of the F# implementation, but in the scala you've got nested calls to go, so it's not in tail position. The simplest way to think about it is from the stacks point of view: is there any other information the stack needs to 'remember', when doing a recursive call?

In the case of the nested go calls there obviously is, because the inner call has to be completed before the computation 'goes back' and completes the outer call.

Fib can recursively be defined like so:

def fib(k:Int) = {
  @tailrec
  def go(k:Int, p:Int, c:Int) : Int = {
    if(k == 0) p
    else { go(k-1, c p+c) }
  }
  go(k,0 1)
}
0
Christophe Calvès On

Unfortunately, the JVM does not support tail-call optimisation yet (?) (to be fair, it can sometimes optimize some calls). Scala implements tail-recursion optimisation via program transformation (every tail-recursive function is equivalent to a loop). This is generally enough for simple recursive functions but mutual recursion or continuation-passing style require the full optimisation.

This is indeed problematic when using advanced functional patterns like CPS or monadic style. To avoid blowing the stack up you need using Trampolines. It works but this is neither as convenient nor efficient as proper tail-call optimisation. Edward Kmett's comments on the subject is a good reading.