Ich werde versuchen, eine Erklärung in einfachen Worten zu geben. Wie andere bereits betont haben, gilt die normale Kopfform nicht für Haskell, daher werde ich sie hier nicht berücksichtigen.
Normalform
Ein Ausdruck in normaler Form wird vollständig ausgewertet, und es kann kein Unterausdruck weiter ausgewertet werden (dh 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
Normale Form des schwachen Kopfes
Ein Ausdruck in schwacher Kopfnormalform wurde für den äußersten Datenkonstruktor oder die Lambda-Abstraktion (den Kopf ) ausgewertet . Unterausdrücke können ausgewertet worden sein oder nicht . Daher liegt jeder Ausdruck in normaler Form auch in normaler Form mit schwachem Kopf vor, 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, liegt die normale Kopfform vor. Wenn es sich um eine Funktionsanwendung handelt, ist dies nicht der Fall.
Diese Ausdrücke sind in schwacher 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, liegen alle oben aufgeführten Normalformausdrücke auch in schwacher Kopfnormalform vor.
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 (++)
Stapel läuft über
Das Auswerten eines Ausdrucks in eine normale Form mit schwachem Kopf kann erfordern, dass andere Ausdrücke zuerst in WHNF ausgewertet werden. Um beispielsweise 1 + (2 + 3)
WHNF zu bewerten , müssen wir zuerst bewerten2 + 3
. Wenn die Auswertung eines einzelnen Ausdrucks zu zu vielen dieser verschachtelten Auswertungen führt, ist das Ergebnis ein Stapelüberlauf.
Dies geschieht, 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 Folgendem verursacht 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
Beachten Sie, dass es ziemlich tief gehen muss, bevor es den Ausdruck in eine normale Form mit schwachem Kopf bringen kann.
Sie fragen sich vielleicht, warum Haskell die inneren Ausdrücke nicht im Voraus reduziert? Das liegt an Haskells Faulheit. Da generell nicht davon ausgegangen werden kann, dass jeder Unterausdruck benötigt wird, werden Ausdrücke von außen nach innen ausgewertet.
(GHC verfügt über einen Strenge-Analysator, der einige Situationen erkennt, in denen immer ein Unterausdruck erforderlich ist, und dieser dann vorab auswerten kann. Dies ist jedoch nur eine Optimierung, und Sie sollten sich nicht darauf verlassen, um Überläufe zu vermeiden.)
Diese Art des Ausdrucks ist dagegen völlig sicher:
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 die Auswertung der inneren Teile im Voraus erzwingen.
seq
seq
ist eine spezielle Funktion, mit der die Auswertung von Ausdrücken erzwungen wird. Seine Semantik seq x y
bedeutet, dass jedes Mal, wenn y
es als schwache Kopfnormalform bewertet wird, auch als schwache Kopfnormalform bewertet x
wird.
Es wird unter anderem in der Definition von verwendet foldl'
der strengen Variante von verwendet foldl
.
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 zu 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 nicht in allen Fällen gerettet, da der Akkumulator nur für WHNF ausgewertet wird. In diesem Beispiel ist der Akkumulator ein Tupel, sodass nur die Auswertung des Tupelkonstruktors erzwungen wird und nichtacc
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 dafür sorgen, dass die Auswertung des Tupelkonstruktors die Auswertung von acc
und erzwingt len
. 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.