如何在Haskell中编写一个恒定空间长度函数?

时间:2010-05-06 00:39:41

标签: haskell lazy-evaluation

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)

3 个答案:

答案 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。通常的示例是foldlfoldl'

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),然后就像我上面说的那样工作。