为什么这个递归函数没有被优化? (Haskell的)

时间:2014-05-27 15:25:30

标签: haskell recursion

我写了自己的总和' Haskell中的函数:

mySum [a] = a
mySum (a:as) = a + mySum as

并用

进行测试
main = putStrLn . show $ mySum [1 .. 400000000]

仅收到堆栈溢出错误。

以相同的方式使用Prelude的总和:

main = putStrLn . show $ sum [1 .. 400000000]

我没有堆栈溢出。

这可能是我正在评估的巨大列表,特别是如果传递给我的函数的列表正在严格评估,尽管我不怀疑这个的唯一理由是使用Prelude∑列表我没有错误。

3 个答案:

答案 0 :(得分:10)

您的函数因堆栈溢出而失败,因为它不是tail recursive。每次调用时都会消耗一个堆栈帧,并保持每个步骤的“a”的部分和。

Prelude's sum是通过尾调用实现的:

sum     l       = sum' l 0
   where
    sum' []     a = a
    sum' (x:xs) a = sum' xs (a+x)

{-# SPECIALISE sum     :: [Int] -> Int #-}
{-# SPECIALISE sum     :: [Integer] -> Integer #-}
{-# INLINABLE sum #-}

不消耗堆栈空间。

请注意,specialize和inline编译指示是公开严格性信息所必需的,该信息使“a”累加器可以安全地使用而不会累积thunk。更现代的版本是:

    sum' []     !a = a
    sum' (x:xs) !a = sum' xs (a+x)

明确严格性假设。这almost等同于foldl'版本。严格。

答案 1 :(得分:4)

您可能希望编译器对您的方法执行尾调用优化。不幸的是,mySum的这个定义不是尾随可调优化的。它需要的是被调用的最后一个函数是递归调用,所以在这种情况下,你希望mySum成为最后一个被调用的函数。但是,定义中调用的最后一个函数是(+),而不是mySum。您可以改为将其写为@DonStewart建议的,在我能够之前设法输入该解决方案。

答案 2 :(得分:3)

编辑:我刚刚意识到这个问题是重复的,我基本上已经从Does Haskell have tail-recursive optimization?重新发明了答案

GHC使用所谓的lazy evaluation来评估表达式。对于此讨论,懒惰评估的最相关特征是所谓的“最左侧,最外层评估”或normal-order evaluation。要查看正常的顺序评估,让我们按照sum的两个实现,foldr实现和foldl实现的评估:

foldr (+) 0 (1:2:3:[])
1 + foldr (+) 0 (2:3:[])
1 + (2 + foldr (+) 0 (3:[]))
1 + (2 + (3 + foldr (+) 0 [])))
1 + (2 + (3 + 0))
1 + (2 + 3)
1 + 5
6

请注意,因为对foldr的递归调用不是最左边的,所以最外层的延迟评估不能减少它。但是,因为(+)在其第二个参数中是严格的,所以将评估右侧,留下一系列添加。因为对(+)的调用是最左边的,所以这个实现类似于sum的实现。

人们经常听说,由于尾递归,foldl效率更高,但是它是这样吗?

foldl (+) 0 (1:2:3:[])
foldl (+) (0+1) (2:3:[])
foldl (+) ((0+1)+2) (3:[])
foldl (+) (((0+1)+2)+3) []
((0+1)+2)+3
(1+2)+3
3+3
6

请注意一些差异。首先,对foldl的递归调用是最左边的,因为它通过正常顺序评估减少,所以不会占用堆栈上的任何额外空间。但是,对(+)的调用不会减少而执行会占用堆栈空间。这足以说服你“尾递归”不足以防止GHC中的空间泄漏。

因此,我们可以使用尾部位置调用来防止表示对foldl(或Don的版本中的sum)的调用的thunk的积累,但是我们如何防止(+)的thunks的积累?我们可以使用严格注释,或让foldl'为我们添加:

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

请注意,这需要不断的堆栈空间常量堆空间。

总之,如果你的递归调用是最左边的,最外面的(对应于尾部位置),它们可以通过延迟评估来减少。这是必要的,但不足以阻止评估递归函数使用O(n)堆栈和堆空间。 foldlfoldr样式递归本身都需要O(n)堆栈和堆空间。 foldl - 样式递归对累积参数进行严格注释是获得评估以在恒定空间中操作所必需的。