最近我一直在尝试了解GHC在评估
seq
时到底强制了什么。假设我保存以下定义:
f :: Int -> [Int] -> [Int]
f = \n -> \ns -> if n <= 0 then ns else f (n - 1) (n : ns)
x = f 5 []
然后
:load
他们进入 GHCi 会话。
在该会话中,观察到以下行为:
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]
简单地说,尽管 x 和 y 的定义和调用相同,但为什么它们的计算结果不同?这反映了 GHC 的实际作用?
(我倾向于
y
的结果,因为在评估 x
和 y
到 WHNF 时,它们的所有尾部条目都在过程中被评估为文字,但也许我错了。)
当您
:load
编码到GHCi中时,默认情况下GHCi会使用断点对其进行检测。默认情况下不启用断点,但如果您选择使用 GHCi 的调试器,它们必须存在于代码中以便稍后启用它们。相比之下,您在 GHCi 提示符中键入的代码不会 会在其中散布断点(因此,您无法在提示符处键入的代码内设置断点)。
断点出现在 Core(GHC 的高级 IR)中。您可以通过将
-ddump-simpl
传递给 GHC 或 GHCi 来查看 Core。这是当我在运行 f
时从文件中 :load
输入 Lib.hs
时的样子(你可以看到我称之为 ghci -ddump-simpl
)。
Rec {
-- RHS size: {terms: 20, types: 7, coercions: 0, joins: 0/0}
f [Occ=LoopBreaker] :: Int -> [Int] -> [Int]
[GblId]
f = break<Lib,6>()
\ (n_aBW :: Int) ->
break<Lib,5>(n_aBW)
\ (ns_aBX :: [Int]) ->
break<Lib,4>(n_aBW,ns_aBX)
case break<Lib,0>(n_aBW)
<= @Int GHC.Classes.$fOrdInt n_aBW (GHC.Types.I# 0#)
of {
False ->
break<Lib,3>(n_aBW,ns_aBX)
f (break<Lib,1>(n_aBW)
- @Int GHC.Internal.Num.$fNumInt n_aBW (GHC.Types.I# 1#))
(break<Lib,2>(n_aBW,ns_aBX) GHC.Types.: @Int n_aBW ns_aBX);
True -> ns_aBX
}
end Rec }
将其与我在同一会话中输入
g
时的结果进行比较。 (如果您使用 f
将其编译为目标代码,ghc -ddump-simpl
也看起来像这样):
letrec {
g_aTj [Occ=LoopBreaker]
:: GHC.Types.Int -> [GHC.Types.Int] -> [GHC.Types.Int]
[LclId,
Arity=2,
Unf=Unf{Src=<vanilla>, TopLvl=False,
Value=True, ConLike=True, WorkFree=True, Expandable=True,
Guidance=IF_ARGS [0 0] 160 0}]
g_aTj
= \ (n_aTk :: GHC.Types.Int) (ns_aTl :: [GHC.Types.Int]) ->
case GHC.Classes.<=
@GHC.Types.Int GHC.Classes.$fOrdInt n_aTk (GHC.Types.I# 0#)
of {
GHC.Types.False ->
g_aTj
(GHC.Internal.Num.-
@GHC.Types.Int GHC.Internal.Num.$fNumInt n_aTk (GHC.Types.I# 1#))
(GHC.Types.: @GHC.Types.Int n_aTk ns_aTl);
GHC.Types.True -> ns_aTl
}; } in
-- ... some GHCi prompt stuff under the let
显着的区别在于,在
(:)
中调用时,f
上会出现 break
,而在 g
中则没有 break
。当没有设置断点时,每个break
就像一个恒等函数:break x = x
(我将忽略标识每个<Module, BreakpointNumber>
的break
部分)。忽略所有其他断点,f
-with-breakpoints 就像写的那样
f n ns = if n <= 0 then ns else f (n - 1) (break (n : ns))
特别是
x = f 5 []
= f 4 (break (5 : []))
= f 3 (break (4 : break (5 : [])))
= f 2 (break (3 : break (4 : break (5 : []))))
= f 1 (break (2 : break (3 : break (4 : break (5 : [])))))
= f 0 (break (1 : break (2 : break (3 : break (4 : break (5 : []))))))
= break (1 : break (2 : break (3 : break (4 : break (5 : [])))))
= 1 : break (2 : break (3 : break (4 : break (5 : [])))) -- reached WHNF!
请注意,一旦
x
到达 WHNF
,尾巴仍然指向未评估的 break
重击。相反,将 y
转换为 WHNF 可以给出完全评估的结果 1 : 2 : 3 : 4 : 5 : []
,因为 break
中没有 g
。
总结:
:sprint x
打印1 : _
,因为_
代表一个可能需要触发断点的值,但还没有机会检查是否应该触发断点。在提示符下定义的内容(如 y
)不支持断点。您还可以通过传递 :load
来关闭 -fno-break-points
代码的断点,这消除了 x
和 y
之间的差异。
$ ghci -fno-break-points
GHCi, version 9.10.1: https://www.haskell.org/ghc/ :? for help
ghci> :load Lib.hs
[1 of 1] Compiling Lib ( Lib.hs, interpreted )
Ok, one module loaded.
ghci> seq x ()
()
ghci> :sprint x
x = [1,2,3,4,5]
ghci>