严格性问题:如何让GHC/GHCi将值永久存储到变量中
GHCi中顶层变量重复计算的问题与解决
问题核心
你遇到的是GHCi字节码解释器的默认行为:顶层的=绑定(不用let定义的变量)默认不共享计算结果,每次引用都会重新求值对应的thunk,哪怕之前已经计算过。这和编译后的Haskell程序不同——编译后的程序顶层绑定会自动共享,只计算一次。
为什么你的严格性尝试无效?
你用的$!、seq、BangPatterns都是控制求值时机的工具,但它们解决不了GHCi顶层绑定不共享的问题:
bar = last $! foo:$!确实会让last的参数foo严格求值,但bar本身是顶层绑定,每次调用bar都会重新执行整个last $! foo表达式,所以每次都要重新计算foo和last foo。fooseqlast foo:seq仅保证foo被求值到WHNF(弱头范式),也就是只确认foo是列表构造器(:)或空列表,不会完全遍历整个列表。而且同样,每次调用bar都会重新执行这个seq表达式,重新计算。baz = barseqbar:调用baz时,会先执行barseqbar——这里seq触发一次bar的计算,然后返回bar,而返回的bar又是顶层绑定,会再次触发计算,所以总共算两次。eval !x = x:eval bar是顶层绑定,每次调用baz都会重新执行eval bar,自然会重新计算bar。
解决方法
方法1:用let定义共享变量
GHCi中,同一个会话内的let绑定是作用域内共享的,计算结果会被缓存:
*Main> let foo = foldl (++) [] (take 5000 $ repeat [10, 123, 323, 33, 11, 345, 23, 33, 23, 11, 987]) *Main> let bar = last foo *Main> bar 987 (1.82 secs, 11,343,660,560 bytes) *Main> bar 987 (0.00 secs, 62,800 bytes) -- 直接复用缓存结果
方法2:启用GHCi的对象代码模式
默认的字节码模式不共享顶层绑定,切换到对象代码模式(编译为机器码)后,顶层绑定会和编译后的程序一样共享结果:
*Main> :set -fobject-code *Main> foo = foldl (++) [] (take 5000 $ repeat [10, 123, 323, 33, 11, 345, 23, 33, 23, 11, 987]) *Main> bar = last foo *Main> bar 987 (1.82 secs, 11,343,660,560 bytes) *Main> bar 987 (0.00 secs, 62,800 bytes)
注意:这个模式下加载顶层绑定的速度会慢一些,但能获得和编译程序一致的共享行为。
方法3:强制求值并绑定到let变量
如果已经用顶层=定义了变量,可以用let强制求值并缓存结果:
*Main> foo = foldl (++) [] (take 5000 $ repeat [10, 123, 323, 33, 11, 345, 23, 33, 23, 11, 987]) *Main> bar = last foo *Main> let cachedBar = bar `seq` bar -- 强制求值bar并缓存 *Main> cachedBar 987 (1.82 secs, 11,343,660,560 bytes) *Main> cachedBar 987 (0.00 secs, 62,800 bytes)
补充:编译程序的差异
在编译后的Haskell程序中,顶层绑定默认是共享的,编译器会自动缓存计算结果,不会出现重复计算的问题。只有在GHCi的字节码解释模式下,才会有这个特殊行为,目的是为了快速修改和重载顶层绑定,牺牲了共享性。
内容的提问来源于stack exchange,提问作者Syncrossus
相关产品推荐
相关产品推荐

