我对Haskell的懒惰评估感到困惑

时间:2014-05-23 04:07:24

标签: haskell lazy-evaluation

我关注Haskell懒惰评估的效率。 考虑以下代码

main = print $ x + x
   where x = head [1..]

此处,由于懒惰,x首先保留head [1..]而不是结果1的表达式, 但是当我打电话给x + x时,表达式head [1..]会被执行两次吗?

我在haskell.org上找到了以下描述

  另一方面,懒惰评估意味着仅在需要其结果时评估表达式(注意从"减少"到#34;评估"的转变)。因此,当评估引擎看到一个表达式时,它会构建一个thunk数据结构,其中包含评估表达式所需的任何值,以及指向表达式本身的指针。当实际需要结果时,评估引擎调用表达式,然后将thunk替换为结果以供将来参考。

这是否意味着,在x + x中,在调用第一个x时,会执行head [1..]并将x重新分配给1,而第二个x只是在调用它的引用?

我明白了吗?

3 个答案:

答案 0 :(得分:11)

这是关于特定Haskell实现的问题,而不是关于Haskell本身的问题,因为该语言对事物的评估方式没有特别的保证。

但是在GHC(以及大多数其他实现中,据我所知):是的,当评估thunks时,它们会被内部结果替换,因此对同一thunk的其他引用会从评估它的工作中受益第一次。

需要注意的是,对于哪些表达式最终实现为对同一个thunk的引用,并没有真正的保证。一般情况下,只要结果相同,编译器就可以对它喜欢的代码进行任何转换。当然,在编译器中实现代码转换的原因通常是尝试使代码更快,因此希望不会以使其变得更糟的方式重写事物,但它永远不会是完美的。

在实践中,你通常会非常安全地假设每当你给一个表达式命名时(如where x = head [1..]),那么所有对该名称的使用(在绑定范围内)都将引用一个thunk。

答案 1 :(得分:9)

首先,x只是thunk。你可以看到如下:

λ Prelude> let x = head [1..]
λ Prelude> :sprint x
x = _

此处_表示尚未评估x。仅记录其定义。

然后,您可以通过仅仅意识到x + x是指向此thunk的指针来理解x是如何构造的:两个x都将指向同一个thunk。一旦被评估,另一个是,因为它是相同的thunk。

您可以使用ghc-vis

查看
λ Prelude> :vis
λ Prelude> :view x
λ Prelude> :view x + x

应该向您显示以下内容:

thunk

在这里你可以看到x + x thunk实际上指向了x thunk的两倍。

现在,如果您评估x,请打印它,例如:

λ Prelude> print x

您将获得:

evaluated

你可以在这里看到x thunk不再是thunk:它的值是1。

答案 2 :(得分:3)

评估表达式有两种方法:

  1. 懒惰(首先评估最外层)。
  2. 严格(评估最里面的第一个)。
  3. 考虑以下功能:

    select x y z = if x > z then x else y
    

    现在让我们来称呼它:

    select (2 + 3) (3 + 4) (1 + 2)
    

    如何评估?

    严格评估:首先评估最内层。

    select (2 + 3) (3 + 4) (1 + 2)
    
    select 5 (3 + 4) (1 + 2)
    
    select 5 7 (1 + 2)
    
    select 5 7 3
    
    if 5 > 3 then 5 else 7
    
    if True then 5 else 7
    
    5
    

    严格的评估减少了6次。要评估select,我们首先必须评估其论点。在严格的评估中,始终完全评估函数的参数。因此,函数是"按值调用"。因此,没有额外的簿记。

    懒惰评估:首先评估最外层。

    select (2 + 3) (3 + 4) (1 + 2)
    
    if (2 + 3) > (1 + 2) then (2 + 3) else (3 + 4)
    
    if 5 > (1 + 2) then 5 else (3 + 4)
    
    if 5 > 3 then 5 else (3 + 4)
    
    if True then 5 else (3 + 4)
    
    5
    

    懒惰评估只减少了5次。我们从未使用(3 + 4),因此我们从未评估过它。在惰性求值中,我们可以在不评估其参数的情况下评估函数。参数仅在需要时进行评估。因此,功能是按需要调用"。

    然而"按需要呼叫"评估策略需要额外的簿记 - 您需要跟踪表达式是否已被评估。在上面的表达式中,当我们评估x = (2 + 3)时,我们不需要再次评估它。但是,我们需要跟踪它是否被评估。


    Haskell支持严格和懒惰的评估。但是它默认支持延迟评估。要启用严格评估,您必须使用特殊的seqdeepSeq函数。

    同样,您可以使用JavaScript等严格的语言进行延迟评估。但是,您需要跟踪表达式是否已被评估。您可以研究如何用JavaScript或类似语言实现thunk。