Haskells Weak Head Normal Form

9

Natknąłem się na niektóre irytujące rzeczy. Wiem, że haskell działa ze słabą normalną postacią głowy (WHNF) i wiem, co to jest. Wpisując następujący kod do ghci (używam polecenia: sprint, który redukuje wyrażenie do WHNF według mojej wiedzy.):

let intlist = [[1,2],[2,3]]
:sprint intlist

daje mi intlist = _to całkowicie sens.

let stringlist = ["hi","there"]
:sprint stringlist 

daje stringlist = [_,_] To już mnie dezorientuje. Ale wtedy:

let charlist = [['h','i'], ['t','h','e','r','e']]
:sprint charlist

zaskakująco daje charlist = ["hi","there"]

O ile rozumiem Haskella, ciągi znaków są niczym innym jak listami znaków, co wydaje się potwierdzone przez sprawdzenie typów "hi" :: [Char]i znaków ['h','i'] :: [Char].

Jestem zdezorientowany, ponieważ w moim rozumieniu wszystkie trzy powyższe przykłady są mniej więcej takie same (lista list) i dlatego powinny sprowadzać się do tego samego WHNF, a mianowicie _. czego mi brakuje?

Dzięki

duepiert
źródło
To wydaje się być powiązane
Bergi
@Bergi te pytania są z pewnością powiązane, ale wydaje się, że żadne nie odnosi się do przyczyny "bla"i ['b','l','a']wyjdzie inaczej.
lewo około
@leftaroundabout Ponieważ "bla"może być przeciążony, ale ['b','l','a']jest znany jako String/ [Char]?
Bergi
1
@Bergi też o tym myślałem, ale nie jest to całkiem prawdopodobne, ponieważ ['b', 'l', 'a']może być również przeciążone , podobnie jak "bla"przeciążenie tylko wtedy, gdy -XOverloadedStringsjest włączone.
lewo około
2
Wydaje się, że jest związany z parserem, a może jest specyficzny dla GHCi? (Nie wiem, jak testujesz WHNF w kodzie skompilowanym w GHC.) Same cytaty wydają się być wyzwalaczem.
chepner

Odpowiedzi:

5

Zauważ, że :sprintnie nie zmniejszyć wyrażenie do WHNF. Gdyby tak było, to 4zamiast tego podano _:

Prelude> let four = 2 + 2 :: Int
Prelude> :sprint four
four = _

Zamiast tego :sprintbierze nazwę powiązania, przegląda wewnętrzną reprezentację wartości powiązania i pokazuje już „ocenione części” (tj. Części, które są konstruktorami), jednocześnie wykorzystując _jako symbol zastępczy dla nieocenionych zespołów (tj. Zawieszona leniwa funkcja połączenia). Jeśli wartość jest całkowicie nieoceniona, nie zostanie przeprowadzona ocena, nawet dla WHNF. (A jeśli wartość zostanie całkowicie oszacowana, dostaniesz to, nie tylko WHNF.)

To, co obserwujesz w swoich eksperymentach, to kombinacja polimorficznych i monomorficznych typów liczbowych, różnych wewnętrznych reprezentacji literałów łańcuchowych w porównaniu z wyraźnymi listami znaków itp. Zasadniczo obserwujesz techniczne różnice w sposobie kompilacji różnych wyrażeń literalnych do kodu bajtowego. Tak więc interpretacja tych szczegółów implementacji jako mających coś wspólnego z WHNF spowoduje beznadziejne zamieszanie. Zasadniczo powinieneś używać wyłącznie :sprintjako narzędzie do debugowania, a nie jako sposób na poznanie WHNF i semantyki oceny Haskell.

Jeśli naprawdę chcesz zrozumieć, co :sprintsię dzieje, możesz włączyć kilka flag w GHCi, aby zobaczyć, w jaki sposób obsługiwane są wyrażenia, i ostatecznie skompilować je do kodu bajtowego:

> :set -ddump-simpl -dsuppress-all -dsuppress-uniques

Następnie możemy zobaczyć powód, dla którego intlistpodajesz _:

> 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>)
     [])

Możesz zignorować wywołanie returnIOzewnętrzne i zewnętrzne :i skoncentrować się na części zaczynającej się od((\ @ a $dNum -> ...

Oto $dNumsłownik Numograniczenia. Oznacza to, że wygenerowany kod nie rozwiązał jeszcze rzeczywistego typu aw tym typie Num a => [[a]], więc całe wyrażenie jest nadal reprezentowane jako wywołanie funkcji przyjmujące (słownik dla) odpowiedniego Numtypu. Innymi słowy, jest to nieoceniony kawał, a otrzymujemy:

> :sprint intlist
_

Z drugiej strony określ typ jako Int, a kod jest zupełnie inny:

> let intlist = [[1::Int,2],[2,3]]
==================== Simplified expression ====================
returnIO
  (: ((: (: (I# 1#) (: (I# 2#) []))
         (: (: (I# 2#) (: (I# 3#) [])) []))
      `cast` <Co:6>)
     [])

podobnie jak :sprintwynik:

> :sprint intlist
intlist = [[1,2],[2,3]]

Podobnie, dosłowne ciągi znaków i wyraźne listy znaków mają zupełnie inne reprezentacje:

> 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>)
     [])

a różnice w danych :sprintwyjściowych reprezentują artefakty, które części wyrażenia, które GHCi uważa za ocenione ( :konstruktory jawne ) w porównaniu do nieocenionych (thunks unpackCString#).

KA Buhr
źródło