尾递归,我应该在Haskell中使用它

时间:2013-11-03 04:31:06

标签: haskell recursion tail-recursion

我知道Haskell中的尾递归由于它的懒惰而受到一些困难。也就是说,在Haskell中使用尾递归是否明智?

2 个答案:

答案 0 :(得分:7)

与往常一样,这些重大问题的答案是“它取决于”。

当您以懒惰的方式编程时,您通常可以通过简单的递归来逃避,例如

 map f (x:xs) = f x : map f xs
 map _ []     = []

实际上是如何在标准库中定义map。这很好,因为尾递归函数产生的结果通常不能很好地延迟,如果你执行类似head . map (+1) $ [1..]的操作,尾递归映射将不会终止。

然而,有时我们不想懒惰。经典的例子是当我们太懒而且开始建立我们真正想要评估的thunk时,比如总结一个列表。

sum xs = someFold (+) 0 xs

既然+对于它的参数和关联是严格的,那么没有理由使用foldr但是foldl'是完美的,它是尾递归的并且严格评估,这提供了显着的空间改进。 '很重要,在许多尾递归函数中,有一个累加器在每个递归步骤上被修改,foldl'将强制它的评估,这会阻止我们的尾递归函数过于懒惰并构建thunks。 / p>

故事的寓意,当你懒洋洋地编程并产生懒惰的数据结构时,尾递归并不是什么大不了的事。但是当你严格编程并在两个参数中都使用函数strict时,尾递归对于保持良好的性能非常重要。

答案 1 :(得分:7)

尾部递归在Haskell中并不像在严格的语言中那样直接或大得多。通常,您应该专注于编写高效函数。例如,foldr通常很有效率

foldr f z []     = z
foldr f z (x:xs) = x `f` foldr f z xs

如果组合函数f能够懒惰地产生部分结果,那么消耗foldr结果的任何东西也可以懒得要求它们。一个典型的例子是“foldr identity”

foldr (:) [] [1,2,3]          -- "force" it once
1 : {{ foldr (:) [] [2,3] }}

{{...}}是一个懒惰的thunk。如果foldr的调用上下文是head,那么我们就完成了

head (foldr (:) [] [1,2,3])
head (1 : {{ foldr (:) [] [2,3] }})
1

但是,如果f中的foldr是严格的,则foldr可以线性创建多个调用帧

foldr (+) 0 [1,2,3]
1 + {{ foldr (+) 0 [2,3] }}           -- we know it's one more than *something*
1 + (2 + {{ foldr (+) 0 [3] }})       -- ...
1 + (2 + (3 + {{ foldr (+) 0 [] }}))
1 + (2 + (3 + 0))                     -- and now we can begin to compute
1 + (2 + 3)
1 + 5
6

虽然foldl'允许严格组合功能立即运行

foldl' f z []     = z
foldl' f z (x:xs) = let z' = f z x in z' `seq` foldl' f z' xs

其中seq噪声迫使Haskell像这样评估

foldl' (+) 0 [1,2,3]
foldl' (+) (1+0) [2,3]
foldl' (+) 1 [2,3]
foldl' (+) (1+2) [3]
foldl' (+) 3 [3]
foldl' (+) (3+3) []
foldl' (+) 6 []
6

这看起来很像尾递归调用。

有关详细信息,请参阅the wiki