`seq` 显然是否强制评估整个递归定义的列表,具体取决于它如何加载到 GHCi

问题描述 投票:0回答:1

最近我一直在尝试了解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 时,它们的所有尾部条目都在过程中被评估为文字,但也许我错了。)

haskell lazy-evaluation ghci
1个回答
0
投票

当您

: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>
© www.soinside.com 2019 - 2024. All rights reserved.