这与 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 : _
如果我们为 ft
和 tf
添加显式类型签名或启用优化,则元组构造将不会发生:
$ 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,特别是因为优化会处理性能问题。
take 5 odds
并获得与:sp evens
相同的行为,这实际上是没有意义的,因为evens
中有文字0
。 - bheklilrevens
和odds
在第一次显式观察之前是“有点”多态的。然后它们神奇地单态化为[Integer]
,并且从此以后所有内容都是可见的。例如,尝试将evens, odds :: [Integer]
添加到您的let
中查看差异。 - Daniel Wagnerlet x = [1,2,3,4]
和let y () = [1,2,3,4]
。如果我们执行:sp x
,我们得到x = [1,2,3,4]
,但是:sp y ()
是y = _
。因此,在这种情况下,evens
中的字面量0
不会被提前计算,因为它显然不是一个常量。 - J. Abrahamsonevens
和odds
的默认类型签名是[Integer]
,至少在ghc 7.6.3之前是这样。这是在不必显式提及的情况下推断出的类型。为什么提及类型会有任何区别呢?我认为这只意味着类型推断器或:sp
本身存在错误。 - is7s