奇怪的GHCi懒惰的评价

时间:2014-03-18 21:43:57

标签: haskell lazy-evaluation ghci

我在ghci中为偶数和奇数定义了两个相互递归的列表,如下所示:

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

然后我使用:sp

查询thunk
> :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未在第一时间进行评估并在第二次和所有后续评估中进行评估?发生了什么?

1 个答案:

答案 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 : _

如果我们向fttf添加显式类型的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的这种行为是完全的 正确和好,特别是因为优化会影响性能。