我关注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
只是在调用它的引用?
我明白了吗?
答案 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
应该向您显示以下内容:
在这里你可以看到x + x
thunk实际上指向了x
thunk的两倍。
现在,如果您评估x
,请打印它,例如:
λ Prelude> print x
您将获得:
你可以在这里看到x
thunk不再是thunk:它的值是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支持严格和懒惰的评估。但是它默认支持延迟评估。要启用严格评估,您必须使用特殊的seq
和deepSeq
函数。
同样,您可以使用JavaScript等严格的语言进行延迟评估。但是,您需要跟踪表达式是否已被评估。您可以研究如何用JavaScript或类似语言实现thunk。