(++)运算符和(:)运算符以及惰性求值

时间:2011-06-03 01:58:42

标签: lazy-evaluation haskell

RealWorldHaskell的第8章

globToRegex' (c:cs) = escape c ++ globToRegex' cs

这个函数不是尾递归的,它说答案依赖于Haskell非严格(懒惰)评估策略。 (++)运算符的简单定义如下,并不是尾递归。

(++) :: [a] -> [a] -> [a]

(x:xs) ++ ys = x : (xs ++ ys)
[]     ++ ys = ys
     

在严格的语言中,如果我们评估"foo" ++ "bar",则构造整个列表,然后返回。在严格的评估需要之前,非严格评估会将大部分工作推迟。

     

如果我们要求表达式"foo" ++ "bar"的元素,函数定义的第一个模式匹配,我们返回表达式x : (xs ++ ys) 因为(:)构造函数是非严格的,所以xs ++ ys的评估可以推迟:我们以任何需要的速率生成更多结果元素。当我们生成更多结果时,我们将不再使用x,因此垃圾收集器可以回收它。由于我们按需生成结果元素,并且不保留我们完成的部分,因此编译器可以在恒定空间中评估我们的代码

(强调补充。)

上面粗体的解释对Haskell来说是必不可少的,但是

  1. 我们怎么理解?
  2. 底层发生了什么?
  3. x:(xs ++ ys)会在恒定的空间中评估”,怎么样?这听起来像尾递归一样!

3 个答案:

答案 0 :(得分:8)

请记住,"foo"只是'f':'o':'o':[]的语法糖。

也就是说,String只是[Char]的别名,它只是一个链接的字符列表。

当客户端代码消费链接列表时,它会将其分解为头尾(例如x:xs),对头部做某事(如果需要),然后进行递归为尾巴。

当您的代码构建链接列表时,由于延迟评估,所有它需要做的就是返回 thunk 或承诺它将返回链接列表要求。当头被解除引用时,它是按需提供的,并且尾部留作列表其余部分的承诺。

应该很容易看出,只要列表没有被复制或以其他方式存储,每个thunk将被使用一次然后被丢弃,以便整个存储空间是恒定的。

许多严格的语言暴露了一种机制(通常称为生成器)来完成相同类型的惰性列表生成,但是对于懒惰的评估,这些功能作为语言的一部分“免费”出现 - 实质上,所有Haskell列表都是生成器!

答案 1 :(得分:7)

与其他FP语言相比,依赖于延迟评估而不是尾递归是Haskell的一个特征。两者在限制内存使用方面发挥相关作用;哪一种是适当的机制取决于所产生的数据。

如果您的输出可以逐步消耗,那么您应该更喜欢利用延迟评估,因为输出只会在需要时生成,从而限制堆消耗。如果你急切地构造输出,那么你将自己辞去使用堆,但至少可以通过尾递归来保存堆栈。

如果您的输出无法逐步消耗 - 也许您正在计算Int - 那么懒惰可能会让您留下一堆不需要的东西,其评估将会破坏您的筹码。在这种情况下,需要调用严格的累加器和尾递归。

所以,如果你渴望你可能会浪费堆构建一个大数据结构。如果你很懒,你可以将简化(例如将1 + 1减少到2)推迟到堆中,但最终只会在支付吹笛者时玷污你的筹码。

要玩硬币的两面,请考虑foldl'foldr

答案 2 :(得分:5)

尾递归会使堆栈保持不变,但是在严格的语言中,堆会随着x : (xs ++ ys)的计算而增长。在Haskell中,因为它是非严格的,所以在计算下一个值之前将释放x(除非调用者不必要地持有对x的引用),因此堆保持不变。