Vou tentar dar uma explicação em termos simples. Como outros já apontaram, a forma normal da cabeça não se aplica a Haskell, então não a considerarei aqui.
Forma normal
Uma expressão na forma normal é totalmente avaliada e nenhuma subexpressão pode ser avaliada mais (ou seja, não contém thunks não avaliados).
Essas expressões estão todas na forma normal:
42
(2, "hello")
\x -> (x + 1)
Essas expressões não estão na forma normal:
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 normal de cabeça fraca
Uma expressão na forma normal de cabeça fraca foi avaliada para o construtor de dados mais externo ou a abstração lambda (a cabeça ). As subexpressões podem ou não ter sido avaliadas . Portanto, toda expressão de forma normal também está na forma normal de cabeça fraca, embora o oposto não seja válido em geral.
Para determinar se uma expressão está na forma normal de cabeça fraca, precisamos apenas observar a parte mais externa da expressão. Se é um construtor de dados ou um lambda, está na forma normal de cabeça fraca. Se é um aplicativo de função, não é.
Essas expressões estão na forma normal de cabeça fraca:
(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 (:)
Como mencionado, todas as expressões de forma normal listadas acima também estão na forma normal de cabeça fraca.
Essas expressões não estão na forma normal de cabeça fraca:
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 (++)
Estouros de pilha
A avaliação de uma expressão para a forma normal de cabeça fraca pode exigir que outras expressões sejam avaliadas primeiro pelo WHNF. Por exemplo, para avaliar o 1 + (2 + 3)
WHNF, primeiro precisamos avaliar 2 + 3
. Se a avaliação de uma única expressão levar a muitas dessas avaliações aninhadas, o resultado será um estouro de pilha.
Isso acontece quando você constrói uma expressão grande que não produz nenhum construtor de dados ou lambdas até que uma grande parte dela tenha sido avaliada. Isso geralmente é causado por esse tipo de uso de 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
Observe como ele precisa se aprofundar bastante antes de conseguir expressar a expressão na forma normal da cabeça fraca.
Você pode se perguntar: por que Haskell não reduz as expressões internas antes do tempo? Isso é por causa da preguiça de Haskell. Como não se pode presumir em geral que toda subexpressão será necessária, as expressões são avaliadas de fora para dentro.
(O GHC possui um analisador de rigidez que detecta algumas situações em que uma subexpressão é sempre necessária e pode avaliá-la antes do tempo. Entretanto, isso é apenas uma otimização e você não deve contar com isso para evitar transbordamentos).
Este tipo de expressão, por outro lado, é completamente seguro:
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.
Para evitar a construção dessas grandes expressões quando sabemos que todas as subexpressões terão que ser avaliadas, queremos forçar as partes internas a serem avaliadas com antecedência.
seq
seq
é uma função especial usada para forçar expressões a serem avaliadas. Sua semântica é que seq x y
significa que sempre que y
é avaliada para a forma normal da cabeça fraca, x
também é avaliada para a forma normal da cabeça fraca.
Está entre outros lugares usados na definição de foldl'
, a variante estrita de foldl
.
foldl' f a [] = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs
Cada iteração foldl'
força o acumulador ao WHNF. Portanto, evita construir uma expressão grande e, portanto, evita o transbordamento da pilha.
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.
Porém, como o exemplo do HaskellWiki menciona, isso não salva você em todos os casos, pois o acumulador é avaliado apenas como WHNF. No exemplo, o acumulador é uma tupla, portanto, forçará apenas a avaliação do construtor da tupla, e não acc
ou 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.
Para evitar isso, devemos fazer com que a avaliação do construtor da tupla force a avaliação de acc
e len
. Fazemos isso 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.