Que signifie forme normale de tête faible (WHNF)? Que fait tête forme normale (HNF) et forme normale (NF) signifier?
Real World Haskell déclare:
La fonction seq familière évalue une expression sous ce que nous appelons la forme normale principale (HNF abrégé). Il s'arrête une fois qu'il atteint le constructeur le plus à l'extérieur (la "tête"). Ceci est distinct de la forme normale (NF), dans laquelle une expression est complètement évaluée.
Vous entendrez également les programmeurs Haskell se référer à la forme normale de la tête faible (WHNF). Pour les données normales, la forme normale de tête faible est identique à la forme normale de tête. La différence ne se pose que pour les fonctions et est trop abstruse pour nous concerner ici.
J'ai lu quelques ressources et définitions ( Wiki Haskell et Liste de diffusion Haskell et Dictionnaire gratuit ), mais je ne comprends pas. Quelqu'un peut-il peut-être donner un exemple ou donner une définition profane?
Je suppose que ce serait semblable à:
WHNF = thunk : thunk
HNF = 0 : thunk
NF = 0 : 1 : 2 : 3 : []
Comment seq
et ($!)
concerne-t-il les FNH et les FNS?
Je suis toujours confus. Je sais que certaines des réponses disent d'ignorer le HNF. À la lecture des différentes définitions, il semble qu’il n’y ait pas de différence entre les données ordinaires du Fonds d’affectation spéciale pour le désert de Vancouver et celles du Fonds d’accompagnement naturel. Cependant, il semble qu'il y ait une différence lorsqu'il s'agit d'une fonction. S'il n'y a pas eu de différence, pourquoi seq
est-il nécessaire pour foldl'
?
Un autre point de confusion provient du wiki Haskell, qui indique que seq
se réduit à WHNF et ne fait rien à l'exemple suivant. Ensuite, ils disent qu'ils doivent utiliser seq
pour forcer l'évaluation. N'est-ce pas le forcer à HNF?
Code de débordement de pile de débutants commun:
myAverage = uncurry (/) . foldl' (\(acc, len) x -> (acc+x, len+1)) (0,0)
Les personnes qui comprennent la forme normale de tête faible (whnf) peuvent immédiatement comprendre ce qui ne va pas ici. (acc + x, len + 1) est déjà dans whnf, alors seq, qui réduit une valeur à whnf, ne fait rien à cela. Ce code construira des thunks exactement comme dans l'exemple de foldl original, ils seront simplement dans un tuple. La solution consiste simplement à forcer les composants du tuple, par exemple.
myAverage = uncurry (/) . foldl' (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)
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, je ne l'examinerai donc pas ici.
Une expression sous forme normale est entièrement évaluée et aucune sous-expression ne peut être évaluée plus avant (c’est-à-dire qu’elle ne contient pas de thunks non évalués).
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
Une expression sous forme normale de tête faible a été évaluée par rapport au constructeur de données le plus externe ou à une abstraction lambda (le tête). Sous-expressions peut avoir été évalué ou non. Par conséquent, toute expression de forme normale est également sous forme de tête normale faible, bien que le contraire ne soit pas vrai en général.
Pour déterminer si une expression est sous la forme normale de tête faible, il suffit de regarder la partie la plus externe de l'expression. Si c'est un constructeur de données ou un lambda, c'est sous la forme normale de la tête faible. Si c'est une application de fonction, ce n'est pas.
Ces expressions sont sous la forme normale de la 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 énumérées ci-dessus sont également sous forme normale à tête faible.
Ces expressions ne sont pas sous la forme normale de la 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 (++)
L'évaluation d'une expression en tête de forme normale peut nécessiter que d'autres expressions soient d'abord évaluées en WHNF. Par exemple, pour évaluer 1 + (2 + 3)
en WHNF, nous devons d’abord évaluer 2 + 3
. Si l'évaluation d'une seule expression entraîne un trop grand nombre de ces évaluations imbriquées, il en résulte un dépassement de capacité de la pile.
Cela se produit lorsque vous créez une grande expression qui ne produit aucun constructeur de données ni lambda tant qu'une grande partie de celle-ci n'a pas été évaluée. Celles-ci sont souvent causées 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 puisse obtenir l'expression dans la forme normale de tête faible.
Vous vous demandez peut-être pourquoi Haskell ne réduit pas les expressions internes à l’avance? C'est à cause de la paresse de Haskell. Comme 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 dispose d'un analyseur de rigueur qui détectera certaines situations dans lesquelles une sous-expression est toujours nécessaire et peut ensuite l'évaluer à l'avance. Il ne s'agit toutefois que d'une optimisation et vous ne devez pas vous fier à elle pour vous éviter 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 l'évaluation des parties internes à l'avance.
seq
seq
est une fonction spéciale utilisée pour forcer l'évaluation d'expressions. Sa sémantique est que seq x y
Signifie que chaque fois que y
est évalué à une forme normale de tête faible, 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. Cela évite donc de créer une grande expression et donc de ne pas surcharger 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 de HaskellWiki, cela ne vous sauve pas dans tous les cas, car l'accumulateur est uniquement évalué en WHNF. Dans l'exemple, l'accumulateur est un tuple, il ne forcera donc que l'évaluation du constructeur Tuple et non pas 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 Tuple force l'évaluation de acc
et de len
. Nous faisons cela 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.
La section sur Forme normale de Thunks and Weak Head dans les Wikibooks de Haskell description de la paresse fournit une très bonne description de WHNF ainsi que cette description utile:
Évaluation de la valeur (4, [1, 2]) pas à pas. La première étape est complètement non évaluée; tous les formulaires suivants sont au format WHNF et le dernier est également au format normal.
Les programmes Haskell sont des expressions et sont exécutés en effectuant une évaluation .
Pour évaluer une expression, remplacez toutes les applications de fonction par leurs définitions. L'ordre dans lequel vous faites cela importe peu, mais cela reste important: commencez par l'application la plus externe et procédez de gauche à droite; cela s'appelle évaluation paresseuse .
Exemple:
take 1 (1:2:3:[])
=> { apply take }
1 : take (1-1) (2:3:[])
=> { apply (-) }
1 : take 0 (2:3:[])
=> { apply take }
1 : []
L'évaluation s'arrête lorsqu'il ne reste plus aucune application de fonction à remplacer. Le résultat est en forme normale (ou forme normale réduite , RNF). Quel que soit l'ordre dans lequel vous évaluez une expression, vous obtiendrez toujours la même forme normale (mais uniquement si l'évaluation se termine).
Il existe une description légèrement différente pour l'évaluation paresseuse. À savoir, il est dit que vous devriez tout évaluer pour forme normale de la tête faible seulement. Il y a précisément trois cas pour qu'une expression soit en WHNF:
constructor expression_1 expression_2 ...
(+) 2
ou sqrt
\x -> expression
En d'autres termes, la tête de l'expression (c'est-à-dire l'application de la fonction la plus externe) ne peut plus être évaluée, mais l'argument de la fonction peut contenir des expressions non évaluées.
Exemples de WHNF:
3 : take 2 [2,3,4] -- outermost function is a constructor (:)
(3+1) : [4..] -- ditto
\x -> 4+5 -- lambda expression
Remarques
Une bonne explication avec des exemples est donnée à l'adresse http://foldoc.org/Weak+Head+Normal+Form La forme normale de la tête simplifie même les bits d'une expression à l'intérieur d'une abstraction de fonction, tandis que "faible" head forme normale s'arrête aux abstractions de fonction.
De la source, si vous avez:
\ x -> ((\ y -> y+x) 2)
c'est sous la forme normale de la tête faible, mais pas sous la forme normale de la tête ... parce que l'application possible est bloquée à l'intérieur d'une fonction qui ne peut pas encore être évaluée.
La forme normale de la tête réelle serait difficile à mettre en œuvre efficacement. Cela nécessiterait de fouiller dans les fonctions. Ainsi, l’avantage de la forme normale à tête faible est que vous pouvez toujours implémenter des fonctions en tant que type opaque, ce qui le rend plus compatible avec les langages compilés et l’optimisation.
Le WHNF ne veut pas que le corps de lambdas soit évalué, alors
WHNF = \a -> thunk
HNF = \a -> a + c
seq
veut que son premier argument soit en WHNF, donc
let a = \b c d e -> (\f -> b + c + d + e + f) b
b = a 2
in seq b (b 5)
évalue à
\d e -> (\f -> 2 + 5 + d + e + f) 2
au lieu de, qu'est-ce qui utiliserait HNF
\d e -> 2 + 5 + d + e + 2
En gros, supposons que vous ayez une sorte de thunk, t
.
Maintenant, si nous voulons évaluer t
en WHNF ou NHF, qui sont les mêmes sauf pour les fonctions, nous constaterions que nous obtenons quelque chose comme:
t1 : t2
où t1
et t2
sont des thunks. Dans ce cas, t1
serait votre 0
(ou plutôt, un thunk à 0
donné pas de déballage supplémentaire)
seq
et $!
evalute WHNF. Notez que
f $! x = seq x (f x)