Che cos'è la forma normale della testa debole?


290

Cosa significa Weak Head Normal Form (WHNF)? Che cosa significa Head Normal form (HNF) e Normal Form (NF)?

Real World Haskell afferma:

La familiare funzione seq valuta un'espressione di ciò che chiamiamo head normal form (abbreviato HNF). Si ferma quando raggiunge il costruttore più esterno (la "testa"). Questo è distinto dalla forma normale (NF), in cui un'espressione viene completamente valutata.

Sentirai anche i programmatori Haskell riferirsi alla forma normale della testa debole (WHNF). Per i dati normali, la forma normale della testa debole è la stessa della forma normale della testa. La differenza sorge solo per le funzioni, ed è troppo astrusa per interessarci qui.

Ho letto alcune risorse e definizioni ( Haskell Wiki e Haskell Mail List e Free Dictionary ) ma non capisco. Qualcuno può forse dare un esempio o fornire una definizione di laico?

Immagino che sarebbe simile a:

WHNF = thunk : thunk

HNF = 0 : thunk 

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

In che modo seqe in ($!)relazione a WHNF e HNF?

Aggiornare

Sono ancora confuso. So che alcune delle risposte dicono di ignorare HNF. Dalla lettura delle varie definizioni sembra che non vi sia alcuna differenza tra i dati regolari in WHNF e HNF. Tuttavia, sembra che ci sia una differenza quando si tratta di una funzione. Se non ci fosse differenza, perché è seqnecessario foldl'?

Un altro punto di confusione è dal Wiki di Haskell, che afferma che si seqriduce a WHNF, e non farà nulla nel seguente esempio. Quindi dicono che devono usare seqper forzare la valutazione. Non lo sta forzando a HNF?

Codice di overflow dello stack per principianti comune:

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

Le persone che comprendono seq e la forma normale della testa debole (whnf) possono immediatamente capire cosa non va qui. (acc + x, len + 1) è già in whnf, quindi seq, che riduce un valore a whnf, non fa nulla per questo. Questo codice genererà thunk proprio come nell'esempio originale di foldl, saranno solo all'interno di una tupla. La soluzione è solo quella di forzare i componenti della tupla, ad es

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

- Haskell Wiki su Stackoverflow


1
Generalmente parliamo di WHNF e RNF. (RNF è quello che chiami NF)
alternativa il

5
@monadic Cosa significa R in RNF?
dave4420,

7
@ dave4420: Ridotto
marc

Risposte:


399

Proverò a dare una spiegazione in termini semplici. Come altri hanno sottolineato, la forma normale della testa non si applica a Haskell, quindi non la considererò qui.

Forma normale

Un'espressione in forma normale viene completamente valutata e nessuna sottoespressione può essere valutata ulteriormente (ovvero non contiene thunk non valutati).

Queste espressioni sono tutte in forma normale:

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

Queste espressioni non sono in forma normale:

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

Forma normale testa debole

Un'espressione in forma normale testa debole è stata valutata per il costruttore di dati più esterno o per l'astrazione lambda (la testa ). Le sottoespressioni possono o meno essere state valutate . Pertanto, ogni espressione di forma normale è anche in forma normale testa debole, sebbene il contrario non valga in generale.

Per determinare se un'espressione è in forma normale testa debole, dobbiamo solo guardare la parte più esterna dell'espressione. Se è un costruttore di dati o un lambda, è in forma normale testa debole. Se è un'applicazione funzionale, non lo è.

Queste espressioni sono in forma normale testa debole:

(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 (:)

Come accennato, tutte le espressioni di forma normale sopra elencate sono anche in forma normale testa debole.

Queste espressioni non sono in forma normale testa debole:

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 (++)

Stack overflow

La valutazione di un'espressione nella forma normale della testa debole può richiedere che altre espressioni siano valutate prima in WHNF. Ad esempio, per valutare 1 + (2 + 3)WHNF, dobbiamo prima valutare 2 + 3. Se la valutazione di una singola espressione porta a troppe di queste valutazioni nidificate, il risultato è un overflow dello stack.

Ciò accade quando si crea un'espressione di grandi dimensioni che non produce costruttori di dati o lambdas fino a quando non viene valutata gran parte di essa. Questi sono spesso causati da questo tipo di utilizzo di 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

Si noti come deve andare abbastanza in profondità prima che possa ottenere l'espressione in una forma normale testa debole.

Potresti chiederti, perché Haskell non riduce in anticipo le espressioni interiori? Ciò è dovuto alla pigrizia di Haskell. Poiché non si può presumere in generale che sarà necessaria ogni sottoespressione, le espressioni vengono valutate dall'esterno in.

(GHC ha un analizzatore di rigore che rileverà alcune situazioni in cui una sottoespressione è sempre necessaria e può quindi valutarla in anticipo. Tuttavia, questa è solo un'ottimizzazione e non dovresti fare affidamento su di essa per salvarti dagli overflow).

Questo tipo di espressione, d'altra parte, è completamente sicuro:

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. 

Per evitare di costruire queste grandi espressioni quando sappiamo che tutte le sottoespressioni dovranno essere valutate, vogliamo forzare le parti interne a essere valutate in anticipo.

seq

seqè una funzione speciale che viene utilizzata per forzare la valutazione delle espressioni. La sua semantica seq x ysignifica che ogni volta che yviene valutata in forma normale testa debole, xviene anche valutata in forma normale testa debole.

È tra l'altro utilizzato nella definizione di foldl', la variante rigorosa di foldl.

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

Ogni iterazione di foldl'forza l'accumulatore a WHNF. Evita quindi di creare una grande espressione e quindi evita di traboccare lo stack.

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.

Ma come menziona l'esempio su HaskellWiki, questo non ti salva in tutti i casi, poiché l'accumulatore viene valutato solo su WHNF. Nell'esempio, l'accumulatore è una tupla, quindi forza solo la valutazione del costruttore della tupla e non acco 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.

Per evitare ciò, dobbiamo fare in modo che la valutazione del costruttore tupla costringa la valutazione di acce len. Lo facciamo usando 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.

31
La forma normale della testa richiede che anche il corpo di una lambda sia ridotto, mentre la forma normale della testa debole non ha questo requisito. Così \x -> 1 + 1è WHNF ma non HNF.
Hammar,

Wikipedia afferma che HNF è "[a] il termine è nella forma normale della testa se non c'è beta-redex nella posizione della testa". Haskell è "debole" perché non contiene sottoespressioni beta-redex?

Come entrano in gioco i costruttori rigorosi di dati? Sono proprio come invocare i seqloro argomenti?
Bergi,

1
@CaptainObvious: 1 + 2 non è né NF né WHNF. Le espressioni non sono sempre in una forma normale.
Hammar,

2
@Zorobay: per stampare il risultato, GHCi finisce per valutare completamente l'espressione in NF, non solo in WHNF. Un modo per distinguere le due varianti è abilitare le statistiche di memoria con :set +s. Puoi quindi vedere che foldl' ffinisce per allocare più thunk difoldl' f' .
Hammar,

43

La sezione sulla forma normale di Thunks e Weak Head nella descrizione della pigrizia di Haskell Wikibooks fornisce una descrizione eccellente di WHNF insieme a questa utile rappresentazione:

Valutazione graduale del valore (4, [1, 2]).  Il primo stadio è completamente sottovalutato;  tutte le forme successive sono in WHNF, e anche l'ultima è in forma normale.

Valutazione passo per passo del valore (4, [1, 2]). Il primo stadio è completamente sottovalutato; tutte le forme successive sono in WHNF e anche l'ultima è in forma normale.


5
So che la gente dice di ignorare la forma normale della testa, ma puoi dare un esempio in quel diagramma che hai come appare una forma normale della testa?
CMCDragonkai,

28

I programmi Haskell sono espressioni e vengono eseguiti eseguendo una valutazione .

Per valutare un'espressione, sostituire tutte le applicazioni con le loro definizioni. L'ordine in cui lo fai non ha molta importanza, ma è comunque importante: inizia con l'applicazione più esterna e procedi da sinistra a destra; questo si chiama valutazione pigra .

Esempio:

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

La valutazione si interrompe quando non ci sono più applicazioni con funzioni da sostituire. Il risultato è in forma normale (o in forma normale ridotta , RNF). Non importa in quale ordine valuti un'espressione, finirai sempre con la stessa forma normale (ma solo se la valutazione termina).

C'è una descrizione leggermente diversa per la valutazione pigra. Vale a dire, dice che dovresti valutare tutto solo in forma normale testa debole . Esistono esattamente tre casi per un'espressione in WHNF:

  • Un costruttore: constructor expression_1 expression_2 ...
  • Una funzione integrata con troppi argomenti, come (+) 2osqrt
  • Un'espressione lambda: \x -> expression

In altre parole, il capo dell'espressione (cioè l'applicazione della funzione più esterna) non può essere valutato ulteriormente, ma l'argomento della funzione può contenere espressioni non valutate.

Esempi di WHNF:

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

Appunti

  1. La "testa" in WHNF non si riferisce alla testa di un elenco, ma all'applicazione della funzione più esterna.
  2. A volte, le persone chiamano "thunk" espressioni non valutate, ma non credo sia un buon modo per capirlo.
  3. La forma normale della testa (HNF) è irrilevante per Haskell. Si differenzia da WHNF in quanto anche i corpi delle espressioni lambda vengono valutati in una certa misura.

L'uso seqin foldl'vigore è la valutazione da WHNF a HNF?

1
@snmcdonald: No, Haskell non utilizza HNF. La valutazione seq expr1 expr2valuterà la prima espressione expr1in WHNF prima di valutare la seconda espressione expr2.
Heinrich Apfelmus,

26

Una buona spiegazione con esempi è fornita su http://foldoc.org/Weak+Head+Normal+Form normale forma di testa semplifica anche i bit di un'espressione all'interno di un'astrazione di funzione, mentre la forma normale di testa "debole" si ferma alle astrazioni di funzione .

Dalla fonte, se hai:

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

che è in forma normale testa debole, ma non in forma normale testa ... perché la possibile applicazione è bloccata all'interno di una funzione che non può ancora essere valutata.

L'attuale forma normale della testa sarebbe difficile da attuare in modo efficiente. Richiederebbe frugare all'interno delle funzioni. Quindi il vantaggio della forma normale testa debole è che puoi ancora implementare funzioni come un tipo opaco, e quindi è più compatibile con i linguaggi compilati e l'ottimizzazione.


12

Il WHNF non vuole che il corpo di lambda sia valutato, quindi

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

seq vuole che il suo primo argomento sia in WHNF, quindi

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

valuta

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

invece di, cosa sarebbe usare HNF

\d e -> 2 + 5 + d + e + 2

Oppure ho frainteso l'esempio o mescoli 1 e 2 in WHNF e HNF.
Zhen,

5

In sostanza, si supponga di avere una sorta di thunk, t.

Ora, se vogliamo valutare tWHNF o NHF, che sono gli stessi tranne per le funzioni, troveremmo che otteniamo qualcosa di simile

t1 : t2dove t1e t2sono i thunk. In questo caso, t1sarebbe il tuo 0(o meglio, un thunk a 0cui non viene dato unboxing extra)

seqe $!valutare WHNF. Nota che

f $! x = seq x (f x)

1
@snmcdonald Ignora HNF. seq afferma che quando questo viene valutato su WHNF, valuta il primo argomento su WHNF.
alternativa il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.