length :: [a] -> Int
的规范实施是:
length [] = 0
length (x:xs) = 1 + length xs
非常漂亮但是因为它使用线性空间而遭受堆栈溢出。
尾递归版:
length xs = length' xs 0
where length' [] n = n
length' (x:xs) n = length xs (n + 1)
没有遇到这个问题,但我不明白这是如何在懒惰的语言中以恒定的空间运行的。
运行时是否在运行列表时累积了大量(n + 1)
个thunks?不应该这个函数Haskell消耗O(n)空间并导致堆栈溢出吗?
(如果重要的话,我正在使用GHC)
答案 0 :(得分:15)
是的,你在累积参数方面遇到了常见的陷阱。通常的办法是对累积参数进行严格评估;为此,我喜欢严格的应用程序运算符$!
。如果你不强制严格,GHC的优化器可能会认为这个函数是严格的,但它可能不是。绝对不是依赖它 - 有时你想要累积参数被懒惰地评估,O(N)空间就好了,谢谢。
如何在Haskell中编写一个常量空间长度函数?
如上所述,使用严格的应用程序运算符强制评估累积参数:
clength xs = length' xs 0
where length' [] n = n
length' (x:xs) n = length' xs $! (n + 1)
$!
的类型为(a -> b) -> a -> b
,它会在应用函数之前强制评估a
。
答案 1 :(得分:12)
在GHCi中运行第二个版本:
> length [1..1000000]
*** Exception: stack overflow
所以回答你的问题:是的,它确实会遇到这个问题,就像你期望的那样。
然而,GHC比普通编译器更聪明;如果您使用优化结果编译,它将为您修复代码并使其在恒定空间中工作。
更一般地说,有一些方法可以在Haskell代码中的特定点强制 strictness ,从而阻止构建深层嵌套的thunk。通常的示例是foldl
与foldl'
:
len1 = foldl (\x _ -> x + 1) 0
len2 = foldl' (\x _ -> x + 1) 0
除了foldl
是惰性而foldl'
是严格的以外,两个函数都是左边的折叠,它们执行“相同”的操作。结果是len1
在GHCi中因堆栈溢出而死亡,而len2
正常工作。
答案 2 :(得分:1)
尾递归函数不需要维护堆栈,因为函数返回的值只是尾调用返回的值。因此,不是创建一个新的堆栈帧,而是重新使用当前的堆栈帧,本地数据将被传入尾调用的新值覆盖。因此,每个n+1
都会写入旧n
所在的相同位置,并且您可以持续使用空间。
编辑 - 实际上,正如你所写的那样,你是对的,它会使(n+1)
重击并导致溢出。易于测试,只需尝试length [1..1000000]
..你可以通过强制它首先评估它来解决这个问题:length xs $! (n+1)
,然后就像我上面说的那样工作。