enumFromTo
for Integerのインスタンスを確認する必要があり、最後は次のとおりです。
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)
-- strict accumulator, so
-- head (drop 1000000 [1 .. ]
-- works
と
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
予想どおり、完全に怠惰です。
Integer Enum クラスの場合、 に対して (明示的に) 厳密なアキュムレータがありenumFrom
ます。境界のあるケース (例: [1..n]
) では、 を呼び出しenumDeltaToInteger
てから を呼び出しますup_list
。これはワーカーを使用してリストを制限に達するまで展開します。
ただし、ヘルパーup_list
では厳密です( との比較を参照してください)。x
go
lim
GHCi で実行すると、これは最適化されず、 を返す前に 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
では、通常のケースではなく、ケースのリストを保持するのはなぜですか? enumFromTo
通常のケースは、 forInteger
と forの遅延に依存して、一定の空間で適切に実行されlast
ます。その場合の 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 実行のヒープ プロファイルを実行すると、保持されているリスト全体が表示されます。
これは、少なくともサンクの連鎖ではなく、リスト全体が厳密に構築され、破棄されるまで保持されていることを示しています。
last
謎は次のとおりです。ghcではなく、ghciでリスト引数を保持しているのは何ですか?
私は現在、ghci の内部 (または微妙な) 詳細を疑っています -- これは ghci チケットの価値があると思います。