我试图了解Haskell中的尾递归。我想我明白它是什么以及它是如何工作的但是我想确保我没有弄乱它。
这是“标准”因子定义:
factorial 1 = 1
factorial k = k * factorial (k-1)
运行时,例如factorial 3
,我的功能会自行调用3次(给它或者拿它)。如果我想计算因子99999999,这可能会产生问题,因为我可能有堆栈溢出。在我到达factorial 1 = 1
后,我将不得不在堆栈中“返回”并将所有值相乘,因此我有6个操作(3个用于调用函数本身,3个用于乘以值)。
现在我向您介绍另一种可能的因子实现:
factorial 1 c = c
factorial k c = factorial (k-1) (c*k)
这个也是递归的。它会称自己为3次。但是它没有必须“回来”计算所有结果的乘法的问题,因为我已经将结果作为函数的参数传递。
根据我的理解,这就是Tail Recursion的内容。现在,它似乎比第一个好一点,但你仍然可以轻松地拥有堆栈溢出。我听说Haskell的编译器会在后台将Tail-Recursive函数转换为for循环。我想这就是为什么它能够为尾递归功能付出代价呢?
如果这就是原因,那么如果编译器不打算做这个聪明的技巧,那么绝对没有必要尝试使函数尾递归 - 我是对的吗?例如,虽然理论上C#编译器可以检测并将尾递归函数转换为循环,但我知道(至少是我所听到的)目前它没有这样做。所以现在绝对没有必要使函数尾递归。是吗?
谢谢!
答案 0 :(得分:36)
这里有两个问题。一个是尾递归,另一个是Haskell处理事物的方式。
关于尾递归,您似乎有正确的定义。有用的部分是,因为只需要每次递归调用的最终结果,所以不需要在堆栈上保留较早的调用。该函数不是“自称”,而是更接近于“替换”自身,最终看起来就像一个迭代循环。这是一个非常直接的优化,正常的编译器通常会提供。
第二个问题是延迟评估。因为Haskell只根据需要计算表达式,所以默认情况下尾递归并不像通常那样工作。它不是替换每个调用,而是构建一个巨大的嵌套“thunk”堆,即尚未请求其值的表达式。如果这个thunk堆足够大,它确实会产生堆栈溢出。
Haskell实际上有两种解决方案,具体取决于您需要做什么:
如果结果由嵌套数据构造函数组成 - 比如生成列表 - 那么您希望避免尾递归;而是将递归放在其中一个构造函数字段中。这将使结果也变得懒惰并且不会导致堆栈溢出。
如果结果由单个值组成,则需要严格评估 ,以便在需要最终值时强制递归的每个步骤。这给出了通常的尾递归假迭代。
另外,请记住,GHC非常聪明,如果您使用优化进行编译,它通常会发现评估应该严格的地方并为您处理。但这在GHCi中不起作用。
答案 1 :(得分:7)
您应该使用内置机制,然后您不必考虑使函数尾递归的方法
fac 0 = 1
fac n = product [1..n]
或者如果产品尚未定义:
fac n = foldl' (*) 1 [1..n]
(请参阅http://www.haskell.org/haskellwiki/Foldr_Foldl_Foldl%27关于使用哪个折叠......版本)