为何GHCi中seq对递归列表的求值表现因定义方式而异?
GHCi中seq对相同定义表达式的求值差异解析
现象回顾
你定义了逻辑完全一致的递归函数和调用,但文件加载的x与GHCi交互式let绑定的y,在执行seq后用:sprint查看的结果截然不同:
-- 文件中定义 f :: Int -> [Int] -> [Int] f = \n -> \ns -> if n <= 0 then ns else f (n - 1) (n : ns) x = f 5 []
ghci> let g :: Int -> [Int] -> [Int]; g = \n -> \ns -> if n <= 0 then ns else g (n - 1) (n : ns) ghci> let y = g 5 [] ghci> seq x () () ghci> seq y () () ghci> :sprint x x = 1 : _ ghci> :sprint y y = [1,2,3,4,5]
差异原因
核心是GHC对顶级定义与交互式let绑定的默认求值策略、优化逻辑不同:
- 文件加载的顶级定义(x):GHC默认遵循标准惰性求值语义,
seq的作用仅是将参数求值到WHNF(弱头范式)。对于列表类型来说,WHNF只要求确定它是(:)构造子即可,不需要递归求值构造子的第二个参数(即列表尾部)。因此seq x ()仅触发x求值到1 : _,尾部仍处于未求值的惰性状态。 - GHCi交互式let绑定(y):GHCi的交互式环境为了提升用户体验,默认会对部分表达式进行更严格的求值(甚至到NF(完全范式))。当你用
let绑定y = g 5 []时,GHCi自动触发了完整的递归求值,将整个列表完全展开为字面量,因此seq y ()后:sprint显示完整列表。
哪种是GHC的“真实行为”?
两种都是GHC的真实行为,只是应用场景不同:
- x的结果符合
seq的标准语义:seq的规范就是仅将参数求值到WHNF,这是Haskell惰性求值的核心行为。如果需要完全求值x,你需要触发更严格的操作(比如调用length x,会遍历整个列表强制全部求值)。 - y的结果是GHCi交互式环境的特殊处理,并非
seq本身的行为。你可以通过修改GHCi选项让y的表现与x一致,比如执行:set -fno-full-laziness后重新定义y,:sprint就会显示y = 1 : _。
内容的提问来源于stack exchange,提问作者Rafi
相关产品推荐
相关产品推荐

