Haskell中的尾递归

dev*_*ium 18 recursion haskell tail-recursion

我试图理解Haskell中的尾递归.我想我明白它是什么以及它是如何工作的但是我想确保我没有搞砸了.

这是"标准"因子定义:

factorial 1 = 1
factorial k = k * factorial (k-1)
Run Code Online (Sandbox Code Playgroud)

例如,在运行时,factorial 3我的函数会自行调用3次(给它或者拿它).如果我想计算因子99999999,这可能会产生问题,因为我可能有堆栈溢出.在我到达之后,factorial 1 = 1我将不得不在堆栈中"返回"并将所有值相乘,因此我有6个操作(3个用于调用函数本身,3个用于乘以值).

现在我向您介绍另一种可能的因子实现:

factorial 1 c = c
factorial k c = factorial (k-1) (c*k)
Run Code Online (Sandbox Code Playgroud)

这个也是递归的.它会称自己为3次.但它没有问题,然后仍然必须"回来"计算所有结果的乘法,因为我已经将结果作为函数的参数传递.

根据我的理解,这就是Tail Recursion的内容.现在,它似乎比第一个好一点,但你仍然可以轻松地拥有堆栈溢出.我听说Haskell的编译器会在后台将Tail-Recursive函数转换为for循环.我想这就是为什么它能够为尾递归功能付出代价呢?

如果这就是原因,那么如果编译器不打算做这个聪明的技巧,那么绝对没有必要尝试使函数尾递归 - 我是对的吗?例如,虽然理论上C#编译器可以检测并将尾递归函数转换为循环,但我知道(至少是我所听到的)目前它没有这样做.所以现在绝对没有必要使函数尾递归.是吗?

谢谢!

C. *_*ann 36

这里有两个问题.一个是尾递归,另一个是Haskell处理事物的方式.

关于尾递归,您似乎有正确的定义.有用的部分是,因为只需要每次递归调用的最终结果,所以不需要在堆栈上保留较早的调用.该函数不是"自称",而是更接近于"替换"自身,最终看起来就像一个迭代循环.这是一个非常直接的优化,正常的编译器通常会提供.

第二个问题是懒惰评估.因为Haskell只根据需要计算表达式,所以默认情况下尾递归并不像通常那样工作.它不是替换每个调用,而是构建一个巨大的嵌套"thunk"堆,即尚未请求其值的表达式.如果这个thunk堆足够大,它确实会产生堆栈溢出.

Haskell实际上有两个解决方案,具体取决于您需要做什么:

  • 如果结果由嵌套数据构造函数组成 - 比如生成列表 - 那么你想避免尾递归; 而是将递归放在其中一个构造函数字段中.这将使结果也变得懒惰并且不会导致堆栈溢出.

  • 如果结果由单个值组成,则需要严格评估它,以便在需要最终值时立即强制递归的每个步骤.这给出了尾部递归所期望的通常的伪迭代.

另外,请记住,GHC非常聪明,如果您使用优化进行编译,它通常会发现评估应该严格的地方并为您处理.但是,这在GHCi中不起作用.

  • @rsenna:从某种意义上说,我不是一个学者.我不做研究,我只是编写程序来完成工作.我学习了新的抽象概念,因为它们扩展了我思考程序结构的能力,并且我使用工具来自动化任何我能做到的事情.你似乎在说忽视抽象和瘫痪工具......对于"现实世界"来说更好吗?我真的不明白. (5认同)
  • @rsenna:我不会称之为毫无意义,只是在最简单案例的优化版本是语言原语时更容易解决.TCO仍然是严格优越的,因为你可以有两个函数相互调用或更复杂的东西. (4认同)
  • @rsenna:我不完全确定你在倡导什么.您是否说命令式语言编译器不应该实现TCO,因为性能损失会阻止用户编写递归函数,或者您认为命令式语言不应该允许递归函数?或者是其他东西? (3认同)
  • @rsenna:呃,我真的没有看到如何省去编译器优化会让语言变得更加混乱?而"只做一种方法"的事情很难被认真对待,因为如果我们选择单一的迭代技术,一般递归是比大多数语言具有的随机各种循环的更好的选择. (2认同)

Lan*_*dei 7

您应该使用内置机制,然后您不必考虑使函数尾递归的方法

fac 0 = 1
fac n = product [1..n]
Run Code Online (Sandbox Code Playgroud)

或者,如果产品尚未定义:

fac n = foldl' (*) 1 [1..n]
Run Code Online (Sandbox Code Playgroud)

(请参阅http://www.haskell.org/haskellwiki/Foldr_Foldl_Foldl%27关于哪个折叠...使用的版本)

  • 你意识到没有优化,`foldl`会导致大型列表上的堆栈溢出,对吧?在上下文中似乎有点讽刺. (8认同)
  • 这是一条经验法则:`foldl`几乎不是一个可以使用的. (5认同)
  • 这似乎不是问题的重点。问题是尾递归是什么。-1 (2认同)
  • camccann 已经给出了一个很好的、足够的答案。我不知道你如何看待这一点,但如果我的问题得到**两个**的回答,以及一些额外的信息、考虑或批评,我总是很高兴。您为什么不直接写一个更有帮助的答案,而不是投反对票呢? (2认同)