我知道Haskell中的尾递归由于它的懒惰而受到一些困难。也就是说,在Haskell中使用尾递归是否明智?
答案 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。