Willkommen in der Welt der faulen Bewertung.
Wenn Sie in Bezug auf eine strenge Bewertung darüber nachdenken, sieht Foldl "gut" und Foldr "schlecht" aus, da Foldl schwanzrekursiv ist, aber Foldr müsste einen Turm im Stapel bauen, damit es das letzte Element zuerst verarbeiten kann.
Eine träge Auswertung dreht jedoch den Spieß um. Nehmen Sie zum Beispiel die Definition der Kartenfunktion:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
Dies wäre nicht allzu gut, wenn Haskell eine strikte Bewertung verwenden würde, da zuerst der Schwanz berechnet und dann der Artikel vorangestellt werden müsste (für alle Artikel in der Liste). Die einzige Möglichkeit, dies effizient zu tun, besteht anscheinend darin, die Elemente in umgekehrter Reihenfolge zu erstellen.
Dank der verzögerten Auswertung von Haskell ist diese Kartenfunktion jedoch tatsächlich effizient. Listen in Haskell können als Generatoren betrachtet werden, und diese Zuordnungsfunktion generiert ihr erstes Element, indem f auf das erste Element der Eingabeliste angewendet wird. Wenn ein zweites Element benötigt wird, wird das gleiche erneut ausgeführt (ohne zusätzlichen Speicherplatz zu benötigen).
Es stellt sich heraus, dass map
dies beschrieben werden kann mit foldr
:
map f xs = foldr (\x ys -> f x : ys) [] xs
Es ist schwer zu sagen, wenn man es sich ansieht, aber eine faule Bewertung setzt ein, weil foldr sofort f
sein erstes Argument vorbringen kann :
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
Da das f
definierte von map
das erste Element der Ergebnisliste nur mit dem ersten Parameter zurückgeben kann, kann die Falte in konstantem Raum träge arbeiten.
Jetzt beißt die faule Bewertung zurück. Versuchen Sie beispielsweise, sum [1..1000000] auszuführen. Es ergibt sich ein Stapelüberlauf. Warum sollte es? Es sollte nur von links nach rechts ausgewertet werden, oder?
Schauen wir uns an, wie Haskell es bewertet:
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
sum = foldl (+) 0
sum [1..1000000] = foldl (+) 0 [1..1000000]
= foldl (+) ((+) 0 1) [2..1000000]
= foldl (+) ((+) ((+) 0 1) 2) [3..1000000]
= foldl (+) ((+) ((+) ((+) 0 1) 2) 3) [4..1000000]
...
= (+) ((+) ((+) (...) 999999) 1000000)
Haskell ist zu faul, um die Ergänzungen auszuführen. Stattdessen endet es mit einem Turm von nicht bewerteten Thunks, die gezwungen werden müssen, eine Nummer zu bekommen. Der Stapelüberlauf tritt während dieser Auswertung auf, da er tief rekursiv sein muss, um alle Thunks auszuwerten.
Glücklicherweise gibt es in Data.List eine spezielle Funktion foldl'
, die streng funktioniert. foldl' (+) 0 [1..1000000]
wird nicht überlaufen. (Anmerkung: Ich habe versucht , Ersatz foldl
mit foldl'
in den Test, aber es machte es tatsächlich langsamer ausgeführt.)
a
alsa = (:)