46

Why won't the Scala compiler apply tail call optimization unless a method is final?

For example, this:

class C {
    @tailrec def fact(n: Int, result: Int): Int =
        if(n == 0)
            result
        else
            fact(n - 1, n * result)
}

results in

error: could not optimize @tailrec annotated method: it is neither private nor final so can be overridden

What exactly would go wrong if the compiler applied TCO in a case such as this?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Seth Tisue
  • 29,985
  • 11
  • 82
  • 149
  • This question confuses `TCO`, which could be safely used with this method, and the more restrictive `tailrec`, which can't be used because the method might not be self-recursive. – Tim Feb 27 '19 at 17:29

5 Answers5

56

Consider the following interaction with the REPL. First we define a class with a factorial method:

scala> class C {
         def fact(n: Int, result: Int): Int =
           if(n == 0) result
           else fact(n - 1, n * result)
       }
defined class C

scala> (new C).fact(5, 1)
res11: Int = 120

Now let's override it in a subclass to double the superclass's answer:

scala> class C2 extends C {
         override def fact(n: Int, result: Int): Int = 2 * super.fact(n, result)
       }
defined class C2

scala> (new C).fact(5, 1)
res12: Int = 120

scala> (new C2).fact(5, 1)

What result do you expect for this last call? You might be expecting 240. But no:

scala> (new C2).fact(5, 1)
res13: Int = 7680

That's because when the superclass's method makes a recursive call, the recursive call goes through the subclass.

If overriding worked such that 240 was the right answer, then it would be safe for tail-call optimization to be performed in the superclass here. But that isn't how Scala (or Java) works.

Unless a method is marked final, it might not be calling itself when it makes a recursive call.

And that's why @tailrec doesn't work unless a method is final (or private).

UPDATE: I recommend reading the other two answers (John's and Rex's) as well.

Seth Tisue
  • 29,985
  • 11
  • 82
  • 149
  • Might be worth spelling out that "might not be calling itself" is a Scala-specific issue that has nothing to do with tail call elimination in general. All of these tail calls would be eliminated in SML, OCaml, F# etc. – J D Jan 31 '12 at 15:49
  • How could you write the `fact` parent and child such that the child's intended 2 × parent's `fact`? – Kevin Meredith Apr 16 '14 at 02:51
  • @KevinMeredith: I think that'd make a great separate question. – Seth Tisue Apr 16 '14 at 03:15
23

Recursive calls might be to a subclass instead of to a superclass; final will prevent that. But why might you want that behavior? The Fibonacci series doesn't provide any clues. But this does:

class Pretty {
  def recursivePrinter(a: Any): String = { a match {
    case xs: List[_] => xs.map(recursivePrinter).mkString("L[",",","]")
    case xs: Array[_] => xs.map(recursivePrinter).mkString("A[",",","]")
    case _ => a.toString
  }}
}
class Prettier extends Pretty {
  override def recursivePrinter(a: Any): String = { a match {
    case s: Set[_] => s.map(recursivePrinter).mkString("{",",","}")
    case _ => super.recursivePrinter(a)
  }}
}

scala> (new Prettier).recursivePrinter(Set(Set(0,1),1))
res8: String = {{0,1},1}

If the Pretty call was tail-recursive, we'd print out {Set(0, 1),1} instead since the extension wouldn't apply.

Since this sort of recursion is plausibly useful, and would be destroyed if tail calls on non-final methods were allowed, the compiler inserts a real call instead.

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
  • 3
    Well, when I read yours, I was left with the impression, "Scala fails to optimize so it can produce weird, crazy results instead of exactly what you want." So I thought I'd better provide another example that made it a little clearer why 7680 is the "right" answer. – Rex Kerr Jan 24 '11 at 19:01
  • "this sort of recursion is plausibly useful". Its the backbone of functional techniques like continuation passing style. – J D Jan 30 '12 at 22:08
  • @JonHarrop - Indeed. CPS and other functional techniques relying upon this are plausibly useful. – Rex Kerr Jan 31 '12 at 04:06
  • FWIW, here is an example of it being used in the finance sector http://zbray.com/2011/11/02/solving-the-0-1-knapsack-problem-using-continuation-passing-style-with-memoization-in-f/ – J D Jan 31 '12 at 15:48
7

Let foo::fact(n, res) denote your routine. Let baz::fact(n, res) denote someone else's override of your routine.

The compiler is telling you that the semantics allow baz::fact() to be a wrapper, that MAY upcall (?) foo::fact() if it wants to. Under such a scenario, the rule is that foo::fact(), when it recurs, must activate baz::fact() rather than foo::fact(), and, while foo::fact() is tail-recursive, baz::fact() may not be. At that point, rather than looping on the tail-recursive call, foo::fact() must return to baz::fact(), so it can unwind itself.

Shawn Mehan
  • 4,513
  • 9
  • 31
  • 51
John R. Strohm
  • 7,547
  • 2
  • 28
  • 33
  • thanks, John. my answer focuses on what the result is, but you're right to also point out that the tail-call property is lost, too. – Seth Tisue Jan 24 '11 at 18:42
  • Probably worth spelling out that they're both tail recursive and the problem is that Scala is only capable of tail call optimizing one of them (due to the lack of tail call elimination in the JVM). – J D Jan 30 '12 at 22:13
  • @JonHarrop, tail call elimination is done by the compiler, not the "processor" (JVM). The JVM is essentially a microprocessor, implemented in software, with a machine language that is convenient for Java. JVM has a branch instruction ("goto") and a subroutine call instruction ("jsr") and it is the COMPILER'S job to decide which instruction to emit. If the Java compiler doesn't do tail call optimization, that's the compiler's fault. (Note: In the second decade of the 21st century, a production-grade compiler that can't do tail call optimization must be regarded as hopelessly broken.) – John R. Strohm Oct 22 '14 at 12:52
  • @JohnR.Strohm: "JVM has a branch instruction ("goto") and a subroutine call instruction ("jsr") and it is the COMPILER'S job to decide which instruction to emit". That is only true in the special case of a function tail calling itself. In general, tail calls can go anywhere. Real microprocessors have jump instructions that can take the program counter anywhere. The JVM `goto` instruction is limited to targets in the current function body. So the JVM is incapable of expressing tail calls in general natively. The Scala compiler is helpless and crippled by this deficiency in the VM. – J D Oct 23 '14 at 14:15
  • 1
    @JonHarrop: I see your point, and you're correct. I was thinking of tail recursion optimization, which is a special case of general tail call optimization. – John R. Strohm Oct 23 '14 at 14:19
  • @JohnR.Strohm: FWIW, Sun researched the possibility of putting tail call elimination in Hotspot but it was not done and their production focus has been on dynamic languages (e.g. `InvokeDynamic`) since rather than functional languages: https://blogs.oracle.com/jrose/entry/tail_calls_in_the_vm – J D Oct 23 '14 at 15:02
1

What exactly would go wrong if the compiler applied TCO in a case such as this?

Nothing would go wrong. Any language with proper tail call elimination will do this (SML, OCaml, F#, Haskell etc.). The only reason Scala does not is that the JVM does not support tail recursion and Scala's usual hack of replacing self-recursive calls in tail position with goto does not work in this case. Scala on the CLR could do this as F# does.

J D
  • 48,105
  • 13
  • 171
  • 274
  • hey mate can you tell me why f# blows up in this case http://ideone.com/VgJnR6 - mono on ideone fails with sigsegv and tryfsharp in win8 crashes IE – OlegYch Aug 13 '13 at 01:06
  • @OlegYch Mono and TryFSharp don't do proper tail call elimination. Your program works fine on .NET when TCO is enabled. – J D Aug 13 '13 at 13:52
  • Thanks Jon, appreciate your feedback! – OlegYch Aug 13 '13 at 14:58
  • @OlegYch I would recommend to re-try your sample again with Mono because nowadays it has much better tailcall support – knocte Dec 13 '18 at 06:44
  • @knocte I would be amazed if Mono handled tail call optimisation properly but I'll take a look. – J D Dec 13 '18 at 15:41
  • jaykrell has been working on that the last months/years... be sure to test the last preview – knocte Dec 13 '18 at 15:56
0

The popular and accepted answer to this question is actually misleading, because the question itself is confusing. The OP does not make the distinction between tailrec and TCO, and the answer does not address this.

The key point is that the requirements for tailrec are more strict than the requirements for TCO.

The tailrec annotation requires that tail calls are made to the same function, whereas TCO can be used on tail calls to any function.

The compiler could use TCO on fact because there is a call in the tail position. Specifically, it could turn the call to fact into a jump to fact by adjusting the stack appropriately. It does not matter that this version of fact is not the same as the function making the call.

So the accepted answer correctly explains why a non-final function cannot be tailrec because you cannot guarantee that the tail calls are to the same function and not to an overloaded version of the function. But it incorrectly implies that it is not safe to use TCO on this method, when in fact this would be perfectly safe and a good optimisation.

[ Note that, as explained by Jon Harrop, you cannot implement TCO on the JVM, but that is a restriction of the compiler, not the language, and is unrelated to tailrec ]


And for reference, here is how you can avoid the problem without making the method final:

class C {
  def fact(n: Int): Int = {
    @tailrec
    def loop(n: Int, result: Int): Int =
      if (n == 0) {
        result
      } else {
        loop(n - 1, n * result)
      }

    loop(n, 1)
  }
}

This works because loop is a concrete function rather than a method and cannot be overridden. This version also has the advantage of removing the spurious result parameter to fact.

This is the pattern I use for all recursive algorithms.

Tim
  • 26,753
  • 2
  • 16
  • 29