我们需要查看针对整数的 enumFromTo
实例,最后:
last [] = errorEmptyList "last"
last (x:xs) = last' x xs
where last' y [] = y
last' _ (y:ys) = last' y ys
在GHC.Enum中定义如下:
enumFrom x = enumDeltaInteger x 1
enumFromThen x y = enumDeltaInteger x (y-x)
enumFromTo x lim = enumDeltaToInteger x 1 lim
在哪里
enumDeltaInteger :: Integer -> Integer -> [Integer]
enumDeltaInteger x d = x `seq` (x : enumDeltaInteger (x+d) d)
和
enumDeltaToInteger :: Integer -> Integer -> Integer -> [Integer]
enumDeltaToInteger x delta lim
| delta >= 0 = up_list x delta lim
| otherwise = dn_list x delta lim
up_list :: Integer -> Integer -> Integer -> [Integer]
up_list x0 delta lim = go (x0 :: Integer)
where
go x | x > lim = []
| otherwise = x : go (x+delta)
last
是完全惰性的,这一点是可以预料的。
对于整数枚举类,我们为enumFrom
(显式地)设置了一个严格的累加器。在有界情况下(例如[1..n]
),它调用enumDeltaToInteger
然后进入up_list
,up_list
使用一个worker来展开列表直到达到其限制。
但是在go
辅助函数中,up_list
对x
是严格的(请参见与lim
的比较)。
在GHCi中运行时,所有这些都没有得到优化,导致会调用naive的enumFromTo,最后返回()
。
let
it_ax6 :: ()
it_ax6 =
case last
@ GHC.Integer.Type.Integer
(GHC.Enum.enumFromTo
@ GHC.Integer.Type.Integer
GHC.Num.$fEnumInteger
(GHC.Integer.smallInteger 1)
(GHC.Real.^
@ GHC.Integer.Type.Integer
@ GHC.Integer.Type.Integer
GHC.Num.$fNumInteger
GHC.Real.$fIntegralInteger
(GHC.Integer.smallInteger 10)
(GHC.Integer.smallInteger 7)))
of _ -> GHC.Unit.()
in
GHC.Base.thenIO
@ ()
@ [()]
(System.IO.print @ () GHC.Show.$fShow() it_ax6)
(GHC.Base.returnIO
@ [()] (GHC.Types.: @ () it_ax6 (GHC.Types.[] @ ())))
那么为什么在seq
情况下我们要保留列表,而在正常情况下不需要呢?正常情况在构建空间上运行良好,依赖于 Integer
和 last
的 enumFromTo
延迟性。该情况的 GHCi 核心看起来像:
let {
it_aKj :: GHC.Integer.Type.Integer
[LclId,
Unf=Unf{Src=<vanilla>, TopLvl=False, Arity=0, Value=False,
ConLike=False, Cheap=False, Expandable=False,
Guidance=IF_ARGS [] 170 0}]
it_aKj =
GHC.List.last
@ GHC.Integer.Type.Integer
(GHC.Enum.enumFromTo
@ GHC.Integer.Type.Integer
GHC.Num.$fEnumInteger
(GHC.Integer.smallInteger 1)
(GHC.Real.^
@ GHC.Integer.Type.Integer
@ GHC.Integer.Type.Integer
GHC.Num.$fNumInteger
GHC.Real.$fIntegralInteger
(GHC.Integer.smallInteger 10)
(GHC.Integer.smallInteger 7))) } in
GHC.Base.thenIO
@ ()
@ [()]
(System.IO.print
@ GHC.Integer.Type.Integer GHC.Num.$fShowInteger it_aKj)
(GHC.Base.returnIO
@ [()]
(GHC.Types.:
@ ()
(it_aKj
`cast` (UnsafeCo GHC.Integer.Type.Integer ()
:: GHC.Integer.Type.Integer ~ ()))
(GHC.Types.[] @ ())))
所以这些几乎是相同的,差别在于:
- 在
seq
版本中,last (enumFromTo ..)
被强制放在case
内部。
- 在常规版本中,它是一个惰性的
let
。
- 在
seq
版本中,该值被计算然后被丢弃,产生了一个()
--没有任何东西查看结果。
- 在常规情况下,它被检查并显示。
奇怪的是,这里没有任何神奇的地方:
let x = case last (enumFromTo 1 n) of _ -> ()
这使得它保留值。
正如我们所看到的,up_list
的实现在累加器方面非常严格(因为它与 lim
进行比较,并且该列表是惰性展开的 -- 因此 last
应该能够在常数空间中使用它)。手动编写表达式可以确认这一点。
对 ghci 执行进行堆分析显示整个列表被保留:
![enter image description here](https://istack.dev59.com/GTp88.webp)
至少可以确定它不是惰性求值的一系列延迟计算,而是整个列表被严格构建并保持,直到被丢弃。
所以谜团是:在 ghci 中是什么将列表参数保留给了 last
,而在 ghc 中没有呢?
我怀疑这是 ghci 的一些内部(或微妙)细节问题-- 我认为这值得一个 ghci ticket。
seq (last [1::Int ..10^8]) ()
应该已经解决了这个问题... - hvrrepeat 1
没有空间泄漏,而使用[1,1..]
则有。也许enumFrom
和相关函数的算术运算在某种程度上负责? - n. m.repeat
生成的无限列表由于共享而占用恒定的空间。如果您尝试使用map id (repeat 1)
来破坏共享,则会再次泄漏。 - hammarlast (map id (repeat 1)) ^seq^ ()
时会发生泄漏,但是执行last (map id (repeat ())) ^seq^ ()
不会。然而,当执行last (map id (repeat Wtf)) ^seq^ ()
时,其中data Wtf = Wtf
,仍会发生泄漏。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳