【问题标题】:Is this definition of a tail recursive fibonacci function tail-recursive?这个尾递归斐波那契函数的定义是尾递归的吗?
【发布时间】:2015-09-08 20:38:16
【问题描述】:

我已经看到以下 F# 定义的连续传递式斐波那契函数,我一直认为它是尾递归的:

let fib k =
  let rec fib' k cont =
    match k with
    | 0 | 1 -> cont 1
    | k -> fib' (k-1) (fun a -> fib' (k-2) (fun b -> cont (a+b)))
  fib' k id

在 Scala 中尝试等效代码时,我使用了现有的 @tailrec,但当 Scala 编译器通知我递归调用不在尾部位置时,我感到措手不及:

  def fib(k: Int): Int = {
    @tailrec def go(k: Int, cont: Int => Int): Int = {
      if (k == 0 || k == 1) cont(1)
      else go(k-1, { a => go(k-2, { b => cont(a+b) })})
    }
    go(k, { x => x })
  }

我相信我的 Scala 实现等同于 F#,所以我想知道为什么该函数不是尾递归的?

【问题讨论】:

    标签: scala f# functional-programming tail-recursion continuation-passing


    【解决方案1】:

    第 4 行对go 的第二次调用不在尾部位置,它被包裹在一个匿名函数中。 (它在尾部位置对于那个函数,但不是对于go 本身。)

    对于延续传递风格,您需要适当的尾调用,遗憾的是 Scala 没有。 (为了在 JVM 上提供 PTC,您需要管理自己的堆栈,而不是使用破坏与其他语言互操作性的 JVM 调用堆栈,但是,互操作性是 Scala 的主要设计目标。)

    【讨论】:

    • 在这种情况下,很容易找到不需要 CPS 的斐波那契函数的替代实现。那么,我应该如何在二叉搜索树中实现 max / sum 函数呢?除了天真的递归实现之外,我看到的唯一方法是使用堆栈(双端队列)。对此有什么想法吗?
    【解决方案2】:

    JVM 对尾调用消除的支持是有限的。

    我不能说 F# 实现,但是在 scala 中你有嵌套调用 go,所以它不在尾部位置。考虑它的最简单方法是从堆栈的角度来看:在进行递归调用时,堆栈是否需要“记住”任何其他信息?

    在嵌套 go 调用的情况下,显然存在,因为内部调用必须在计算“返回”并完成外部调用之前完成。

    Fib 可以这样递归定义:

    def fib(k:Int) = {
      @tailrec
      def go(k:Int, p:Int, c:Int) : Int = {
        if(k == 0) p
        else { go(k-1, c p+c) }
      }
      go(k,0 1)
    }
    

    【讨论】:

      【解决方案3】:

      不幸的是,JVM 还不支持尾调用优化(?)(公平地说,它有时可以优化一些调用)。 Scala 通过程序转换实现了尾递归优化(每个尾递归函数都相当于一个循环)。对于简单的递归函数,这通常就足够了,但相互递归或继续传递样式需要完全优化。

      这在使用 CPS 或 monadic 样式等高级函数模式时确实存在问题。为避免炸毁堆栈,您需要使用Trampolines。它可以工作,但这既不像适当的尾调用优化那样方便也不高效。 Edward Kmett's comments 这个主题很好读。

      【讨论】:

        猜你喜欢
        • 2014-04-02
        • 1970-01-01
        • 2021-11-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-12-03
        • 2020-03-21
        相关资源
        最近更新 更多