Обратите внимание, что :sprint
делает не сокращением выражения до WHNF. Если это так, то следующее даст 4
вместо _
:
Prelude> let four = 2 + 2 :: Int
Prelude> :sprint four
four = _
Скорее, :sprint
берет имя привязки, обходит внутреннее представление значения привязки и показывает уже «вычисленные части» (то есть части, которые являются конструкторами) при использовании _
в качестве заполнителя для неоцененных блоков (т. е. приостановленных вызовов ленивых функций). Если значение полностью не оценено, оценка не будет выполнена, даже для WHNF. (И если значение будет полностью оценено, вы получите это, а не только WHNF.)
То, что вы наблюдаете в своих экспериментах, является комбинацией полиморфизма c против мономорфи c цифра c типы, различные внутренние представления строковых литералов и явные списки символов и т. д. c. По сути, вы наблюдаете технические различия в том, как различные литеральные выражения компилируются в байт-код. Таким образом, интерпретация этих деталей реализации как имеющих отношение к WHNF может безнадежно запутать вас. Как правило, вы должны использовать :sprint
только как инструмент отладки, а не как способ узнать о WHNF и семантике Haskell оценки.
Если вы действительно хотите понять, что делает :sprint
, Вы можете включить несколько флагов в GHCi, чтобы увидеть, как на самом деле обрабатываются выражения и, таким образом, в конечном итоге скомпилированы в байт-код:
> :set -ddump-simpl -dsuppress-all -dsuppress-uniques
После этого мы можем увидеть причину, по которой ваш intlist
дает _
:
> let intlist = [[1,2],[2,3]]
==================== Simplified expression ====================
returnIO
(: ((\ @ a $dNum ->
: (: (fromInteger $dNum 1) (: (fromInteger $dNum 2) []))
(: (: (fromInteger $dNum 2) (: (fromInteger $dNum 3) [])) []))
`cast` <Co:10>)
[])
Вы можете игнорировать returnIO
и внешний вызов :
и сосредоточиться на части, которая начинается с ((\ @ a $dNum -> ...
Здесь $dNum
- словарь для ограничения Num
. Это означает, что сгенерированный код еще не разрешил фактический тип a
в типе Num a => [[a]]
, поэтому все выражение все еще представляется как вызов функции, принимающей (словарь для) соответствующий тип Num
. Другими словами, это неоцененный thunk, и мы получаем:
> :sprint intlist
_
С другой стороны, укажите тип как Int
, и код будет совершенно другим:
> let intlist = [[1::Int,2],[2,3]]
==================== Simplified expression ====================
returnIO
(: ((: (: (I# 1#) (: (I# 2#) []))
(: (: (I# 2#) (: (I# 3#) [])) []))
`cast` <Co:6>)
[])
и вывод :sprint
:
> :sprint intlist
intlist = [[1,2],[2,3]]
Аналогично, буквенные строки и явные списки символов имеют совершенно разные представления:
> let stringlist = ["hi", "there"]
==================== Simplified expression ====================
returnIO
(: ((: (unpackCString# "hi"#) (: (unpackCString# "there"#) []))
`cast` <Co:6>)
[])
> let charlist = [['h','i'], ['t','h','e','r','e']]
==================== Simplified expression ====================
returnIO
(: ((: (: (C# 'h'#) (: (C# 'i'#) []))
(: (: (C# 't'#)
(: (C# 'h'#) (: (C# 'e'#) (: (C# 'r'#) (: (C# 'e'#) [])))))
[]))
`cast` <Co:6>)
[])
и различия в :sprint
output представляет артефакты, которые часть выражения GHCi считает оцененными (явные :
конструкторы) по сравнению с неоцененными (unpackCString#
thunks).