Haskell 奇怪的GHCi惰性评估
我为ghci中的偶数和奇数定义了两个相互递归的列表,如下所示:Haskell 奇怪的GHCi惰性评估,haskell,lazy-evaluation,ghci,Haskell,Lazy Evaluation,Ghci,我为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 = _ 请注意,赔率thun
> 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 = _
请注意,赔率
thunk是如何计算的,尽管evens
已计算到第5个元素。我能想出一个直观的解释<必须明确调用代码>赔率才能进行评估:
> 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
,现在都是如何评估赔率的?为什么第一次未评估赔率,第二次及所有后续评估均未评估赔率?发生了什么事 这与GHC如何编译相互递归的绑定有关(绑定是否具有显式类型签名是有区别的)
让我们编写以下简单的程序,它暴露了相同的问题,但消除了对整数重载或单态限制可能扮演的角色的所有怀疑:
module MutRec where
ft = False : map not tf
tf = map not ft
将其加载到GHCi(我使用的是7.6.3)会产生:
让我们看看这个模块的核心代码
$ 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
的顶部仍然会显示为“砰”的一声,即使下面已经进行了评估。事实上,我们可以验证,仅对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也实施的限制。如果你愿意
多态递归,您需要一个额外的类型签名
为了在核心语言中忠实地对此进行建模,脱糖者制作了
每个未注递归函数。此单态版本用于递归
调用时,通用版本用于外部调用。你甚至可以在很短的时间内看到这一点
函数,例如rep
(这是repeat
的重新实现)。去糖化的核
rep x = x : rep x
是
外部的rep
是多态的,因此在开始时类型抽象\(@a\u aeM)->
。内部rep_aeJ
是单态的,用于递归调用
如果向rep
rep :: a -> [a]
rep x = x : rep x
然后对多态版本进行递归调用,生成的核心就变成
更简单:
您可以看到类型参数@a_b
是如何在开始时拾取并重新应用的
在每次递归调用中执行rep
我们看到的用于相互递归绑定的元组结构只是一个简单的例子
这一原则的概括。你们建立了相互作用的内部单态版本
递归函数,然后在元组中泛化它们,并提取多态
来自元组的版本
所有这些都独立于绑定是否是多态的。
它们是递归的就足够了。我认为GHC的这种行为是完全错误的
正确且正常,特别是因为优化考虑了性能影响。无法重现这一点。你的操作系统是什么,ghc版本是什么?我可以用ghc 7.6.3在Win7上复制它。我也可以做拿5个赔率
,得到的相同行为:sp evens
,这真的没有意义,因为evens
中有文字0
。我有一个猜测:evens
和赔率
是“某种”多态的,直到你第一次明确地观察它们。然后它们被神奇地单形化为[Integer]
,从那时起,一切都是可见的。e、 g.尝试将evens,赔率::[Integer]
添加到您的中,让查看差异。@bheklillr当文字“看起来像函数”时,不会提前计算它。比较让x=[1,2,3,4]
和让y()=[1,2,3,4]
。如果我们做:sp x
我们得到x=[1,2,3,4]
,但是:sp y()
是y=\ucode>。因此,在这种情况下,evens
中的文字0
不会得到早期计算,因为它显然不是一个常数。@DanielWagnerevens
和赔率的默认类型签名是[Integer]
,至少在ghc 7.6.3之前是这样。这是推断的类型,不必显式地提及它。为什么提到类型会有什么不同?我认为这只是意味着类型INFENCER或:sp
本身存在缺陷。但添加显式类型签名后行为改变的事实是不合理的,不是吗?@is7s行为会改变吗?结果不是这样。GHC被“允许”使用任何它喜欢的、不会改变结果的评估策略<代码>:sp
正在窥探通常不会“公开”的实施细节。不过,我也很想知道元组构造在GHC中实现了什么,但这是一个额外的间接层次,因此对性能有很大影响。我决不会认为添加已正确推断的类型会对性能产生任何影响@我已经编辑了我的答案。我同意本的观点,GHC可以做它所做的事情。抱怨一个未经优化的Haskell项目的潜在性能下降是没有道理的。会的
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 -> [a]
rep x = x : rep x
Rec {
rep
rep = \ (@ a_b) (x_aeH :: a_b) -> : @ a_b x_aeH (rep @ a_b x_aeH)
end Rec }