Haskell 为什么不是';这个递归函数不是正在优化吗?(哈斯克尔)

Haskell 为什么不是';这个递归函数不是正在优化吗?(哈斯克尔),haskell,recursion,Haskell,Recursion,我用Haskell编写了自己的“sum”函数: mySum [a] = a mySum (a:as) = a + mySum as 并用计算机进行了测试 main = putStrLn . show $ mySum [1 .. 400000000] 仅接收堆栈溢出错误 以同样的方式使用前奏曲的总和: main = putStrLn . show $ sum [1 .. 400000000] 我没有堆栈溢出 这可能是我正在评估的巨大列表,特别是如果传递给我的函数的列表被严格评估,尽管我不怀疑

我用Haskell编写了自己的“sum”函数:

mySum [a] = a
mySum (a:as) = a + mySum as
并用计算机进行了测试

main = putStrLn . show $ mySum [1 .. 400000000]
仅接收堆栈溢出错误

以同样的方式使用前奏曲的总和:

main = putStrLn . show $ sum [1 .. 400000000]
我没有堆栈溢出


这可能是我正在评估的巨大列表,特别是如果传递给我的函数的列表被严格评估,尽管我不怀疑这一点的唯一原因是使用相同列表的Prelude总和不会出错。

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

通过尾部调用实现:

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 pragmas对于公开严格性信息是必需的,这些严格性信息使“a”累加器在不累积thunks的情况下可以安全使用。更现代的版本是:

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

明确严格的假设。这相当于
foldl'
wrt版本。严格性。

您可能希望编译器对您的方法执行尾部调用优化。不幸的是,
mySum
的这个定义不是尾部调用优化的。它需要的是最后一个被调用的函数是递归调用,因此在本例中,您希望
mySum
是最后一个被调用的函数。但是,定义中调用的最后一个函数是
(+)
,而不是
mySum
。你可以按照@DonStewart的建议写出来,他在我能写出来之前就把答案打出来了。

编辑:我刚刚意识到这个问题是重复的,我基本上是从

GHC使用已知的表达式计算表达式。在本讨论中,惰性评估最相关的特性是所谓的“最左边、最外面的评估”或。要查看正常顺序评估的实际效果,让我们看一下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中的空间泄漏

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

foldl' (+) 0 (1:2:3:[])
foldl' (+) 1 (2:3:[])
foldl' (+) 3 (3:[])
foldl' (+) 6 []
6
请注意,这需要恒定的堆栈空间和恒定的堆空间


总之,如果递归调用是最左边的,最外面的(对应于尾部位置)可以通过延迟求值来减少。这对于防止递归函数的计算使用O(n)堆栈和堆空间是必要的,但还不够
foldl
foldr
风格的递归本身都占用了O(n)堆栈和堆空间
foldl
-必须对累积参数进行严格注释,才能使计算在恒定空间中运行。

您知道为什么sum会这样实现吗?更具体地说,我认为
foldl'(+)0
是自然实现。在ghci中,它似乎也能更好地工作,因为实际的总和对我来说是溢出的,但这一个不会。现在我们有了很好的内联和专门化(以及左折叠的融合),没有理由不使用
fold'(+)
。情况并非总是如此。完整的定义包括一个折叠。除非在
sum'
中对
a
进行严格限制,否则使用此
sum
绝对会出现堆栈溢出错误。库的实现当然依赖于严格性分析和专业化。最好是在累加器上敲一下。这个问题是的重复,所以也可以在那里寻找答案。不过它是TR modulo
(+)
。Friedman和Wise在1974年描述了这一转变,当时他们还提到了“累积和关联构造函数,如整数乘法…[如]阶乘”(
+
显然也适用)。没有“thunks的积累”在您的
foldr
示例中:为下一个尾部创建的每个新thunk都会被严格的
+
构造函数立即消耗。也就是说,这是一堆东西的收尾。在
foldl
示例中,确实存在(嵌套的)thunks的构建-由
{u2}
引用的
{0+1}
,以及一个接一个的
{u3}
。然后,当到达
[]
时,强制执行最外层的thunk,这会导致创建求和堆栈
{uU3}={uU2}+3=({0+1}+2)+3=(1+2)+3=…
当然,具有非严格组合函数的foldr本身就是O(1)谢谢,威尔。更新该部分以修复。