奇怪的GHCi惰性求值

20

我在ghci中定义了两个相互递归的列表来表示偶数和奇数:

> let evens = 0:map (+1) odds; odds = map (+1) evens

然后我使用:sp来查询thunks。

> :sp evens
evens = _
> :sp odds
odds = _
> take 5 evens
[0,2,4,6,8]
> :sp evens
evens = 0 : 2 : 4 : 6 : 8 : _
:sp odds
odds = _

注意,尽管evens已经被评估到第5个元素,但odds未被评估。我可以为此提供一个直观的解释。必须显式调用odds才能进行评估:

> take 5 odds
[1,3,5,7,9]
>:sp odds
odds = 1 : 3 : 5 : 7 : 9 : _

然而,现在当我这样做时:

> take 10 evens
[0,2,4,6,8,10,12,14,16,18]
> :sp evens
evens = 0 : 2 : 4 : 6 : 8 : 10 : 12 : 14 : 16 : 18 : _
> :sp odds
odds = 1 : 3 : 5 : 7 : 9 : 11 : 13 : 15 : 17 : _

注意当 evens 被评估时,odds thunk 现在是被评估的吗?为什么第一次没有评估,第二次和所有后续评估都评估了 odds?发生了什么事?


1
无法重现。你的操作系统、GHC版本等是什么? - jev
我可以在Win7上使用GHC 7.6.3重现它。 我还可以执行take 5 odds并获得与:sp evens相同的行为,这实际上是没有意义的,因为evens中有文字0 - bheklilr
7
我有一个猜想:evensodds在第一次显式观察之前是“有点”多态的。然后它们神奇地单态化为[Integer],并且从此以后所有内容都是可见的。例如,尝试将evens, odds :: [Integer]添加到您的let中查看差异。 - Daniel Wagner
当一个字面量“看起来像一个函数”时,它不会被提前计算。比较 let x = [1,2,3,4]let y () = [1,2,3,4]。如果我们执行 :sp x,我们得到 x = [1,2,3,4],但是 :sp y ()y = _。因此,在这种情况下,evens 中的字面量 0 不会被提前计算,因为它显然不是一个常量。 - J. Abrahamson
1
@DanielWagner evensodds的默认类型签名是[Integer],至少在ghc 7.6.3之前是这样。这是在不必显式提及的情况下推断出的类型。为什么提及类型会有任何区别呢?我认为这只意味着类型推断器或:sp本身存在错误。 - is7s
1个回答

12
这与 GHC 如何编译相互递归的绑定有关(而且绑定是否有显式类型签名会有区别)。
让我们编写下面这个简单程序,它展示了同样的问题,但消除了整数重载或单态性限制可能发挥的作用的所有怀疑:
module MutRec where

ft = False : map not tf
tf = map not ft

将此加载到GHCi中(我正在使用7.6.3)会产生以下结果:

*MutRec> take 5 ft
[False,False,False,False,False]
*MutRec> :sp ft
ft = False : False : False : False : False : _
*MutRec> :sp tf
tf = _

让我们看一下此模块的核心代码

$ ghc -O0 MutRec -fforce-recomp -ddump-simpl -dsuppress-all
[1 of 1] Compiling MutRec           ( MutRec.hs, MutRec.o )

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 28, types: 42, coercions: 0}

Rec {
ft1_rkA
ft1_rkA = : False a_rkC

tf1_rkB
tf1_rkB = map not ft1_rkA

a_rkC
a_rkC = map not tf1_rkB
end Rec }

ds_rkD
ds_rkD = (ft1_rkA, tf1_rkB)

ft
ft = case ds_rkD of _ { (ft2_Xkp, tf2_Xkr) -> ft2_Xkp }

tf
tf = case ds_rkD of _ { (ft2_Xkq, tf2_Xks) -> tf2_Xks }

这就是全部的解释。相互递归的定义最终会在一个Rec块中结束,直接相互引用。但是 GHC 正在构建一对ds_rkD并从该对中重新提取组件。这是一个额外的间接过程。这就解释了为什么在 GHCi 中部分评估ft之后,即使在其下面已经进行了评估,tf的顶部仍将显示为 thunk。实际上,我们可以验证,仅对tf进行最小评估就足以暴露这一点:

*MutRec> take 5 ft
[False,False,False,False,False]
*MutRec> :sp ft
ft = False : False : False : False : False : _
*MutRec> :sp tf
tf = _
Prelude MutRec> seq tf ()
()
Prelude MutRec> :sp tf
tf = True : True : True : True : _

如果我们为 fttf 添加显式类型签名或启用优化,则元组构造将不会发生:

$ ghc -O MutRec -fforce-recomp -ddump-simpl -dsuppress-all
[1 of 1] Compiling MutRec           ( MutRec.hs, MutRec.o )

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 12, types: 11, coercions: 0}

Rec {
ft1
ft1 = map not tf

ft
ft = : False ft1

tf
tf = map not ft
end Rec }

现在,GHCi的行为将更加自然。


编辑

我查看了GHC的源代码,试图找出这种行为差异的原因。看起来这是多态绑定类型推断工作方式的副作用。

如果一个绑定是多态的,但没有类型签名,那么它的递归使用是单态的。这是Hindley-Milner中的限制,也被GHC实现。如果你想要多态递归,你需要一个额外的类型签名。

为了在Core语言中忠实地模拟这一点,解糖器会制作每个未注释的递归函数的单态版本。这个单态版本用于递归调用,广义版本用于外部调用。即使对于像rep(它是repeat的重新实现)这样的小函数,你也可以看到这一点。解糖后的core:

rep x = x : rep x
是什么。
rep
rep =
  \ (@ a_aeM) ->
    letrec {
      rep_aeJ
      rep_aeJ =
        \ (x_aeH :: a_aeM) -> : @ a_aeM x_aeH (rep_aeJ x_aeH); } in
    rep_aeJ

外部的 rep 是多态的,因此在开头有类型抽象 \ (@ a_aeM) ->。内部的 rep_aeJ 是单态的,并在递归调用中使用。

如果您对 rep 添加显式类型注释

rep :: a -> [a]
rep x = x : rep x

然后递归调用会使用多态版本,生成的Core变得更简单:

Rec {
rep
rep = \ (@ a_b) (x_aeH :: a_b) -> : @ a_b x_aeH (rep @ a_b x_aeH)
end Rec }

你可以看到,在每个递归调用中,类型参数@ a_b是如何在开始时被捕获并重新应用于rep的。

我们正在看到的互相递归绑定的元组构造只是这个原则的一般化。您建立内部单态版本的相互递归函数,然后将它们概括为一个元组,并从元组中提取多态版本。

所有这些都与绑定实际上是否是多态无关。它们是递归的就足够了。我认为GHC的这种行为完全正确和OK,特别是因为优化会处理性能问题。


但是,事实上,在添加显式类型签名后行为发生变化是不合理的,不是吗? - is7s
4
@is7s 行为会改变吗?结果不会。 GHC 可以使用任何不改变结果的评估策略。:sp 窥视了通常不会“暴露”的实现细节。我也很想知道元组构造对于 GHC 的作用是什么。 - Ben
1
@Ben 但这是额外的间接层,因此会影响性能。我从未想过添加已经正确推断的类型会在性能方面有任何差异! - is7s
1
@is7s 我已经编辑了我的回答。我同意Ben的观点,GHC允许它所做的事情。在一个未经优化的Haskell程序中抱怨潜在的性能损失是没有意义的。只有当优化不能解决这个问题时才会成为问题。 - kosmikus
如果一个绑定是多态的但没有类型签名... 但在我们的情况下,绑定实际上是单态的。推断出的类型是 [Integer] - is7s
引用我的回答:“所有这些都与绑定是否实际上是多态的无关。它们递归即可。”我猜这可以改变,但也许不值得特殊情况。 - kosmikus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接