Frege 通过简单地生成 while 循环来进行尾递归优化。
一般尾调用是通过懒惰“顺便”处理的。如果编译器看到对已知(间接)递归的可疑函数的尾调用,则返回惰性结果(thunk)。因此,调用该函数的真正负担在于调用者。这样可以避免深度取决于数据的堆栈。
话虽如此,静态堆栈深度在函数式语言中已经比在 Java 中更深了。因此,需要为某些程序提供更大的堆栈(即使用 -Xss1m)。
存在一些病态的情况,即构建大的 thunk 并且在评估它们时会发生堆栈溢出。一个臭名昭著的例子是 foldl 函数(与 Haskell 中的问题相同)。因此,Frege 中的标准左折叠是 fold,它在累加器中是尾递归和严格的,因此可以在恒定堆栈空间中工作(如 Haskells foldl')。
以下程序应该不堆栈溢出,但在 2 或 3 秒后打印“false”:
module Test
-- inline (odd)
where
even 0 = true
even 1 = false
even n = odd (pred n)
odd n = even (pred n)
main args = println (even 123_456_789)
它的工作原理如下:println 必须有一个要打印的值,因此尝试评估(甚至 n)。但它得到的只是一个 thunk to (odd (pred n))。因此它试图评估这个 thunk,它得到另一个 thunk 到 (even (pred (pred n)))。 even 必须评估 (pred (pred n)) 以查看参数是 0 还是 1,然后返回另一个已经评估 n-2 的 thunk (odd (pred (n-2))。
这样,所有调用(在 JVM 级别)都是在 println 中完成的。任何时候都不会真正调用奇数,反之亦然。
如果取消了 inline 指令,则会得到 even 的尾递归版本,结果会快十倍。
不用说,这种笨拙的算法仅用于演示 - 通常会通过位操作来检查均匀性。
这里是另一个版本,是病态的,会堆栈溢出:
even 0 = true
even 1 = false
even n = not . odd $ n
odd = even . pred
这里的问题是not 是尾调用并且它的论点是严格的(即,要否定某些东西,你必须首先拥有那个东西)。因此,当计算 even n 时,not 必须完全评估 odd n,而 odd n 又必须完全评估 even (pred n),因此需要 2*n 堆栈帧。
不幸的是,这不会改变,即使 JVM 有一天应该有适当的尾调用。原因是严格函数的参数中的递归。