Was bedeutet Schwache Kopfnormalform (WHNF)? Was bedeutet Kopf Normalform (HNF) und Normalform (NF) bedeuten?
Real World Haskell besagt:
Die vertraute seq-Funktion wertet einen Ausdruck in der sogenannten Kopfnormalform (abgekürzt HNF) aus. Es stoppt, sobald es den äußersten Konstruktor (den „Kopf“) erreicht. Dies unterscheidet sich von der Normalform (NF), in der ein Ausdruck vollständig ausgewertet wird.
Sie werden auch Haskell-Programmierer hören, die sich auf die Normalform (WHNF) für schwache Köpfe beziehen. Bei normalen Daten ist die schwache Kopfnormalform dieselbe wie die Kopfnormalform. Der Unterschied ergibt sich nur für Funktionen und ist zu abstrus, um uns hier zu beschäftigen.
Ich habe einige Ressourcen und Definitionen gelesen ( Haskell Wiki und Haskell Mail List und Free Dictionary ), aber ich verstehe es nicht. Kann jemand vielleicht ein Beispiel geben oder eine Laiendefinition liefern?
Ich vermute, es wäre ähnlich wie:
WHNF = thunk : thunk
HNF = 0 : thunk
NF = 0 : 1 : 2 : 3 : []
Wie machen seq
und ($!)
beziehen sich auf WHNF und HNF?
Ich bin immer noch verwirrt. Ich weiß, dass einige der Antworten sagen, HNF zu ignorieren. Beim Lesen der verschiedenen Definitionen scheint es keinen Unterschied zwischen regulären Daten in WHNF und HNF zu geben. Allerdings scheint es einen Unterschied zu geben, wenn es um eine Funktion geht. Wenn es keinen Unterschied gab, warum ist seq
notwendig für foldl'
?
Ein weiterer Punkt der Verwirrung stammt aus dem Haskell-Wiki, das besagt, dass seq
auf WHNF reduziert wird und im folgenden Beispiel nichts unternimmt. Dann sagen sie, dass sie seq
verwenden müssen, um die Auswertung zu erzwingen. Zwingt das HNF nicht dazu?
Häufiger Neuling-Stack-Überlaufcode:
myAverage = uncurry (/) . foldl' (\(acc, len) x -> (acc+x, len+1)) (0,0)
Menschen, die die normale Form von seq und schwachem Kopf (whnf) verstehen, können sofort verstehen, was hier falsch läuft. (acc + x, len + 1) ist bereits in whnf, also hat seq, das einen Wert auf whnf reduziert, nichts damit zu tun. Dieser Code baut Thunks auf, genau wie das ursprüngliche Foldl-Beispiel. Sie befinden sich nur in einem Tupel. Die Lösung besteht lediglich darin, die Komponenten des Tupels, z.
myAverage = uncurry (/) . foldl' (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)
Ich werde versuchen, eine Erklärung in einfachen Worten zu geben. Wie bereits erwähnt, trifft die normale Kopfform auf Haskell nicht zu, weshalb ich hier nicht darauf eingehen werde.
Ein Ausdruck in normaler Form wird vollständig ausgewertet, und kein Unterausdruck kann weiter ausgewertet werden (d. H. Er enthält keine nicht ausgewerteten Thunks).
Diese Ausdrücke sind alle in normaler Form:
42
(2, "hello")
\x -> (x + 1)
Diese Ausdrücke sind nicht in normaler Form:
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
Ein Ausdruck in schwacher Kopfnormalform wurde für den äußersten Datenkonstruktor oder die Lambda-Abstraktion (Kopf) ausgewertet. Unterausdrücke können ausgewertet worden sein oder nicht. Daher hat jeder Ausdruck in normaler Form auch eine schwache Kopfnormalform, obwohl das Gegenteil im Allgemeinen nicht zutrifft.
Um festzustellen, ob ein Ausdruck eine schwache Kopfnormalform hat, müssen wir nur den äußersten Teil des Ausdrucks betrachten. Wenn es sich um einen Datenkonstruktor oder ein Lambda handelt, weist es eine normale Kopfschwäche auf. Wenn es sich um eine Funktionsanwendung handelt, ist dies nicht der Fall.
Diese Ausdrücke haben eine schwache Kopfnormalform:
(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 (:)
Wie bereits erwähnt, haben alle oben aufgeführten Normalformausdrücke auch eine Normalform mit schwachem Kopf.
Diese Ausdrücke haben keine schwache Kopfnormalform:
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 (++)
Die Auswertung eines Ausdrucks in schwacher Kopfnormalform erfordert möglicherweise, dass andere Ausdrücke zuerst als WHNF ausgewertet werden. Um beispielsweise 1 + (2 + 3)
zu WHNF auszuwerten, müssen wir zuerst 2 + 3
Auswerten. Wenn die Auswertung eines einzelnen Ausdrucks zu vielen dieser verschachtelten Auswertungen führt, ist das Ergebnis ein Stapelüberlauf.
Dies passiert, wenn Sie einen großen Ausdruck erstellen, der keine Datenkonstruktoren oder Lambdas erzeugt, bis ein großer Teil davon ausgewertet wurde. Diese werden häufig durch diese Art der Verwendung von foldl
verursacht:
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
Beachten Sie, wie tief es gehen muss, bevor es den Ausdruck in die normale Form eines schwachen Kopfes bringen kann.
Sie fragen sich vielleicht, warum Haskell die inneren Ausdrücke nicht vorzeitig reduziert? Das liegt an Haskells Faulheit. Da nicht generell davon ausgegangen werden kann, dass jeder Teilausdruck benötigt wird, werden Ausdrücke von außen nach innen ausgewertet.
(GHC verfügt über einen Strenge-Analysator, der Situationen erkennt, in denen immer ein Teilausdruck benötigt wird, und diesen vorab auswertet. Dies ist jedoch nur eine Optimierung, und Sie sollten sich nicht darauf verlassen, dass Sie vor Überläufen geschützt sind.).
Diese Art von Ausdruck ist dagegen völlig ungefährlich:
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.
Um zu vermeiden, dass diese großen Ausdrücke erstellt werden, wenn wir wissen, dass alle Unterausdrücke ausgewertet werden müssen, möchten wir erzwingen, dass die inneren Teile vorab ausgewertet werden.
seq
seq
ist eine spezielle Funktion, mit der die Auswertung von Ausdrücken erzwungen wird. Die Semantik besagt, dass seq x y
Bedeutet, dass jedes Mal, wenn y
als Normalform für schwache Köpfe ausgewertet wird, x
auch als Normalform für schwache Köpfe ausgewertet wird.
Es wird unter anderem in der Definition von foldl'
, Der strengen Variante von foldl
, verwendet.
foldl' f a [] = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs
Jede Iteration von foldl'
Zwingt den Akkumulator auf WHNF. Es wird daher vermieden, einen großen Ausdruck aufzubauen, und es wird daher ein Überlaufen des Stapels vermieden.
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.
Wie im Beispiel in HaskellWiki erwähnt, werden Sie dadurch jedoch nicht in allen Fällen gespart, da der Akku nur nach WHNF ausgewertet wird. In diesem Beispiel ist der Akkumulator ein Tupel, sodass nur die Auswertung des Tupel-Konstruktors erzwungen wird und nicht acc
oder 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.
Um dies zu vermeiden, müssen wir den Tuple-Konstruktor so einstellen, dass er die Auswertung von acc
und len
erzwingt. Wir tun dies mit 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.
Der Abschnitt über Thunks and Weak Head Normal Form in den Haskell Wikibooks Beschreibung der Faulheit bietet eine sehr gute Beschreibung der WHNF zusammen mit dieser hilfreichen Darstellung:
Werten Sie den Wert (4, [1, 2]) schrittweise aus. Die erste Stufe ist völlig unbewertet; Alle nachfolgenden Formulare sind in WHNF, und das letzte ist auch in normaler Form.
Haskell-Programme sind Ausdrücke und werden durch Ausführen einer Auswertung ausgeführt.
Ersetzen Sie zum Auswerten eines Ausdrucks alle Funktionsanwendungen durch ihre Definitionen. Die Reihenfolge, in der Sie dies tun, spielt keine Rolle, aber es ist immer noch wichtig: Beginnen Sie mit der äußersten Anwendung und gehen Sie von links nach rechts vor; Dies nennt man Lazy Evaluation .
Beispiel:
take 1 (1:2:3:[])
=> { apply take }
1 : take (1-1) (2:3:[])
=> { apply (-) }
1 : take 0 (2:3:[])
=> { apply take }
1 : []
Die Auswertung wird beendet, wenn keine Funktionsanwendungen mehr zu ersetzen sind. Das Ergebnis ist in Normalform (oder reduzierter Normalform , RNF). Unabhängig davon, in welcher Reihenfolge Sie einen Ausdruck bewerten, erhalten Sie immer dieselbe normale Form (jedoch nur, wenn die Auswertung endet).
Es gibt eine etwas andere Beschreibung für die Lazy Evaluation. Es heißt nämlich, dass Sie alles nur mit schwache Kopfnormalform bewerten sollten. Es gibt genau drei Fälle für einen Ausdruck in WHNF:
constructor expression_1 expression_2 ...
(+) 2
oder sqrt
\x -> expression
Mit anderen Worten, der Kopf des Ausdrucks (d. H. Die äußerste Funktionsanwendung) kann nicht weiter ausgewertet werden, aber das Funktionsargument kann unbewertete Ausdrücke enthalten.
Beispiele für WHNF:
3 : take 2 [2,3,4] -- outermost function is a constructor (:)
(3+1) : [4..] -- ditto
\x -> 4+5 -- lambda expression
Anmerkungen
Eine gute Erklärung mit Beispielen finden Sie unter http://foldoc.org/Weak+Head+Normal+Form Die normale Kopfform vereinfacht sogar die Teile eines Ausdrucks innerhalb einer Funktionsabstraktion, während "schwach" Kopfnormalform bleibt bei Funktionsabstraktionen stehen.
Aus der Quelle, wenn Sie:
\ x -> ((\ y -> y+x) 2)
das ist in schwacher Kopfnormalform, aber nicht in Kopfnormalform ... weil die mögliche Anwendung in einer Funktion steckt, die noch nicht ausgewertet werden kann.
Tatsächliche Kopfnormalform wäre schwierig effizient umzusetzen. Es würde das Herumstöbern innerhalb von Funktionen erfordern. Der Vorteil der Normalform mit schwachem Kopf besteht also darin, dass Sie Funktionen immer noch als undurchsichtigen Typ implementieren können, und daher besser mit kompilierten Sprachen und Optimierungen kompatibel sind.
Die WHNF möchte daher nicht, dass der Körper von Lambdas bewertet wird
WHNF = \a -> thunk
HNF = \a -> a + c
seq
möchte, dass sein erstes Argument in WHNF steht
let a = \b c d e -> (\f -> b + c + d + e + f) b
b = a 2
in seq b (b 5)
bewertet zu
\d e -> (\f -> 2 + 5 + d + e + f) 2
anstatt, was würde HNF verwenden
\d e -> 2 + 5 + d + e + 2
Angenommen, Sie haben eine Art Thunk, t
.
Wenn wir nun t
zu WHNF oder NHF auswerten möchten, die bis auf Funktionen gleich sind, stellen wir fest, dass wir so etwas wie erhalten
t1 : t2
woher t1
und t2
sind Thunks. In diesem Fall, t1
wäre dein 0
(oder besser gesagt, ein Thunk an 0
kein extra Unboxing gegeben)
seq
und $!
WHNF auswerten. Beachten Sie, dass
f $! x = seq x (f x)