我在ghci中为偶数和奇数定义了两个相互递归的列表,如下所示:
> let evens = 0:map (+1) odds; odds = map (+1) evens
然后我使用:sp
> :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 = _
注意虽然已将odds
评估为第5个元素,但未评估evens
thunk。我可以想到一个直观的解释。必须显式调用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 : _
注意在评估odds
时如何评估evens
thunk?为什么odds
未在第一时间进行评估并在第二次和所有后续评估中进行评估?发生了什么?
答案 0 :(得分: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 : _
如果我们向ft
和tf
添加显式类型的sigantures或启用优化,则不会发生元组构造:
$ 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的来源,试图弄清楚差异的原因 行为。这似乎是类型推断如何用于多态绑定的副作用。
如果绑定是多态的但没有类型签名,那么它的递归用途就是 单态。这是GHC也实施的Hindley-Milner的限制。如果你想 多态递归,你需要一个额外的类型签名。
为了在核心语言中忠实地塑造这个,这个des pe的人制作了一个单形的副本
每个未注释的递归函数。这个单态版本用于递归
通话时,通用版本用于外部呼叫。你甚至可以看到这一点
诸如rep
之类的函数(这是repeat
的重新实现)。
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的这种行为是完全的 正确和好,特别是因为优化会影响性能。