0

I am trying to write a program that is 100% iterative, that is, the functions never need to return, because nothing must happen after such a return.

In other words, the program is 100% in tail position. Consider the following toy program:

  def foo(): Unit =
    bar()

  def bar(): Unit =
    foo()

  try
    foo()
  catch
    case s: StackOverflowError =>
      println(" Stack overflow!")

call foo indeed results in a stack overflow which is no surprise, indeed foo calls bar, as such bar needs a stack frame, bar then calls foo, which gain needs a stack frame, etc. It is clear why the stack overflow error occurs.

My question is, how can I define foo and bar as they are, without having a stack overflow? Languages like Scheme allow this program, they would run forever, yes, but the stack would not grow because it knows that nothing needs to happen after calling, e.g., bar from foo, so there is no need to keep the stack frame for foo upon the call to bar. Clearly scala (i.e., the JVM?) does keep the stack frame alive.

Now consider the next code example:

  def foo(): Unit = 
    foo()

  foo()

This program will run forever, but there will never occur a stack overflow.

I am aware of the @tailrec annotation, but to my understanding it would only be applicable to a situation like the second example, but not for the first example.

Any ideas? (I need the first example to run forever like the second example, without having stack overflow.)

Plegeus
  • 139
  • 11
  • 3
    Optimising mutually recursive calls requires tail call elimination rather than simple tail recursion. This is not supported by the JVM because it does not support non-local jumps, so you need to use iteration or trampolining. – Tim May 05 '23 at 13:15
  • You *can* implement proper tail calls on the JVM. Scala just chooses not to, because you can't make it both fast and interoperable. – Jörg W Mittag May 05 '23 at 17:07
  • 2
    the Scala standard library has a utility class that provides trampolining: https://www.scala-lang.org/api/2.13.x/scala/util/control/TailCalls$.html – Seth Tisue May 05 '23 at 18:55

1 Answers1

5

As you note, the JVM forbids non-local jumps, thus if foo and bar are compiled as separate methods (which is generally desirable), tail call elimination is impossible.

However, you can trampoline, by having your foo and bar return a value which the caller interprets as "call this function".

sealed trait TrampolineInstruction[A]

case class JumpOff[A](value: A) extends TrampolineInstruction[A]
case class JumpAgain[A](thunk: => TrampolineInstruction[A])
  extends TrampolineInstruction[A]

@tailrec
def runTrampolined(ti: TrampolineInstruction[A]): A =
  ti match {
    case JumpOff(value) => value
    case JumpAgain(thunk) => runTrampolined(thunk)
  }

def foo(): TrampolineInstruction[Unit] = JumpAgain(bar())
def bar(): TrampolineInstruction[Unit] = JumpAgain(foo())

runTrampolined(foo())  // will not overflow the stack, never completes

Cats provides an Eval monad which encapsulates the idea of trampolining. The above definitions of foo and bar are then

import cats.Eval

def foo(): Eval[Unit] = Eval.defer(bar())
def bar(): Eval[Unit] = Eval.defer(foo())

foo().value  // consumes a bounded (very small, not necessarily 1) number of stack frames, never completes

The monadic qualities of Eval may prove useful for expressing more complex logic without the risk of calling value in the middle of the chain.

Note: JumpOff in the first snippet is basically Eval.Leaf (generally constructed using Eval.now) and JumpAgain is basically Eval.Defer.

Levi Ramsey
  • 18,884
  • 1
  • 16
  • 30
  • Using Scala2 syntax, but it should still be valid Scala3 – Levi Ramsey May 05 '23 at 14:31
  • Thank you, do I understand it correct that both code examples u provided are conceptually the same? I tried running the second snippet, though it stops after two calls (I added println("foo") and for bar in both functions), when I add a '.value' to the foo and bar invocations within the functions bodies, I again get a stack overflow. – Plegeus May 05 '23 at 14:52
  • `Eval.defer` in place of `Eval.always` actually looks like a better fit – Levi Ramsey May 05 '23 at 15:47
  • 1
    you don't have to write your own trampoline or use Cats; you can use https://www.scala-lang.org/api/2.13.x/scala/util/control/TailCalls$.html – Seth Tisue May 05 '23 at 18:55