简体   繁体   English

Haskell中的尾递归

[英]Tail Recursion in Haskell

I am trying to understand tail-recursion in Haskell. 我试图理解Haskell中的尾递归。 I think I understand what it is and how it works but I'd like to make sure I am not messing things up. 我想我明白它是什么以及它是如何工作的但是我想确保我没有搞砸了。

Here is the "standard" factorial definition: 这是“标准”因子定义:

factorial 1 = 1
factorial k = k * factorial (k-1)

When running, for example, factorial 3 , my function will call itself 3 times(give it or take). 在运行时,例如, factorial 3 ,我的函数将自己调用3次(给它或者拿它)。 This might pose a problem if I wanted to calculate factorial 99999999 as I could have a stack overflow. 如果我想计算因子99999999,这可能会产生问题,因为我可能有堆栈溢出。 After I get to factorial 1 = 1 I will have to "come back" in stack and multiply all the values, so i have 6 operations (3 for calling the function itself and 3 for multiplying the values). 在得到factorial 1 = 1我将不得不在堆栈中“返回”并乘以所有值,因此我有6个操作(3个用于调用函数本身,3个用于乘以值)。

Now I present you another possible factorial implementation: 现在我向您介绍另一种可能的因子实现:

factorial 1 c = c
factorial k c = factorial (k-1) (c*k)

This one is recursive, too. 这个也是递归的。 It will call itself 3 times. 它会称自己为3次。 But it doesn't have the problem of then still having to "come back" to calculate the multiplications of all the results, as I am passing already the result as argument of the function. 但它没有问题,然后仍然必须“回来”计算所有结果的乘法,因为我已经将结果作为函数的参数传递。

This is, for what I've understood, what Tail Recursion is about. 根据我的理解,这就是Tail Recursion的内容。 Now, it seems a bit better than the first, but you can still have stack overflows as easily. 现在,它似乎比第一个好一点,但你仍然可以轻松地拥有堆栈溢出。 I've heard that Haskell's compiler will convert Tail-Recursive functions into for loops behind the scenes. 我听说Haskell的编译器会在后台将Tail-Recursive函数转换为for循环。 I guess that is the reason why it pays off to do tail recursive functions? 我想这就是为什么它能够为尾递归功能付出代价呢?

If that is the reason then there is absolutely no need to try to make functions tail recursive if the compiler is not going to do this smart trick -- am I right? 如果这就是原因,那么如果编译器不打算做这个聪明的技巧,那么绝对没有必要尝试使函数尾递归 - 我是对的吗? For example, although in theory the C# compiler could detect and convert tail recursive functions to loops, I know (at least is what I've heard) that currently it doesn't do that. 例如,虽然理论上C#编译器可以检测并将尾递归函数转换为循环,但我知道(至少是我所听到的)目前它没有这样做。 So there is absolutely no point in nowadays making the functions tail recursive. 所以现在绝对没有必要使函数尾递归。 Is that it? 是吗?

Thanks! 谢谢!

There are two issues here. 这里有两个问题。 One is tail recursion in general, and the other is how Haskell handles things. 一个是尾递归,另一个是Haskell处理事物的方式。

Regarding tail recursion, you seem to have the definition correct. 关于尾递归,您似乎有正确的定义。 The useful part is, because only the final result of each recursive call is needed, earlier calls don't need to be kept on the stack. 有用的部分是,因为只需要每次递归调用的最终结果,所以不需要在堆栈上保留较早的调用。 Instead of "calling itself" the function does something closer to "replacing" itself, which ends up pretty much looking like an iterative loop. 该函数不是“自称”,而是更接近于“替换”自身,最终看起来就像一个迭代循环。 This is a pretty straightforward optimization that decent compilers will generally provide. 这是一个非常直接的优化,正常的编译器通常会提供。

The second issue is lazy evaluation . 第二个问题是懒惰评估 Because Haskell only evaluates expression on an as-needed basis, by default tail recursion doesn't quite work the usual way. 因为Haskell只根据需要计算表达式,所以默认情况下尾递归并不像通常那样工作。 Instead of replacing each call as it goes, it builds up a huge nested pile of "thunks", that is, expressions whose value hasn't been requested yet. 它不是替换每个调用,而是构建一个巨大的嵌套“thunk”堆,即尚未请求其值的表达式。 If this thunk pile gets big enough, it will indeed produce a stack overflow. 如果这个thunk堆足够大,它确实会产生堆栈溢出。

There are actually two solutions in Haskell, depending on what you need to do: Haskell实际上有两个解决方案,具体取决于您需要做什么:

  • If the result consists of nested data constructors--like producing a list--then you want to avoid tail recursion; 如果结果由嵌套数据构造函数组成 - 比如生成一个列表 - 那么你想避免尾递归; instead put the recursion in one of the constructor fields. 而是将递归放在其中一个构造函数字段中。 This will let the result also be lazy and won't cause stack overflows. 这将使结果也变得懒惰并且不会导致堆栈溢出。

  • If the result consists of a single value, you want to evaluate it strictly , so that each step of the recursion is forced as soon as the final value is needed. 如果结果由单个值组成,则需要严格评估它,以便在需要最终值时立即强制递归的每个步骤。 This gives the usual pseudo-iteration you'd expect from tail recursion. 这给出了通常的尾递归假迭代。

Also, keep in mind that GHC is pretty darn clever and, if you compile with optimizations, it will often spot places where evaluation should be strict and take care of it for you. 另外,请记住,GHC非常聪明,如果您使用优化进行编译,它通常会发现评估应该严格的地方并为您处理。 This won't work in GHCi, though. 但是,这在GHCi中不起作用。

You should use the built-in mechanisms, then you don't have to think about ways to make your function tail-recursive 您应该使用内置机制,然后您不必考虑使函数尾递归的方法

fac 0 = 1
fac n = product [1..n]

Or if product weren't already defined: 或者,如果产品尚未定义:

fac n = foldl' (*) 1 [1..n]

(see http://www.haskell.org/haskellwiki/Foldr_Foldl_Foldl%27 about which fold... version to use) (请参阅http://www.haskell.org/haskellwiki/Foldr_Foldl_Foldl%27关于哪个折叠...使用的版本)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM