Обратите внимание, что :sprintэто не уменьшает выражение до WHNF. Если бы это было так, то 4вместо этого было бы следующее _:
Prelude> let four = 2 + 2 :: Int
Prelude> :sprint four
four = _
Скорее, он :sprintберет имя привязки, пересекает внутреннее представление значения привязки и показывает уже «вычисленные части» (т.е. части, которые являются конструкторами) при использовании _в качестве заполнителя для неоцененных блоков (т. Е. Приостановленной ленивой функции). звонки). Если значение полностью не оценено, оценка не будет выполнена, даже для WHNF. (И если значение будет полностью оценено, вы получите это, а не только WHNF.)
В ваших экспериментах вы наблюдаете сочетание полиморфных и мономорфных числовых типов, различных внутренних представлений для строковых литералов и явных списков символов и т. Д. В основном вы наблюдаете технические различия в том, как различные литеральные выражения компилируются в байт-код. Таким образом, интерпретация этих деталей реализации как имеющих отношение к 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тип. Другими словами, это неоцененный отрывок, и мы получаем:
> :sprint intlist
_
С другой стороны, укажите тип as 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 выходных данных представляют собой артефакты, которые части выражения GHCi считает оцененными (явные :конструкторы) по сравнению с неоцененными ( unpackCString#thunks).