You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

严格性问题:如何让GHC/GHCi将值永久存储到变量中

GHCi中顶层变量重复计算的问题与解决

问题核心

你遇到的是GHCi字节码解释器的默认行为:顶层的=绑定(不用let定义的变量)默认不共享计算结果,每次引用都会重新求值对应的thunk,哪怕之前已经计算过。这和编译后的Haskell程序不同——编译后的程序顶层绑定会自动共享,只计算一次。

为什么你的严格性尝试无效?

你用的$!、seq、BangPatterns都是控制求值时机的工具,但它们解决不了GHCi顶层绑定不共享的问题:

  1. bar = last $! foo:$!确实会让last的参数foo严格求值,但bar本身是顶层绑定,每次调用bar都会重新执行整个last $! foo表达式,所以每次都要重新计算foo和last foo。
  2. foo seq last foo:seq仅保证foo被求值到WHNF(弱头范式),也就是只确认foo是列表构造器(:)或空列表,不会完全遍历整个列表。而且同样,每次调用bar都会重新执行这个seq表达式,重新计算。
  3. baz = bar seq bar:调用baz时,会先执行bar seq bar——这里seq触发一次bar的计算,然后返回bar,而返回的bar又是顶层绑定,会再次触发计算,所以总共算两次。
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 14:20:00