306 votes

Qu'est-ce que la forme normale de la tête faible ?

Qu'est-ce que Forme normale de la tête faible (WHNF) signifie ? Que signifie Tête Forme normale (HNF) et Forme normale (NF) signifie ?

Haskell dans le monde réel États :

La fonction familière seq évalue une expression à ce que nous appelons la forme normale head normale (abrégée HNF). Elle s'arrête lorsqu'elle atteint le constructeur le plus éloigné (la "tête"). Ceci est distinct de la forme normale (NF), dans laquelle dans laquelle une expression est complètement évaluée.

Vous entendrez également les programmeurs Haskell se référer à la forme normale de tête faible (weak head normal form) (WHNF). Pour les données normales, la forme normale de tête faible est la même que la forme normale de tête. normale. La différence n'apparaît que pour les fonctions, et est trop abstruse pour nous concerner ici. abstruse pour nous concerner ici.

J'ai lu quelques ressources et définitions ( Haskell Wiki et Liste de diffusion Haskell et Dictionnaire libre ) mais je ne comprends pas. Quelqu'un pourrait-il donner un exemple ou une définition pour les profanes ?

Je suppose que ce serait similaire à.. :

WHNF = thunk : thunk

HNF = 0 : thunk 

NF = 0 : 1 : 2 : 3 : []

Comment seq et ($!) sont-elles liées au WHNF et au HNF ?

Mise à jour

Je suis toujours confus. Je sais que certaines réponses disent d'ignorer le HNF. En lisant les différentes définitions, il semble qu'il n'y ait aucune différence entre les données régulières en WHNF et en HNF. Cependant, il semble qu'il y ait une différence lorsqu'il s'agit d'une fonction. S'il n'y avait pas de différence, pourquoi seq nécessaire pour foldl' ?

Un autre point de confusion provient du Haskell Wiki, qui indique que seq réduit à WHNF, et ne fera rien à l'exemple suivant. Ensuite, ils disent qu'ils doivent utiliser seq pour forcer l'évaluation. Est-ce que ce n'est pas forcer le passage à HNF ?

Code de débordement de pile commun aux débutants :

myAverage = uncurry (/) . foldl' (\(acc, len) x -> (acc+x, len+1)) (0,0)

Les personnes qui comprennent le seq et 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, donc seq, qui réduit une valeur à whnf, ne fait rien ici. Ce code va construire des thunks tout comme l'exemple original de foldl, ils seront juste à l'intérieur d'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)

- Haskell Wiki sur Stackoverflow

1 votes

On parle généralement de WHNF et de RNF. (RNF étant ce que vous appelez NF)

5 votes

@monadic Que signifie le R de RNF ?

7 votes

@dave4420 : Réduit

418voto

hammar Points 89293

Je vais essayer de donner une explication en termes simples. Comme d'autres l'ont souligné, la forme normale de tête ne s'applique pas à Haskell, je ne l'examinerai donc pas ici.

Forme normale

Une expression sous forme normale est entièrement évaluée, et aucune sous-expression ne peut être évaluée plus loin (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

Forme normale de la tête faible

Une expression en forme normale de tête faible a été évaluée jusqu'au constructeur de données ou à l'abstraction lambda le plus éloigné (l'élément tête ). Sous-expressions peut ou non avoir été évalué . Par conséquent, toute expression de forme normale est également en 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 en forme normale de tête faible, il suffit de regarder la partie la plus extérieure de l'expression. S'il s'agit d'un constructeur de données ou d'un lambda, il est en forme normale de tête faible. Si c'est une application de fonction, elle ne l'est pas.

Ces expressions sont en 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 en forme normale énumérées ci-dessus sont également en forme normale de tête faible.

Ces expressions ne sont pas dans 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 (++)

Débordements de pile

L'évaluation d'une expression en forme normale de tête faible peut nécessiter que d'autres expressions soient évaluées en WHNF d'abord. Par exemple, pour évaluer 1 + (2 + 3) à 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 pile.

Cela se produit lorsque vous construisez une grande expression qui ne produit pas de constructeurs de données ou de lambdas avant qu'une grande partie de celle-ci n'ait été évaluée. Ces problèmes sont souvent causés par ce type d'utilisation de la fonction 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 loin avant de pouvoir transformer l'expression en 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. Puisque l'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 vers l'intérieur.

(GHC a un analyseur de rigueur qui détectera certaines situations où une sous-expression est toujours nécessaire et il peut alors l'évaluer à l'avance. Il ne s'agit cependant que d'une optimisation, et vous ne devriez pas compter sur elle 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 est la suivante seq x y signifie que chaque fois que y est évalué en forme normale de tête faible, x est également évalué en forme normale de tête faible.

Il est entre autres utilisé 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 construire une grande expression, et donc de faire déborder 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, donc cela ne forcera que l'évaluation du constructeur de tuple, et pas de 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 . Pour ce faire, nous utilisons 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.

36 votes

La forme normale de tête exige que le corps d'une lambda soit également réduit, alors que la forme normale de tête faible n'a pas cette exigence. Ainsi, \x -> 1 + 1 est WHNF mais pas HNF.

0 votes

Wikipedia indique que HNF est "[a] term is in head normal form if there is no beta-redex in head position". Haskell est-il "faible" parce qu'il ne fait pas de bêta-rex des sous-expressions ?

0 votes

Comment les constructeurs de données stricts entrent-ils en jeu ? Est-ce que c'est comme appeler seq sur leurs arguments ?

43voto

aculich Points 4563

La section sur Forme normale de Thunks et de Weak Head dans les Wikibooks Haskell description de la paresse fournit une très bonne description du WHNF ainsi que cette description utile :

Evaluating the value (4,  1, 2 ) step by step. The first stage is completely unevaluated; all subsequent forms are in WHNF, and the last one is also in normal form.

Évaluation de la valeur (4, [1, 2]) étape par étape. La première étape est complètement non-évaluée ; toutes les formes suivantes sont en WHNF, et la dernière est également en forme normale.

5 votes

Je sais que les gens disent d'ignorer la forme normale de tête, mais pouvez-vous donner un exemple dans ce diagramme que vous avez à quoi ressemble une forme normale de tête ?

29voto

Heinrich Apfelmus Points 7200

Les programmes Haskell sont expressions et ils sont dirigés par des exécutants évaluation .

Pour évaluer une expression, remplacez toutes les applications des fonctions par leurs définitions. L'ordre dans lequel vous le faites n'a pas beaucoup d'importance, mais il est tout de même important : commencez par l'application la plus extérieure et procédez de gauche à droite ; c'est ce que l'on 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 n'y a plus d'applications fonctionnelles à 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 seulement si l'évaluation se termine).

Il existe une description légèrement différente pour l'évaluation paresseuse. En fait, elle dit que vous devriez évaluer tout ce qui est à forme normale de tête faible seulement. Il y a précisément trois cas pour qu'une expression soit en WHNF :

  • Un constructeur : constructor expression_1 expression_2 ...
  • Une fonction intégrée avec trop peu d'arguments, comme (+) 2 ou sqrt
  • Une expression lambda : \x -> expression

En d'autres termes, la tête de l'expression (c'est-à-dire l'application de la fonction la plus extérieure) 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

Notes

  1. La "tête" dans WHNF ne fait pas référence à la tête d'une liste, mais à l'application de fonction la plus extérieure.
  2. Parfois, les gens appellent les expressions non évaluées des "thunks", mais je ne pense pas que ce soit une bonne façon de les comprendre.
  3. Forme normale de tête (HNF) n'est pas pertinent pour Haskell. Elle diffère de la WHNF en ce que les corps des expressions lambda sont également évalués dans une certaine mesure.

0 votes

Est-ce que l'utilisation de seq sur foldl' forcer l'évaluation de WHNF à HNF ?

1 votes

@snmcdonald : Non, Haskell ne fait pas usage de HNF. Évaluation de seq expr1 expr2 évaluera la première expression expr1 en WHNF avant d'évaluer la deuxième expression expr2 .

26voto

Chris Smith Points 1310

Une bonne explication avec des exemples est donnée à l'adresse suivante http://foldoc.org/Weak+Tête+Normal+Forme La forme normale de tête simplifie même les bits d'une expression à l'intérieur d'une abstraction de fonction, tandis que la forme normale de tête "faible" s'arrête aux abstractions de fonction.

De la source, si vous avez :

\ x -> ((\ y -> y+x) 2)

qui est en forme normale de tête faible, mais pas en forme normale de tête... parce que l'application possible est coincé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 de manière efficace. Il faudrait fouiller dans les fonctions. L'avantage de la forme normale de tête faible est donc que vous pouvez toujours implémenter les fonctions en tant que type opaque, ce qui est plus compatible avec les langages compilés et l'optimisation.

12voto

marc Points 4070

Le WHNF ne veut pas que le corps des lambdas soit évalué, donc

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, ce qui serait d'utiliser HNF

\d e -> 2 + 5 + d + e + 2

0 votes

Soit je comprends mal l'exemple, soit vous mélangez 1 et 2 dans le WHNF et le HNF.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X