Haskell中let与case的惰性差异及相关技术问题咨询
Haskell相关技术问题解答
1. let与case在惰性求值上的差异
- let绑定:完全惰性,
let x = expr in body中,x只有在body里被实际用到时才会触发expr的求值,且仅求值到满足需求的弱头范式(WHNF)。哪怕expr是无法求值的undefined,只要x未被使用,程序就不会报错。 - case表达式:会先对 scrutinee(
case expr of ...中的expr)求值到弱头范式(WHNF)——即求值到第一个构造器(比如列表的[]或:、代数数据类型的构造器),无论后续模式是否用到该值。如果 scrutinee 是undefined,case会直接抛出错误,哪怕模式只是变量绑定。不过若模式是变量,后续变量的使用仍保持惰性,仅 scrutinee 本身会被求值到WHNF。
核心差异示例:
let x = undefined in print 1:正常输出1,因x未被使用。case undefined of x -> print 1:直接抛出Prelude.undefined错误,因case强制求值undefined到WHNF。
2. 可查阅的官方资料
- 《Haskell 2010 Report》:Haskell语言的官方标准文档,明确定义了let绑定、case表达式的求值语义及惰性求值规则。
- GHC用户指南:针对GHC实现的具体求值行为,尤其是严格性、模式匹配的细节,有专门章节说明。
- GHC Wiki:包含大量关于Haskell求值模型、严格性分析的技术细节,比如模式匹配的严格性规则。
3. 在GHCi中实验验证两者的惰性差异
通过带副作用的表达式或undefined可直观测试:
测试let的惰性
在GHCi中输入:
let x = (putStrLn "Evaluated x!" >> return 5) in print 1
仅输出1,因x未被使用,绑定的IO动作不会执行。
再输入:
let x = (putStrLn "Evaluated x!" >> return 5) in x >> print 1
先输出Evaluated x!再输出1,因x被使用,触发了表达式的求值。
测试case的严格性
输入:
case (putStrLn "Evaluated expr!" >> return 5) of x -> print 1
先输出Evaluated expr!再输出1——case强制将 scrutinee 求值到WHNF,IO动作的WHNF触发了副作用。
用undefined测试:
let x = undefined in print 1 -- 正常输出1 case undefined of x -> print 1 -- 抛出Prelude.undefined错误
模式匹配的深层严格性
测试嵌套模式:
data Foo = Bar Int | Baz String let x = Bar undefined in print 1 -- 正常输出1 case x of Bar y -> print 1 -- 正常输出1(仅求值到Bar构造器,y未被使用) case x of Bar (y+1) -> print 1 -- 抛出错误(需求值y计算y+1)
4. 结合readTQueue代码,case匹配reverse ys触发整个列表严格求值的原因
假设readTQueue的相关代码片段类似:
readTQueue tq = do (readEnd, writeEnd) <- readTVar tq case readEnd of [] -> do let revWrite = reverse writeEnd case revWrite of [] -> retry (x:xs) -> do writeTVar tq (xs, []) return x (x:xs) -> do writeTVar tq (xs, writeEnd) return x
触发严格求值的核心原因:
- reverse的特性:
reverse不是惰性生成列表——它必须遍历整个输入列表,才能生成反转后的第一个元素(原列表的最后一个元素)。 - case的求值要求:
case需要将 scrutinee(reverse ys)求值到WHNF,即确定它是[]还是(:)构造器。要确定构造器,必须完成整个reverse的计算:若ys是无限列表,过程永远不会结束;若ys是有限列表,会遍历所有元素完成反转。
简言之,reverse ys无法生成部分结果供case匹配构造器,必须完全计算完毕,因此触发了整个列表的严格求值。
5. do表达式中的let x = …的称呼
直接称为do块中的let绑定(或do表达式中的let绑定)。
需注意:Haskell的do块中,let绑定默认是非递归的;若需递归绑定,必须显式使用let rec(GHC支持该扩展)。而普通非do块内的let绑定默认是递归的,这是两者的小区别。
内容的提问来源于stack exchange,提问作者Enlico
相关产品推荐
相关产品推荐

