【问题标题】:Moving recursive calls in "almost tail position" to true tail position将“几乎尾部位置”的递归调用移动到真正的尾部位置
【发布时间】:2016-06-23 11:55:52
【问题描述】:

在阅读 Guido's reasoning for not adding tail recursion elimination to Python 时,我在 Haskell 中编造了这个几乎尾递归的例子:

triangle :: Int -> Int
triangle 0 = 0
triangle x = x + triangle (x - 1)

这当然不是尾调用,因为虽然递归调用本身是在“返回”中,但 x + 会阻止当前堆栈被重复用于递归调用。

但是,可以将其转换为尾递归代码(尽管相当丑陋和冗长):

triangle' :: Int -> Int
triangle' = innerTriangle 0
    where innerTriangle acc 0 = acc
          innerTriangle acc x = innerTriangle (acc + x) (x - 1)

这里的innerTriangle 是尾递归的,由triangle' 启动。虽然微不足道,但似乎这样的转换也适用于其他任务,例如构建列表(这里acc 可能只是正在构建的列表)。

当然,如果函数返回中没有递归调用,这似乎是不可能的:

someRecusiveAction :: Int -> Bool
someRecursiveAction x = case (someRecursiveAction (x * 2)) of
    True -> someAction x
    False -> someOtherAction x

但我只指“几乎尾”调用,其中递归调用是返回值的一部分但由于另一个函数应用程序包装它而不在尾位置(例如 x + 在 @ 987654331@ 上面的例子)。

这是否可以在功能环境中推广?势在必行的呢?是否可以将所有在其返回中具有递归调用的函数转换为在尾部位置返回的函数(即可以进行尾部调用优化的函数)?

别介意这些都不是在 Haskell 中计算三角形数的“最佳”方法,AFAIK 是 triangle x = sum [0..n]。该代码纯粹是这个问题的人为示例。

注意:我已经阅读了Are there problems that cannot be written using tail recursion?,所以我相当有信心我的问题的答案是肯定的。但是,答案提到了延续传递风格。除非我误解了 CPS,否则我转换后的triangle' 似乎仍然是直接风格。在这种情况下,我很好奇如何让这种转换以直接方式泛化。

【问题讨论】:

  • 不能对所有函数进行转换,使其只有带有 CPS 转换的尾调用吗?

标签: recursion functional-programming theory tail-recursion compiler-theory


【解决方案1】:

有一个有趣的尾递归模运算符优化空间,可以转换一些函数,使它们在恒定空间中运行。最著名的可能是tail-recursion-modulo-cons,其中不完全尾调用是构造函数应用程序的参数。 (这是一个古老的优化,可以追溯到 Prolog 编译器的早期 - 我认为 Warren Abstract Machine 的 David Warren 是第一个发现它的人)。

但是,请注意,此类优化不太适合惰性语言。像 Haskell 这样的语言有非常不同的评估模型,其中尾调用并不那么重要。在 Haskell 中,可能需要一个包含递归调用的构造函数应用程序,因为它会阻止立即评估递归调用并允许延迟消耗计算。请参阅this HaskellWiki page 的讨论。

下面是一个使用严格语言进行模数优化的候选示例:

let rec map f = function
  | [] -> []
  | x::xs -> f x::map f xs

在这个 OCaml 函数中,对 map 的递归调用是位于尾部位置的构造函数应用程序的参数,因此可以应用模数优化。 (OCaml 还没有做这个优化,虽然有一些实验性的补丁浮动。)

转换后的函数可能类似于以下伪 OCaml。请注意,内部循环是尾递归的,并且通过改变前面的缺点来工作:

let rec map f = function
  | [] ->
  | x::xs ->
    let rec loop cons = function
      | [] -> cons.[1] <- []
      | y::ys ->
        let new_cons = f y::NULL in
        cons.[1] <- new_cons;
        loop new_cons ys in
    loop (f x::NULL) xs

(其中 NULL 是一些 GC 不会阻塞的内部值。)

在 Lisp 中通过不同的机制进行尾部攻击也很常见:突变往往是显式编程的,而混乱的细节隐藏在诸如 loop 之类的宏中。

如何推广这些方法是一个有趣的问题。

【讨论】:

    猜你喜欢
    • 2015-08-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-02
    • 1970-01-01
    • 1970-01-01
    • 2016-03-21
    • 2017-03-09
    相关资源
    最近更新 更多