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 y
significa che ogni volta che y
viene valutata in forma normale testa debole, x
viene 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 acc
o 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 acc
e 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.