尾调用是否在 Frege 中进行了优化。我知道 Java 和编译成 JVM 字节码的语言(如 Clojure 和 Scala)都没有 TCO。弗雷格呢?
1 回答
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,然后返回另一个 thunk (odd (pred (n-2)) 其中 n-2 已经被评估。这样,所有调用 (at JVM 级别)是在 println 中完成的。甚至从来没有真正调用过奇数,反之亦然。
如果取消注释 inline 指令,则会得到 even 的尾递归版本,结果的获得速度提高了十倍。
不用说,这种笨拙的算法仅用于演示——通常人们会通过位操作来检查均匀性。
这是另一个版本,这是病态的并且会堆栈溢出:
even 0 = true
even 1 = false
even n = not . odd $ n
odd = even . pred
问题在这里not
是尾调用,它的论点很严格(即,要否定某些东西,你必须首先拥有那个东西)。因此,当even n
计算时,not
必须完全评估odd n
哪个,反过来,必须完全评估even (pred n)
,因此它将占用 2*n 堆栈帧。
不幸的是,这不会改变,即使 JVM 有一天应该有适当的尾调用。原因是严格函数的参数中的递归。