在无限分配最终耗尽可用内存后,失控递归将使 Perl 程序或 Haskell 程序崩溃。两种语言都以不同的方式处理这个潜在的陷阱。
Perl
Perl 是一种多范式语言,但本质上是过程性的,该范式中的深堆栈深度可能表明递归失控。在此诊断中的perldiag documentation 也说明了这一点,并强调了这一点。
- 匿名子程序的深度递归
-
子程序的深度递归
"%s"
(W 递归)这个子程序调用自身(直接或间接)的次数比它返回的次数多 100 倍。 这可能表示无限递归,除非你正在编写奇怪的基准程序,在这种情况下它表示其他东西。
这只是一个运行时警告,所以如果您认为它是虚假的,请使用 pragma 禁用它。
sub known_deep_recursion {
no warnings 'recursion';
...;
}
Perl 文档的同一部分描述了实现相同效果的更激烈且明显不必要的方法。
此阈值可以从 100 更改,方法是重新编译 perl 二进制文件,将 C 预处理器宏 PERL_SUB_DEPTH_WARN 设置为所需的值。
哈斯克尔
Haskell 是一种纯函数式语言,其中递归是必不可少的。写 Haskell tail-recursively,如
如果递归调用的最终结果是函数本身的最终结果,则递归函数是尾递归的。如果递归调用的结果必须进一步处理(例如,通过向其添加 1,或在其开头使用另一个元素),则它不是尾递归。
利用编译器对惰性求值和保护递归的支持,这意味着编写良好的 Haskell 代码既简洁又具有出色的性能。
但 Haskell 也无法避免与递归相关的失控分配。 糟糕编写的 Haskell 代码也会出现堆栈问题,如 HaskellWiki 上的 Stack overflow 页面所述。
Haskell 中没有调用堆栈。相反,我们找到了一个模式匹配堆栈,其条目本质上是 case 表达式,等待其审查者被评估到足以匹配构造函数 (WHNF)。
当 GHC 计算一个 thunked [即未计算的] 表达式时,它使用一个内部堆栈。这个用于 thunk 评估的内部堆栈实际上是可以溢出的。
Haskell 没有像过程语言那样的调用堆栈,因此这些崩溃被称为space leaks。
Haskell 程序有时会消耗比必要更多的内存,这通常是由于太多或太少的懒惰造成的。
比如下面这个简单的Haskell程序
mysum :: [Integer] -> Integer
mysum = foldr (+) 0
main = print (mysum [1..10000000000])
当在我的机器上运行时——如果它不在你的机器上,增加上限直到它运行——结果
mysum: 内存不足
将程序更改为
import Data.List (foldl')
mysum :: [Integer] -> Integer
mysum = foldl' (+) 0
main = print (mysum [1..10000000000])
需要一段时间才能运行,但最终会以正确的结果终止。对于初学者来说,推理 Haskell 程序的空间使用情况可能很棘手。根除空间泄漏using the profiler 是一个高级主题。
总结
两者都是优秀的语言,尤其是在各自的领域。我们程序员有时会犯错误。资源限制和halting problem 将永远在外面密谋反对我们。编程语言以自己的方式处理这些限制。