【问题标题】:Why use a helper function inside a recursive function?为什么在递归函数中使用辅助函数?
【发布时间】:2021-01-03 01:55:00
【问题描述】:

Scala 中的函数式编程一书中,在解释如何在函数式编程中经常使用递归而不是命令式迭代时,作者通过使用称为“go”的辅助函数通过阶乘函数展示了递归”或“循环”,并声明这是标准做法是函数式Scala编程:

...
def factorial(n: Int): Int = {
  @tailrec def go(n: Int, acc: Int): Int = {
    if (n <= 0 ) acc
    else go(n - 1, n*acc)
  }
  go(n, 1)
}

...但是如果不是更简洁地定义它,也可以很容易地定义它,而不需要辅助函数:

...
def factorial(n: Int): Int = {
  if (n <= 0) 1
    else n * factorial(n - 1)
}

我的理解是,通过利用堆栈框架并将返回值“传递”到前一个堆栈框架,可以在递归中实现累积值和避免突变。在这里,作者似乎出于类似目的使用了显式累加器参数。

使用辅助函数来累积这样的值是否有优势,或者他们是否使用此示例通过显式将状态传递给辅助函数来展示递归与命令式迭代的关系?

【问题讨论】:

  • 因为tailrec(尾递归优化)。没有它,它会在某些时候出现堆栈溢出(这个问题在这个网站上多么讽刺)异常+它的效率要低得多
  • @ArtemSokolov 但第二个版本不会以堆栈溢出结束吗?它会达到基本情况。或者你的意思是如果没有注释你不能让编译器抱怨它不能做尾调用优化并且你不能把注释放在外部函数上?
  • 尾递归不使用调用堆栈,而是使用累加器,因此可以消除前者。这种累加器风格的递归只是引擎盖下的一个循环。在懒惰的环境中,您可以选择懒惰的右弃牌。懒惰可以免费提高堆栈安全性。
  • @Aaron 注解不应用尾递归,它只会在未优化时强制编译器失败。第二个版本会因为 SO 而失败,因为没有 tailrec 优化,因为没有函数的尾递归结构。
  • 啊,我明白了,在第二种情况下,递归调用后还有更多工作要做,因此编译器无法将其优化为循环,对吧?所以第一条评论应该是答案。如果你把它作为答案,我会这样标记它。

标签: scala recursion functional-programming tail-recursion


【解决方案1】:

尾递归是一项重要的优化,它可以使递归函数更快,并在递归太深时避免“堆栈溢出”。

没有尾递归的递归

def factorial(n: Int): Int = {
  if (n <= 0) 1
    else n * factorial(n - 1)
}

当我想计算factorial(10) 时会发生什么?首先,我计算factorial(9);然后,我将结果乘以 10。这意味着当我在计算 factorial(9) 时,我需要在某处记下:“记住,当你完成 factorial(9) 后,你仍然需要乘以 10!” .

然后为了计算factorial(9),我必须先计算factorial(8),然后将结果乘以9。所以我写了一个小笔记“当你有factorial(8)的结果时记得乘以9。

继续;最后我到达factorial(0),即1。这时候我有十个小笔记,上面写着“当你完成factorial(0)时记得乘以1”、“当你完成factorial(1)时记得乘以2”等等。

这些笔记被称为“堆栈”,因为它们实际上是堆叠在一起的。如果堆栈太大,程序会因“堆栈溢出”而崩溃。

尾递归

def factorial(n: Int): Int = {
  @tailrec def go(n: Int, acc: Int): Int = {
    if (n <= 0 ) acc
    else go(n - 1, n*acc)
  }
  go(n, 1)
}

这个程序中的函数go是不同的。为了计算go(10, 1),你需要计算go(9, 10);但是当你计算完go(9, 10)后,你就不需要做任何其他事情了!可以直接返回结果。所以没必要留一点“递归调用后记得把结果乘以10”。编译器优化了这种行为:它不是将对go(9, 10) 的调用叠加在对go(10, 1) 的调用之上,而是将对go(10, 1) 的调用替换为对go(9, 10) 的调用。然后它将对go(9, 10) 的调用替换为对go(8, 90) 的调用。所以堆栈在递归期间永远不会增加。这称为尾递归,因为递归调用是函数执行过程中发生的最后一件事(特别是在计算参数时发生乘以 10)。

【讨论】:

  • 这是一个很好的答案,但是Artem在cmets中第一个回答,所以给了他答案。
  • 如果这个答案更好,为什么不改变你的投票呢?可以更改您接受的答案。
  • 我同意如果更准确、更清晰、更正确的帖子更适合您的原始问题,则应将其标记为答案的立场。 Stef 的帖子很棒,值得鼓励。
  • @Stef 没有不尊重,我想因为他先回答我接受了,但我同意你的回答真的很准确。感谢您抽出宝贵时间。
  • 在尾部优化的情况下,编译器是否实际上转换为与 while 循环相同的字节码?
【解决方案2】:

因为tailrec(尾递归优化)。

如果没有它,在某些时候会出现堆栈溢出(这个问题在这个网站上是多么讽刺)异常,而且效率要低得多(主要是通过内存)。

需要注意的是,@tailrec 注释不会执行或应用 tailrec 优化,而只是强制编译器在无法优化时失败。

尾递归优化要求你的下一个迭代函数调用应该是返回表达式(没有额外的操作或计算)。

【讨论】:

  • 这本书并没有明确说明这就是为什么有一个辅助函数,尽管它确实提到了尾调用优化。
猜你喜欢
  • 2019-06-12
  • 2014-12-02
  • 1970-01-01
  • 2016-02-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-17
相关资源
最近更新 更多