"Lazy evaluation"


[Inhalt]...[Einleitung]...[Strikte Auswertung]


2. Nicht-strikte Auswertung


2.1 "Lazy evaluation" oder "Call-by-need"

In "Haskell" werden Ausdrücke grundsätzlich nicht strikt ausgewertet. Ein (Teil-)Ausdruck wird erst durch seinen Wert ersetzt, wenn dieser zum Beispiel für einen arithmetischen Vergleich benötigt wird. Demzufolge wird auch ein als aktueller Parameter einer Funktion übergebener Ausdruck erst ausgewertet, wenn sein Wert innerhalb des Funktionsrumpfes verwendet wird. Wurde der Wert einmal berechnet, werden alle namentlichen Vorkommen des Ausdrucks durch seinen Wert ersetzt. Ein einfaches Beispiel verdeutlicht diese Strategie.

>> const1 a = 1

Definiert ist eine konstante Funktion "const1" mit einem beliebigen Parameter "a" und dem Rückgabewert 1. Der Rückgabewert folgender beispielhafter Funktionsaufrufe ist also immer 1.

(Pseudocode)
>> const1 10 => 1
>> const1 (1+4) => 1
>> const1 'A' => 1
>> const1 (1/0) => 1

Für das Ergebnis der Funktion ist der Wert des aktuellen Parameters irrelevant. Er wird von einem Interpreter deshalb nie ausgewertet. Aus diesem Grund führt der letzte Aufruf mit der Division durch Null als Parameter nicht zu einem Fehler.

Solange also der Wert eines Ausdrucks (einer Funktion) nicht benötigt wird, behandelt "Haskell" diesen als eine Definition. Als solche werden auch entsprechende Teilausdrücke behandelt.

2.2 Hintergrund

Definierte Teilausdrücke werden innerhalb von Ausdrücken oder als aktuelle Parameter zunächst nur durch einen Namen identifiziert. Aufgrund der Verwendung der Namen in Operationen muss der Interpreter entscheiden, ob der Wert des Ausdrucks für das Ergebnis der Operation relevant ist. Zu diesem Zweck werden definierte Ausdrücke auf Basis des Lambda-Kalküls zerlegt. Die resultierenden "Unbekannten" sind Bezeichner für Teilausdrücke (Funktionen). Das folgende erweiterte Beispiel zeigt, dass die Analyse des Umfeldes von Ausdrücken recht komplex werden kann.

2.3 Ein Anwendungsbeispiel

Als Beispiel wird einfache dreidimensionaler Vektortyp definiert. Für die Verarbeitung werden Instanzen der Klasse "Eq" für den Gleichheitstest und der Klasse "Show" für die String-Ausgabe an der Kommandozeile erzeugt.

Code-Beispiel in Haskell [Quelldatei]

>> data LVect3D = LVect3f {u,v,w::Float}
>> newvect u v w = LVect3f u v w

>> instance Show LVect3f
>>     where show n = show(u n)++" "++show(v n)++" "++show(w n)

>> instance Eq LVect3f
>>     where (==) a b = ((u a)==(u b)&&(v a)==(v b)&&(w a)==(w b))

>> (newvect (1+2) 2 4)==(newvect 2 (2/0) 4) => False
>> (newvect 1 2 4)==(newvect 1 (2/0) 4) => _|_

Die beiden Gleichheitstest verdeutlichen die Strategie, wegen derer die nicht-strikte Auswertung auch als "Call-by-need" bezeichnet wird. Die aktuellen Parameter sind nach Erzeugung eines neuen Vektors mit der "newvect"-Methode zunächst Ausdrücke vom Type Float mit den Namen u,v und w. Der Vergleich der Vektoren gegeneinander basiert auf dem arithmetischen Gleichheitstest der einzelnen Koordinaten, also der Ausdrücke u,v,w. Um diesen Test durchzuführen, müssen die Teilausdrücke ausgewertet werden. Das Ergebnis des Tests ist durch die Und-Verknüpfung der Einzelvergleiche definiert, deren Teilausdrücke zu True ausgewertet werden müssen, um als Ergebnis True zu erhalten.

Beim ersten Aufruf wird diese Bedingung bereits bei Auswertung des ganz linken Ausdrucks verletzt. Zunächst wird die Addition ausgewertet, ihr Ergebnis mit dem Wert 2 verglichen. Das Ergebnis ist False, womit der gesamte Test fehlgeschlagen ist. Die restlichen Ausdrücke werden nicht berechnet.

Im zweiten Beispiel liefert der erste Gleichheitstest 1==1 => True. Folglich ist der zweite Teilausdruck relevant für den Gesamtausdruck. Hier führt jedoch die Auswertung des arithmetischen Ausdrucks 2/0 zu einem Laufzeitfehler, da das Ergebnis nicht definiert ist (_|_).

2.4 Unendliche Strukturen

Mit Hilfe nicht-strikter Auswertung kann eine interessante Speicherstruktur realisiert werden, eine unendliche Liste. Eine solche Liste kann in "Haskell" rekursiv erzeugt werden. Da funktionale Programme nicht linear abgearbeitet werden, und mit Hilfe der "Lazy Evaluation", kann es dabei nie zu einer nicht terminierenden Rekursion kommen. Folgende Funktion erzeugt eine unendliche Liste aus einem Integer-Anfangsausdruck, in dem sie zu dem jeweils eingefügten aktuellen Ausdruck den Folgewert hinzufügt.

Code-Beispiel in Haskell [Quelldatei]

>> -- unendliche liste

>> intlist :: Integer -> [Integer]
>> intlist i = i : intlist (i+1)

Aus der Erfahrung mit imperativen Programmiersprachen würde man vermuten, dass diese Funktion nie terminiert. Dass dem nicht so ist, zeigen die beispielhaften Aufrufe:

>> zehnints = take 10 (intlist 0)
>> von13bis21 = take 9 (intlist 13)

(Pseudocode)
>> zehnints => [0,1,2,3,4,5,6,7,8,9]
>> von13bis21 => [13,14,15,16,17,18,19,20,21]

Durch "Lazy Evaluation" werden nie alle Elemente der Liste berechnet. Ausgewertet werden nur die (rekursiven) Aufrufe von "intlist", welche zur Berechnung des geforderten Listenausschnitts benötigt werden (die Funktion "take x" liefert die ersten x Elemente einer Liste zurück).


[Inhalt]...[Einleitung]...[Strikte Auswertung]