Trataré de dar una explicación en términos simples. Como otros han señalado, la forma normal de la cabeza no se aplica a Haskell, por lo que no lo consideraré aquí.
Forma normal
Una expresión en forma normal se evalúa completamente, y no se puede evaluar más la subexpresión (es decir, no contiene thunks no evaluados).
Estas expresiones están todas en forma normal:
42
(2, "hello")
\x -> (x + 1)
Estas expresiones no están en 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
Cabeza débil de forma normal
Se ha evaluado una expresión en forma normal de cabeza débil para el constructor de datos más externo o la abstracción lambda (la cabeza ). Las subexpresiones pueden o no haber sido evaluadas . Por lo tanto, cada expresión de forma normal también está en forma normal de cabeza débil, aunque lo contrario no se cumple en general.
Para determinar si una expresión está en forma normal de cabeza débil, solo tenemos que mirar la parte más externa de la expresión. Si es un constructor de datos o una lambda, está en forma normal de cabeza débil. Si es una aplicación de función, no lo es.
Estas expresiones están en forma normal de cabeza débil:
(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 se mencionó, todas las expresiones de forma normal enumeradas anteriormente también están en forma normal de cabeza débil.
Estas expresiones no están en forma normal de cabeza débil:
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 (++)
La pila se desborda
La evaluación de una expresión en forma normal de cabeza débil puede requerir que otras expresiones se evalúen primero a WHNF. Por ejemplo, para evaluar 1 + (2 + 3)
a WHNF, primero tenemos que evaluar 2 + 3
. Si evaluar una sola expresión lleva a demasiadas de estas evaluaciones anidadas, el resultado es un desbordamiento de la pila.
Esto sucede cuando construye una expresión grande que no produce ningún constructor de datos o lambdas hasta que se haya evaluado una gran parte de ella. Estos a menudo son causados por este 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 cómo tiene que ir bastante profundo antes de que pueda obtener la expresión en forma normal de cabeza débil.
Te preguntarás, ¿por qué Haskell no reduce las expresiones internas antes de tiempo? Eso se debe a la pereza de Haskell. Como no se puede suponer en general que se necesitará cada subexpresión, las expresiones se evalúan desde afuera hacia adentro.
(GHC tiene un analizador de rigurosidad que detectará algunas situaciones en las que siempre se necesita una subexpresión y luego puede evaluarla con anticipación. Sin embargo, esto es solo una optimización y no debe confiar en ella para evitar desbordamientos).
Este tipo de expresión, por otro lado, es 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 construir estas expresiones grandes cuando sabemos que todas las subexpresiones tendrán que ser evaluadas, queremos forzar a las partes internas a ser evaluadas con anticipación.
seq
seq
es una función especial que se usa para forzar que se evalúen expresiones. Su semántica seq x y
significa que siempre que y
se evalúa en forma normal de cabeza débil, x
también se evalúa en forma normal de cabeza débil.
Se encuentra entre otros lugares utilizados en la definición de foldl'
, la variante estricta de foldl
.
foldl' f a [] = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs
Cada iteración de foldl'
fuerza el acumulador a WHNF. Por lo tanto, evita construir una expresión grande y, por lo tanto, evita desbordar la pila.
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.
Pero como menciona el ejemplo en HaskellWiki, esto no lo salva en todos los casos, ya que el acumulador solo se evalúa a WHNF. En el ejemplo, el acumulador es una tupla, por lo que solo forzará la evaluación del constructor de tuplas, y no 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.
Para evitar esto, debemos hacerlo de manera que la evaluación del constructor de tuplas fuerce la evaluación de acc
y len
. Hacemos esto 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.