Note-se que :sprintse não reduzir a uma expressão para WHNF. Se o fizesse, o seguinte daria, em 4vez de _:
Prelude> let four = 2 + 2 :: Int
Prelude> :sprint four
four = _
Em vez disso, :sprintpega o nome de uma ligação, percorre a representação interna do valor da ligação e mostra as já "partes avaliadas" (ou seja, as partes que são construtoras) enquanto usa _como espaço reservado para thunks não avaliados (ou seja, a função lenta suspensa) chamadas). Se o valor for completamente não avaliado, nenhuma avaliação será feita, nem mesmo para o WHNF. (E se o valor for completamente avaliado, você obterá isso, não apenas o WHNF.)
O que você está observando em seus experimentos é uma combinação de tipos numéricos polimórficos versus monomórficos, diferentes representações internas para literais de string versus listas explícitas de caracteres etc. Basicamente, você está observando diferenças técnicas na forma como diferentes expressões literais são compiladas para código de bytes. Portanto, interpretar esses detalhes de implementação como algo que tem a ver com o WHNF vai confundir irremediavelmente você. Geralmente, você deve usar apenas :sprintcomo uma ferramenta de depuração, não como uma maneira de aprender sobre o WHNF e a semântica da avaliação Haskell.
Se você realmente quer entender o que :sprintestá fazendo, pode ativar alguns sinalizadores no GHCi para ver como as expressões estão realmente sendo tratadas e, portanto, eventualmente compiladas no bytecode:
> :set -ddump-simpl -dsuppress-all -dsuppress-uniques
Depois disso, podemos ver o motivo pelo qual você intlistdá _:
> 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>)
[])
Você pode ignorar a chamada returnIOexterna :e concentrar-se na parte que começa com((\ @ a $dNum -> ...
Aqui $dNumestá o dicionário para a Numrestrição. Isso significa que o código gerado ainda não resolveu o tipo real ano tipo Num a => [[a]]; portanto, toda a expressão ainda é representada como uma chamada de função, utilizando um (dicionário para) um Numtipo apropriado . Em outras palavras, é um thunk não avaliado e obtemos:
> :sprint intlist
_
Por outro lado, especifique o tipo como Inte o código é completamente diferente:
> let intlist = [[1::Int,2],[2,3]]
==================== Simplified expression ====================
returnIO
(: ((: (: (I# 1#) (: (I# 2#) []))
(: (: (I# 2#) (: (I# 3#) [])) []))
`cast` <Co:6>)
[])
e assim é a :sprintsaída:
> :sprint intlist
intlist = [[1,2],[2,3]]
Da mesma forma, cadeias literais e listas explícitas de caracteres têm representações completamente diferentes:
> 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>)
[])
e as diferenças na :sprintsaída representam artefatos dos quais partes da expressão que o GHCi considera avaliadas ( :construtores explícitos ) versus não avaliadas (os unpackCString#thunks).