Je vais essayer de donner une explication en termes simples. Comme d'autres l'ont souligné, la forme normale de la tête ne s'applique pas à Haskell, donc je ne l'examinerai pas ici.
Forme normale
Une expression sous forme normale est entièrement évaluée, et aucune sous-expression n'a pu être évaluée plus avant (c'est-à-dire qu'elle ne contient aucun thunks non évalué).
Ces expressions sont toutes sous forme normale:
42
(2, "hello")
\x -> (x + 1)
Ces expressions ne sont pas sous forme 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
Forme normale de la tête faible
Une expression sous forme normale de tête faible a été évaluée pour le constructeur de données le plus à l'extérieur ou l'abstraction lambda (la tête ). Les sous-expressions peuvent ou non avoir été évaluées . Par conséquent, chaque expression de forme normale est également sous forme normale de tête faible, bien que l'inverse ne soit pas vrai en général.
Pour déterminer si une expression est sous une forme normale de tête faible, il suffit de regarder la partie la plus externe de l'expression. S'il s'agit d'un constructeur de données ou d'un lambda, il est sous forme normale de tête faible. Si c'est une application de fonction, ce n'est pas le cas.
Ces expressions sont sous forme normale de tête faible:
(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 (:)
Comme mentionné, toutes les expressions de forme normale répertoriées ci-dessus sont également sous forme normale de tête faible.
Ces expressions ne sont pas en forme normale de tête faible:
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 (++)
Débordements de pile
L'évaluation d'une expression à une forme normale de tête faible peut nécessiter que d'autres expressions soient évaluées en premier au WHNF. Par exemple, pour évaluer 1 + (2 + 3)
au WHNF, nous devons d'abord évaluer 2 + 3
. Si l'évaluation d'une seule expression conduit à un trop grand nombre de ces évaluations imbriquées, le résultat est un débordement de pile.
Cela se produit lorsque vous créez une grande expression qui ne produit aucun constructeur de données ou lambdas tant qu'une grande partie de celle-ci n'a pas été évaluée. Ceux-ci sont souvent causés par ce type d'utilisation 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
Remarquez comment il doit aller assez profondément avant qu'il ne puisse obtenir l'expression sous une forme normale de tête faible.
Vous vous demandez peut-être pourquoi Haskell ne réduit pas à l'avance les expressions intérieures? C'est à cause de la paresse de Haskell. Puisqu'on ne peut pas supposer en général que chaque sous-expression sera nécessaire, les expressions sont évaluées de l'extérieur dans.
(GHC a un analyseur de rigueur qui détectera certaines situations où une sous-expression est toujours nécessaire et il peut ensuite l'évaluer à l'avance. Ce n'est qu'une optimisation, cependant, et vous ne devriez pas vous en remettre à vous pour vous sauver des débordements).
Ce type d'expression, en revanche, est totalement sûr:
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.
Pour éviter de construire ces grandes expressions lorsque nous savons que toutes les sous-expressions devront être évaluées, nous voulons forcer les parties internes à être évaluées à l'avance.
seq
seq
est une fonction spéciale qui est utilisée pour forcer les expressions à être évaluées. Sa sémantique seq x y
signifie que chaque fois qu'il y
est évalué à une forme normale de tête faible, il x
est également évalué à une forme normale de tête faible.
C'est entre autres endroits utilisés dans la définition de foldl'
, la variante stricte de foldl
.
foldl' f a [] = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs
Chaque itération de foldl'
force l'accumulateur à WHNF. Il évite donc de construire une grande expression, et il évite donc de déborder de la pile.
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.
Mais comme le mentionne l'exemple sur HaskellWiki, cela ne vous sauve pas dans tous les cas, car l'accumulateur n'est évalué qu'en WHNF. Dans l'exemple, l'accumulateur est un tuple, il ne forcera donc que l'évaluation du constructeur de tuple, et non 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.
Pour éviter cela, nous devons faire en sorte que l'évaluation du constructeur de tuple force l'évaluation de acc
et len
. Nous le faisons en utilisant 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.