【问题标题】:Why is Perl so afraid of "deep recursion"?为什么 Perl 如此害怕“深度递归”?
【发布时间】:2014-01-09 17:00:56
【问题描述】:

我最近偶然发现了一本书Higher-order Perl,它基本上提出了在 Perl 中以函数方式做事的方法。作者解释说,Perl 有 Lisp 的 7 个核心特性中的 6 个,而 C 没有。

我遇到了一个看起来很适合递归解决方案的问题,我以这种方式对其进行了编码。但是 Perl 抱怨“深度递归”。我搜索了一下,发现一个 Perl 僧侣解释说“Perl 不是 Haskell”。显然,当递归深度超过 100 级时,默认情况下您会收到投诉。

种方法可以扩展此限制或完全关闭它,但我的问题是:

  • Perl 对递归如此紧张,而 Haskell 则完全没有,有什么原因吗?

【问题讨论】:

    标签: perl haskell recursion


    【解决方案1】:

    因为在 Haskell 中,我们有惰性和保护递归以及尾调用优化,而 Perl 两者都没有。

    这实质上意味着每个函数调用都会分配一定数量的内存,称为“堆栈”,直到函数返回。当您编写递归代码时,由于这些嵌套的函数调用,您会构建大量内存,并最终会导致堆栈溢出。 TCO 允许优化掉未使用的块。

    没有这个,依赖递归就不是一个好主意。例如,假设您编写了map 的递归版本,它会因任何大小合适的列表而崩溃。在 Haskell 中,受保护的递归意味着对于大型列表,递归解决方案比类似“循环”的函数快得多

    TLDR:Haskell 的实现旨在处理函数式/递归风格,而 perl 的实现则不是,而且在过去,100 级函数调用是一个合理的限制。

    【讨论】:

    • 不同意;虽然 perl 没有自动递归优化,但 100 的限制更多地来自于假设它表明程序错误而不是任何不好的事情会发生(尽管显然几十年前的内存比现在更受关注)。您可以编写递归代码,它会占用内存,但不会占用“大量”内存。
    • Perl 确实有尾调用优化——它不是自动的——你需要使用 goto 关键字来进行尾调用。
    • @tobyink goto 不是电话。但是 TCO 通常会产生与 goto 类似的代码。此外,如果它不是自动的,它就不是真正的实现优化
    • Perl 中的goto 关键字执行几个不同的任务。 goto LABEL 就像一个传统的“goto”。 goto $coderefgoto &subname 进行尾随。他们从堆栈中擦除当前 sub 的所有痕迹,然后调用 coderef/sub。 (实际上,它们在内部实现为立即返回,然后是正常的子调用。)
    • 您还可以使用Sub::Call::Tail 使用代码进行更简洁的尾调用。
    【解决方案2】:

    “深度递归”警告是可选的,它表明可能出现了问题:大多数情况下,不打算一遍又一遍地调用自己的函数(Perl 是一种多范式语言,许多人们使用功能性成语)。即使有意识地使用递归,也很容易忘记基本情况。

    关闭“深度递归”警告很容易:

    use warnings;
    no warnings 'recursion';
    
    sub recurse {
      my $n = shift;
      if ($n) {
        recurse($n - 1);
      }
      else {
        print "look, no warnings\n";
      }
    }
    
    recurse(200);
    

    输出:

    look, no warnings
    

    确实,Perl 不执行尾递归优化,因为那会弄乱caller 的输出(这对于一些诸如pragma 或Carp 之类的东西至关重要)。如果你想手动执行尾调用,那么

    return foo(@args);
    

    变成

    @_ = @args; # or something like `*_ = \@args`
    goto &foo;
    

    虽然如果你愚蠢地 localized @_ 会发生坏事。

    【讨论】:

      【解决方案3】:

      在无限分配最终耗尽可用内存后,失控递归将使 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 将永远在外面密谋反对我们。编程语言以自己的方式处理这些限制。

      【讨论】:

        【解决方案4】:

        默认限制太低,但适用于最初运行 Perl 的较小机器。如果你正在做严肃的递归工作,现在 100 是可笑的,但正如你所说,它可以调整。我认为 Haskell 有其他方法可以捕获无限递归?

        【讨论】:

        • 现在更现实了。不知道撞到了。谢谢。
        • 我假设 Haskell 将尾递归优化成一个循环。在像 perl 这样的命令式语言中,您只需告诉人们自己编写一个循环;)
        • @UlrichSchwarz 实际上,haskell 有尾调用优化,不仅限于递归。这是不可能仅通过循环来伪造的。
        • (抱歉,它没有从 100 更改。调试器中的类似设置已更改为 1000。我也打算更改它以用于 Perl 的警告。)
        • Haskell 没有捕获无限递归,原因与命令式语言没有捕获无限循环的原因完全相同。
        猜你喜欢
        • 2015-01-08
        • 2011-02-05
        • 1970-01-01
        • 2012-02-07
        • 1970-01-01
        • 2021-03-23
        • 1970-01-01
        • 2014-03-16
        • 2021-11-05
        相关资源
        最近更新 更多