Haskell: Co To jest słaba Głowa normalna forma?

Co oznacza słaba Głowa normalna forma (WHNF)? Co oznacza postać normalna (HNF) i postać normalna (NF)?

Prawdziwy świat Haskell stwierdza:

Znana funkcja seq ocenia wyrażenie na to, co nazywamy głową postać normalna (w skrócie HNF). Zatrzymuje się, gdy dociera do najbardziej oddalonych konstruktor ("Głowa"). Różni się od postaci normalnej (NF), w które wyrażenie jest całkowicie oceniane.

Usłyszysz również Programiści Haskell odnoszą się do słabej głowy normalnej postaci (WHNF). Dla normalnych danych słaba Głowa normalna forma jest taka sama jak głowa normalna forma. Różnica powstaje tylko dla funkcji i jest zbyt niepokoi nas to.

Przeczytałem kilka zasobów i definicji (Haskell Wiki i Haskell Mail List i darmowy słownik ), ale tego nie rozumiem. Czy może ktoś podać przykład lub podać laika definicja?

Zgaduję, że byłoby to podobne do:

WHNF = thunk : thunk

HNF = 0 : thunk 

NF = 0 : 1 : 2 : 3 : []

Jak seq i ($!) odnoszą się do WHNF i HNF?

Update

Wciąż jestem zdezorientowany. Wiem, że niektóre odpowiedzi mówią, żeby ignorować HNF. Po przeczytaniu różnych definicji wydaje się, że nie ma różnicy między zwykłymi danymi w WHNF i HNF. Jednak wydaje się, że istnieje różnica, jeśli chodzi o funkcję. Jeśli nie było różnicy, dlaczego seq jest konieczne dla foldl'?

Inny punkt zamieszania pochodzi z Haskell Wiki, która stwierdza, że seq zmniejsza się do WHNF i nie zrobi nic z poniższym przykładem. Następnie mówią, że muszą użyć seq, aby wymusić ocenę. Czy to nie zmusza go do HNF?

Common newbie stack overflowing code:

myAverage = uncurry (/) . foldl' (\(acc, len) x -> (acc+x, len+1)) (0,0)

Ludzie, którzy rozumieją seq i słabą normalną formę głowy (whnf) mogą natychmiast zrozum, co tu idzie nie tak. (acc + x, len+1) jest już w whnf, czyli seq, co zmniejsza wartość do whnf, nic z tym nie robi. Ten kod będzie budować thunks tak jak w oryginalnym przykładzie foldl, będą po prostu wewnątrz krotki. Rozwiązaniem jest tylko wymusić elementy krotki, np.

myAverage = uncurry (/) . foldl' 
          (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)

-Haskell Wiki na Stackoverflow

Author: hammar, 2011-07-29

6 answers

Postaram się wyjaśnić w prosty sposób. Jak zauważyli inni, Głowa normalna forma nie ma zastosowania do Haskell, więc nie będę rozważać go tutaj.

Postać normalna

Wyrażenie w postaci normalnej jest w pełni oceniane i żadne pod-wyrażenie nie może być dalej oceniane (tzn. nie zawiera nieprzetworzonych thunksów).

Wszystkie te wyrażenia są w postaci normalnej:

42
(2, "hello")
\x -> (x + 1)

Wyrażenia te nie są w postaci normalnej:

1 + 2                 -- we could evaluate this to 3
(\x -> x + 1) 2       -- we could apply the function
"he" ++ "llo"         -- we could apply the (++)
(1 + 1, 2 + 2)        -- we could evaluate 1 + 1 and 2 + 2

Słaba Głowa normalna formularz

Wyrażenie w słabym head w postaci normalnej zostało ocenione do najbardziej oddalonego konstruktora danych lub abstrakcji lambda (head ). Wyrażenia podrzędne mogły być oceniane lub nie . W związku z tym każde wyrażenie postaci normalnej jest również w postaci normalnej głowy, choć przeciwieństwo w ogóle nie występuje.

Aby określić, czy wyrażenie jest w postaci normalnej głowy, musimy tylko spojrzeć na zewnętrzną część wyrażenia. Jeśli to dane konstruktor lub lambda, jest w słabej, normalnej formie. Jeśli jest to aplikacja funkcyjna, to nie jest.

Wyrażenia te są w postaci normalnej głowy:

(1 + 1, 2 + 2)       -- the outermost part is the data constructor (,)
\x -> 2 + 2          -- the outermost part is a lambda abstraction
'h' : ("e" ++ "llo") -- the outermost part is the data constructor (:)

Jak wspomniano, wszystkie wyrażenia postaci normalnej wymienione powyżej są również w postaci normalnej głowy.

Wyrażenia te nie są w postaci normalnej głowy:

1 + 2                -- the outermost part here is an application of (+)
(\x -> x + 1) 2      -- the outermost part is an application of (\x -> x + 1)
"he" ++ "llo"        -- the outermost part is an application of (++)

Przepełnienia stosu

Ocena wyrażenia do postaci normalnej słabej głowy może wymagać, aby inne wyrażenia zostały ocenione do WHNF najpierw. Na przykład, aby ocenić 1 + (2 + 3) do WHNF, najpierw musimy ocenić 2 + 3. Jeśli ocena pojedynczego wyrażenia prowadzi do zbyt wielu zagnieżdżonych ocen, wynikiem jest przepełnienie stosu.

Dzieje się tak, gdy tworzy się duże wyrażenie, które nie tworzy żadnych konstruktorów danych ani lambda, dopóki duża ich część nie zostanie oceniona. Są one często spowodowane przez tego rodzaju użycie foldl:

foldl (+) 0 [1, 2, 3, 4, 5, 6]
 = foldl (+) (0 + 1) [2, 3, 4, 5, 6]
 = foldl (+) ((0 + 1) + 2) [3, 4, 5, 6]
 = foldl (+) (((0 + 1) + 2) + 3) [4, 5, 6]
 = foldl (+) ((((0 + 1) + 2) + 3) + 4) [5, 6]
 = foldl (+) (((((0 + 1) + 2) + 3) + 4) + 5) [6]
 = foldl (+) ((((((0 + 1) + 2) + 3) + 4) + 5) + 6) []
 = (((((0 + 1) + 2) + 3) + 4) + 5) + 6
 = ((((1 + 2) + 3) + 4) + 5) + 6
 = (((3 + 3) + 4) + 5) + 6
 = ((6 + 4) + 5) + 6
 = (10 + 5) + 6
 = 15 + 6
 = 21

Zauważ, jak to musi iść dość głęboko, zanim może dostać ekspresja w słabym kształcie głowy.

Możesz się zastanawiać, dlaczego Haskell nie redukuje wewnętrznych wyrażeń z wyprzedzeniem? To z powodu lenistwa Haskella. Ponieważ nie można ogólnie założyć, że każde podwyrażenie będzie potrzebne, wyrażenia są oceniane z zewnątrz w.

(GHC ma analizator ścisłości, który wykryje pewne sytuacje, w których podekspresja jest zawsze potrzebna, a następnie może ją ocenić z wyprzedzeniem. Jest to jednak tylko optymalizacja, i nie powinieneś na niej polegać, aby uchronić Cię przed przepełnieniami).

Z drugiej strony ten rodzaj wyrażenia jest całkowicie bezpieczny:]}
data List a = Cons a (List a) | Nil
foldr Cons Nil [1, 2, 3, 4, 5, 6]
 = Cons 1 (foldr Cons Nil [2, 3, 4, 5, 6])  -- Cons is a constructor, stop. 

Aby uniknąć budowania tych dużych wyrażeń, gdy wiemy, że wszystkie podwyrażenia będą musiały zostać ocenione, chcemy wymusić, aby wewnętrzne części zostały ocenione z wyprzedzeniem.

seq

seq jest specjalną funkcją, która służy do wymuszania wyrażeń do oceny. Jego semantyka jest taka, że seq x y oznacza, że ilekroć y jest jest również oceniana na słabą formę normalną głowy, x jest również oceniana na słabą formę normalną głowy.

Jest między innymi używany w definicji foldl', ścisłego wariantu foldl.

foldl' f a []     = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs

Każda iteracja foldl' zmusza akumulator do whnf. W związku z tym unika się budowania dużej ekspresji, a tym samym unika przepełnienia stosu.

foldl' (+) 0 [1, 2, 3, 4, 5, 6]
 = foldl' (+) 1 [2, 3, 4, 5, 6]
 = foldl' (+) 3 [3, 4, 5, 6]
 = foldl' (+) 6 [4, 5, 6]
 = foldl' (+) 10 [5, 6]
 = foldl' (+) 15 [6]
 = foldl' (+) 21 []
 = 21                           -- 21 is a data constructor, stop.

Ale jak wspomina przykład na HaskellWiki, to nie oszczędza cię we wszystkich przypadkach, ponieważ akumulator jest tylko oceniony na WHNF. W przykładzie akumulator jest krotką, więc wymusi tylko ocenę konstruktora krotki, a nie acc lub len.

f (acc, len) x = (acc + x, len + 1)

foldl' f (0, 0) [1, 2, 3]
 = foldl' f (0 + 1, 0 + 1) [2, 3]
 = foldl' f ((0 + 1) + 2, (0 + 1) + 1) [3]
 = foldl' f (((0 + 1) + 2) + 3, ((0 + 1) + 1) + 1) []
 = (((0 + 1) + 2) + 3, ((0 + 1) + 1) + 1)  -- tuple constructor, stop.

Aby tego uniknąć, musimy uczynić to tak, aby ocena konstruktora krotki wymusiła ocenę acc i len. Robimy to za pomocą seq.

f' (acc, len) x = let acc' = acc + x
                      len' = len + 1
                  in  acc' `seq` len' `seq` (acc', len')

foldl' f' (0, 0) [1, 2, 3]
 = foldl' f' (1, 1) [2, 3]
 = foldl' f' (3, 2) [3]
 = foldl' f' (6, 3) []
 = (6, 3)                    -- tuple constructor, stop.
 360
Author: hammar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-06 13:27:30

Sekcja o Thunks i słaba Głowa normalna forma w Haskell Wikibooks Opis lenistwa zawiera bardzo dobry opis WHNF wraz z tym pomocnym opisem:

Ocena wartości (4, [1, 2]) krok po kroku. Pierwszy etap jest całkowicie bezcenny; wszystkie kolejne formy są w WHNF, a ostatni jest również w normalnej formie.

Ocena wartości (4, [1, 2]) krok po kroku. Pierwszy etap to całkowicie bezcenne; wszystkie kolejne formy są w WHNF, a ostatnia jeden jest również w normalnej formie.

 41
Author: aculich,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-18 16:27:14

Dobre wyjaśnienie z przykładami podano w http://foldoc.org/Weak forma normalna forma normalna upraszcza nawet fragmenty wyrażenia wewnątrz abstrakcji funkcji, podczas gdy "słaba" forma normalna zatrzymuje się na abstrakcji funkcji.

Ze źródła, jeśli masz:

\ x -> ((\ y -> y+x) 2)

To jest w słaba Głowa normalna forma, ale nie Głowa normalna forma... ponieważ możliwa aplikacja utknęła wewnątrz funkcji, której nie można jeszcze ocenić.

Rzeczywista Głowa normalna forma byłaby trudna do skutecznego wdrożenia. Wymagałoby to grzebania wewnątrz funkcji. Zaletą słabej postaci normalnej jest to, że można nadal implementować funkcje jako typ nieprzezroczysty, a tym samym jest bardziej kompatybilny z skompilowanymi językami i optymalizacją.

 25
Author: Chris Smith,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-30 13:52:33

Programy Haskell są wyrażeniami i są uruchamiane przez wykonywanieewaluacji .

Aby ocenić wyrażenie, Zastąp wszystkie aplikacje funkcji ich definicjami. Kolejność, w jakiej to robisz, nie ma większego znaczenia, ale nadal jest ważna: zacznij od najbardziej oddalonej aplikacji i przejdź od lewej do prawej; nazywa się to leniwa Ocena.

Przykład:

   take 1 (1:2:3:[])
=> { apply take }
   1 : take (1-1) (2:3:[])
=> { apply (-)  }
   1 : take 0 (2:3:[])
=> { apply take }
   1 : []

Ocena zatrzymuje się, gdy nie ma już aplikacji funkcyjnych do zastąp. Wynik jest w normalnej postaci (lub zredukowanej postaci normalnej, RNF). Bez względu na to, w jakiej kolejności oceniasz wyrażenie, zawsze otrzymujesz tę samą normalną formę (ale tylko wtedy, gdy ocena zostanie zakończona).

Jest nieco inny opis dla leniwej oceny. Mianowicie, mówi, że należy ocenić wszystko słaba Głowa normalna forma tylko. Istnieją dokładnie trzy przypadki, w których wyrażenie ma być w WHNF:

  • A konstruktor: constructor expression_1 expression_2 ...
  • wbudowana funkcja ze zbyt małą ilością argumentów, jak (+) 2 lub sqrt
  • wyrażenie lambda: \x -> expression

Innymi słowy, głowa wyrażenia (tj. najbardziej oddalona aplikacja funkcji) nie może być dalej oceniana, ale argument funkcji może zawierać wyrażenia bez wartości.

Przykłady WHNF:

3 : take 2 [2,3,4]   -- outermost function is a constructor (:)
(3+1) : [4..]        -- ditto
\x -> 4+5            -- lambda expression

Uwagi

  1. "głowa" w WHNF nie odnosi się do głowy listy, ale do najbardziej oddalonych zastosowanie funkcji.
  2. czasami ludzie nazywają bezcenne wyrażenia "thunks" , ale nie sądzę, że to dobry sposób, aby to zrozumieć.
  3. Head normal form (HNF) nie ma znaczenia dla Haskell. Różni się od WHNF tym, że ciała wyrażeń lambda są również w pewnym stopniu oceniane.
 25
Author: Heinrich Apfelmus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-28 00:43:02

WHNF nie chce, aby ciało lambda było oceniane, więc

WHNF = \a -> thunk
HNF = \a -> a + c

seq chce, aby jego pierwszy argument był w WHNF, więc

let a = \b c d e -> (\f -> b + c + d + e + f) b
    b = a 2
in seq b (b 5)

Ocenia do

\d e -> (\f -> 2 + 5 + d + e + f) 2

Zamiast, co byłoby za pomocą HNF

\d e -> 2 + 5 + d + e + 2
 12
Author: marc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-28 00:41:52

Zasadniczo, Załóżmy, że masz jakiś rodzaj thunk, t.

Teraz, jeśli chcemy ocenić t na WHNF lub NHF, które są takie same z wyjątkiem funkcji, odkryjemy, że otrzymamy coś w rodzaju

t1 : t2 Gdzie t1 i t2 są thunks. W tym przypadku, t1 będzie twoim 0 (a raczej thunk do 0, bez dodatkowego unboxingu)

seq i $! evalute WHNF. Zauważ, że

f $! x = seq x (f x)
 5
Author: alternative,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-29 13:12:53