在阅读Guido's reasoning for not adding tail recursion elimination to Python时,我在Haskell中编写了几乎尾递归的例子:
triangle :: Int -> Int
triangle 0 = 0
triangle x = x + triangle (x - 1)
这当然不是尾调用,因为虽然递归调用本身在“return”中,但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 +
in上面的triangle
示例。)
这是否可以在功能背景下推广?一个必要的呢?可以在返回时将所有带递归调用的函数转换为尾部位置返回的函数(即可以尾调用优化的函数)吗?
不要紧,这些都不是在Haskell中计算三角形数的“最佳”方法,AFAIK是triangle x = sum [0..n]
。对于这个问题,代码纯粹是一个人为的例子。
注意:我看过Are there problems that cannot be written using tail recursion?,所以我相信我的问题的答案是肯定的。但是,答案提到了继续传递风格。除非我误解了CPS,否则我的变形triangle'
似乎仍然是直接的风格。在这种情况下,我很好奇这种转变可以直接推广。
答案 0 :(得分:3)
有一个有趣的尾递归 - 模运算符优化空间,它可以转换某些函数,使它们在恒定的空间中运行。可能最着名的是tail-recursion-modulo-cons,其中不完全尾调用是构造函数应用程序的参数。 (这是一个古老的优化,可以追溯到Prolog编译器的早期阶段 - 我认为Warren抽象机器的David Warren是第一个发现它的人)。
但请注意,此类优化不太适合惰性语言。像Haskell这样的语言有非常不同的评估模型,其中尾部调用并不那么重要。在Haskell中,可能需要包含递归调用的构造函数应用程序,因为它将阻止立即评估递归调用并允许计算延迟消耗。请参阅this HaskellWiki page上的讨论。
这是一个严格语言模数优化候选者的例子:
let rec map f = function
| [] -> []
| x::xs -> f x::map f xs
在这个OCaml函数中,map的递归调用是尾部位置的构造函数应用程序的参数,因此可以应用模数优化。 (OCaml尚未进行此优化,尽管周围有一些实验性补丁。)
转换后的函数可能看起来像下面的psuedo-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
之类的宏中。
如何推广这些方法是一个有趣的问题。