我认为这里有些混乱,因为“懒惰”一词有时用于两种不同的上下文。
- 惰性语义(相对于渴望语义)
- 惰性函数(实际上应该称为非严格函数)
关于惰性与渴望语义:考虑这个表达式
(\x -> 42) (error "urk!")
上面的求值,结果是什么?
根据 eager 语义,我们在调用函数之前评估参数。结果将是运行时错误。
根据惰性语义,我们立即调用函数。这个过程可以从操作上和外延上理解如下。
在操作上,它被传递一个 thunk,一个描述尚未评估的参数的对象,并且每当需要参数 x 时,它将被“强制”(评估)。我们可以假设x 指向未求值的表达式error "urk!",它会在需要x 时求值。
在表示上,我们使用了一个数学技巧:我们用一个称为“底部”的特殊值来表示错误,并说error "urk!" 具有这样的底部值。
然后,我们简单地假设这个异常值可以被传递。在上面的函数调用中,x 将绑定到“底部”,就好像它是一个正常值一样。这可以说更简单,因为我们不需要关注表达式是如何计算的,而只需要关注底部是如何传播的。
更准确地说,我们让“底部”表示运行时错误和非终止(无限递归),这两者都让程序能够返回实际结果。
例如,我们有if bottom then .. else .. 将始终产生底部。对于底部的bottom + 4 也是如此。同样,case bottom of SomeConstructor -> ...; ... 是底部(好吧,除了newtypes,但让我们忽略这一点)。相反,f bottom 根据f 所做的事情可能会或可能不会是底部:如果它需要参数,结果将是底部。
关于惰性(非严格)函数。如果 f bottom 是底部函数,则函数 f 有时被称为“惰性”(或者,更准确地说,是非严格函数)。
例如:
f x = x+1 -- strict / non lazy
f x = 5 -- non strict / lazy
head xs = case xs of -- strict / non lazy
[] -> error "head: empty list"
(x:xs) -> x
g x = (x,True) -- non strict / lazy
所以,由于head bottom 是底部的case bottom of ...,head 并不懒惰。在操作上,由于head 在产生结果之前要求它的参数,它是严格/非懒惰的。
关于g:惰性语义的一个主要特点是data 构造函数,如对构造函数(,),本质上是惰性的。那就是(bottom, 4) 与底部不同:这使得snd (bottom, 4) = 4 成为可能,即使第一对组件是“错误”值。
所以,g bottom = (bottom, True) 不是底部,我们可以应用snd 提取True 而不会触发错误。