【问题标题】:Tail recursion and call by name / value尾递归和按名称/值调用
【发布时间】:2019-10-19 12:24:10
【问题描述】:

学习 Scala 和函数式编程。在以下尾递归阶乘实现中:

def factorialTailRec(n: Int) : Int = {

    @tailrec
    def factorialRec(n: Int, f: => Int): Int = {
      if (n == 0) f else factorialRec(n - 1, n * f)
    }

    factorialRec(n, 1)
}

我想知道由 value 调用第二个参数与由 name 调用(正如我所做的那样)是否有任何好处。在第一种情况下,每个堆栈框架都承载着一个产品。在第二种情况下,如果我的理解是正确的,整个产品链将在nth 堆栈帧处转移到if ( n== 0) 的情况下,所以我们仍然必须执行相同数量的乘法运算。不幸的是,这不是 a^n 形式的乘积,可以通过重复平方以 log_2n 步计算,而是每次相差 1 的项的乘积。所以我看不到任何优化最终产品的可能方法:它仍然需要 O(n) 项的乘法。

这是正确的吗?就复杂性而言,这里的按值调用是否等同于按名称调用?

【问题讨论】:

    标签: scala tail-recursion callbyname call-by-value


    【解决方案1】:

    通过实验我发现,通过名称调用形式,该方法变成... 非尾递归!我制作了这个示例代码来比较阶乘尾递归和阶乘非尾递归:

     package example
    
     import scala.annotation.tailrec
    
     object Factorial extends App {
    
      val ITERS = 100000
    
      def factorialTailRec(n: Int) : Int = {
        @tailrec
        def factorialTailRec(n: Int, f: => Int): Int = {
          if (n == 0) f else factorialTailRec(n - 1, n * f)
        }
        factorialTailRec(n, 1)
      }
    
      for(i <-1 to ITERS) println("factorialTailRec(" + i + ") = " + factorialTailRec(i))
    
    
      def factorial(n:Int) : Int = {
        if(n == 0) 1 else n * factorial(n-1)
      }
    
      for(i <-1 to ITERS) println("factorial(" + i + ") = " + factorial(i))
    
    }
    

    观察到内部tailRec 函数按名称调用第二个参数。 @tailRec 注释仍然不会引发编译时错误!

    我一直在为 ITERS 变量设置不同的值,对于 100,000 的值,我收到了...StackOverflowError

    (由于Int溢出,结果为零。)

    所以我继续将factorialTailRec/2的签名更改为:

    def factorialTailRec(n: Int, f: Int): Int 
    

    即按值调用参数f。这一次,运行factorialTailRecmain 部分完成得非常好,而factorial/1 当然以完全相同的整数崩溃。

    非常非常有趣。在这种情况下,似乎按名称调用会维护堆栈帧,因为需要计算产品本身一直到调用链。

    【讨论】:

    • 你的函数是tailrec。问题是,它正在构建一个非tailrec 函数(以tailrec 方式),它最后会评估并且后者会破坏堆栈。按名称参数就像一个非 args 函数。
    • 恐怕我不太明白您所说的“以tailrec方式构建非tailrec函数”是什么意思。我对这里发生的事情的直觉是,在返回点(尾递归的基本情况),我们必须评估 n 项的乘积。如果这是开销,我会理解的。不幸的是,终端输出关注,strinctly,一个StackOverflowError,这让我觉得它不可能是tailrec!
    • @Jason: factorialTailRec 尾递归的。然而,它所构建的匿名函数(本质上就是按名称参数)不是。
    • @Jason 你是对的,最后,程序评估 n 项的乘积。问题是,正如 Jörg W Mittag 所说,这个产品是作为非尾递归匿名函数构建的。当您使用按值参数时,它应该可以工作,因为您不会构建一个函数,而是一个值。
    • 知道了,大家。非常感谢。这可以概括,对吧?如果按名称调用其任何一个参数,则无法使用尾递归函数。
    【解决方案2】:

    让我稍微扩展一下您在 cmets 中已经被告知的内容。 这就是编译器对按名称参数进行去糖的方式:

    @tailrec
    def factorialTailRec(n: Int, f: => Int): Int = {
      if (n == 0) {
        val fEvaluated = f
        fEvaluated
      } else {
        val fEvaluated = f // <-- here we are going deeper into stack. 
        factorialTailRec(n - 1, n * fEvaluated)
      }
    }
    

    【讨论】:

    • 哈!猜猜当我用def 替换val 时发生了什么。这种语言是惊人的。谢谢。
    • 我猜它开始工作了,对吧?无论如何,我强烈建议一次使用不超过一个“惊人”的 scala 功能。通常令人惊奇的事情不像你的情况那样总结起来:)
    • 是的,确实如此。只是那时我记得val 会计算,而def... def 是一个名称并等待调用该名称。我希望这里的lazy valdef 具有相同的效果。我会在电脑前调查一次。你说的对我过分热情是对的:过早的优化、万恶之源等等
    猜你喜欢
    • 2016-03-21
    • 1970-01-01
    • 1970-01-01
    • 2017-02-07
    • 1970-01-01
    • 2015-06-07
    • 1970-01-01
    • 1970-01-01
    • 2013-10-02
    相关资源
    最近更新 更多