Programmieren in Haskell Stefan Janssen Programmieren in Haskell Reduktionsstrategien und Lazy Evaluation Reduktion RechenStrategien Lazy Evaluation Stefan Janssen Universität Bielefeld AG Praktische Informatik November 26, 2014 Wie rechnet Haskell? Programmieren in Haskell Stefan Janssen Heute wird geklärt: Was ist ein “Rechenschritt” in der funktionalen Programmierung? In welcher Reihenfolge werden Rechenschritte in Haskell ausgeführt ... ... und warum gerade in dieser? Die dabei gegebenen Erklärungen gelten nicht nur für Haskell, sondern für die funktionale Programmierung im Allgemeinen. Reduktion RechenStrategien Lazy Evaluation Definition von Funktionen durch Gleichungen Programmieren in Haskell Stefan Janssen 1 2 3 Die Funktion length hatten wir wie folgt definiert: length :: [ a ] -> Int length [] = 0 length ( x : xs ) = 1 + length xs Reduktion Definition von Funktionen durch Gleichungen RechenStrategien Lazy Evaluation Definition von Funktionen durch Gleichungen Programmieren in Haskell Stefan Janssen 1 2 3 Die Funktion length hatten wir wie folgt definiert: length :: [ a ] -> Int length [] = 0 length ( x : xs ) = 1 + length xs Aber was ist von folgender Definition zu halten? 4 length ( xs ++ ys ) == length xs + length ys Reduktion Definition von Funktionen durch Gleichungen RechenStrategien Lazy Evaluation Definition von Funktionen durch Gleichungen Programmieren in Haskell Stefan Janssen 1 2 3 Die Funktion length hatten wir wie folgt definiert: length :: [ a ] -> Int length [] = 0 length ( x : xs ) = 1 + length xs Aber was ist von folgender Definition zu halten? 4 length ( xs ++ ys ) == length xs + length ys reicht das, um z.B. length "abraham" auszurechnen? Reduktion Definition von Funktionen durch Gleichungen RechenStrategien Lazy Evaluation Grundbegriffe zu Auswertungs-Strategien: Was heißt Anwenden von Gleichungen in Formeln Programmieren in Haskell Stefan Redex = Stelle in einer Formel, an der die linke Seite Janssen einer Gleichung “passt” (reducible expression) Reduktion “passt” = die auf der linken Seite verlangten Konstruk- Strategien Lazy toren der Argumente liegen vor Evaluation die Substitution der Parameter wird dadurch definiert = “anwenden” = Ersetzen des Redex durch die rechte Seite unter Substitution der Parameter Anwenden einer Gleichung heisst auch “Reduktion”. Rechen- Grundbegriffe zu Auswertungs-Strategien: Was heißt Anwenden von Gleichungen in Formeln Programmieren in Haskell Stefan Redex = Stelle in einer Formel, an der die linke Seite Janssen einer Gleichung “passt” (reducible expression) Reduktion “passt” = die auf der linken Seite verlangten Konstruk- Strategien Lazy toren der Argumente liegen vor Evaluation die Substitution der Parameter wird dadurch definiert = “anwenden” = Ersetzen des Redex durch die rechte Seite unter Substitution der Parameter Anwenden einer Gleichung heisst auch “Reduktion”. Und was passiert, wenn nirgendwo ein Redex ist? Rechen- Normalform Programmieren in Haskell Irgendwann ist eine Formel auch mal ausgerechnet ... Normalform Ein Ausdruck ist in Normalform, wenn er keinen Redex enthält. Die erreicht Normalform ist das Ergebnis, wenn sie nur noch aus Konstruktoren (einschließlich Zahlen) besteht ein Fehler, wenn für keine der definierten Funktionen eine passende Gleichung vorliegt Rechnen Rechnen = Reduktion auf Normalform Stefan Janssen Reduktion RechenStrategien Lazy Evaluation Terminierung Programmieren in Haskell Stefan Janssen ... aber nicht bei jeder Rechnung wird eine Normalform erreicht! Reduktion Terminierung RechenStrategien Wenn eine Normalform erreicht wird, terminiert die Rechnung. Wenn die Redexe nie ganz verschwinden, terminiert die Rechnung nicht. Sie divergiert. Mit einer Rechenstrategie hat man u.U. Einfluss auf die Terminierung Lazy Evaluation Beispiele 1 head ( x : xs ) = x Programmieren in Haskell Stefan Janssen Reduktion RechenStrategien Lazy Evaluation Beispiele 1 head ( x : xs ) = x Programmieren in Haskell Stefan Janssen 2 3 4 5 6 -- ist anwendbar auf die Formel und ergibt head (1:2:2:[]) => 1 head (1:([3 ,4]++[5 ,6])) => 1 head (1: ones ) where ones = 1: ones => 1 Reduktion RechenStrategien Lazy Evaluation Beispiele 1 head ( x : xs ) = x Programmieren in Haskell Stefan Janssen 2 3 4 5 6 -- ist anwendbar auf die Formel und ergibt head (1:2:2:[]) => 1 head (1:([3 ,4]++[5 ,6])) => 1 head (1: ones ) where ones = 1: ones => 1 ist (noch) nicht anwendbar auf Reduktion RechenStrategien Lazy Evaluation Beispiele 1 head ( x : xs ) = x Programmieren in Haskell Stefan Janssen 2 3 4 5 6 -- ist anwendbar auf die Formel und ergibt head (1:2:2:[]) => 1 head (1:([3 ,4]++[5 ,6])) => 1 head (1: ones ) where ones = 1: ones => 1 ist (noch) nicht anwendbar auf 7 8 9 10 head ([1 ,3 ,4] ++ [5 ,6]) head ( map (1+) [1 ,2 ,3]) head ones where ones = 1: ones Reduktion RechenStrategien Lazy Evaluation Beispiele 1 head ( x : xs ) = x Programmieren in Haskell Stefan Janssen 2 3 4 5 6 -- ist anwendbar auf die Formel und ergibt head (1:2:2:[]) => 1 head (1:([3 ,4]++[5 ,6])) => 1 head (1: ones ) where ones = 1: ones => 1 ist (noch) nicht anwendbar auf 7 8 9 10 head ([1 ,3 ,4] ++ [5 ,6]) head ( map (1+) [1 ,2 ,3]) head ones where ones = 1: ones Es muss erst eine Gleichung für ++, map oder ones angewandt werden. Reduktion RechenStrategien Lazy Evaluation Ein Schritt weiter ... Programmieren in Haskell 7 8 9 10 7 8 9 10 Ausgangspunkt: head ([1 ,3 ,4] ++ [5 ,6]) head ( map (1+) [1 ,2 ,3]) head ones where ones = 1: ones Es wird eine Gleichung für ++, map bzw. ones angewandt; nach diesem Schritt ist head ein Redex: head (1:([3 ,4] ++ [5 ,6])) => 1 head ((1+1) : map (1+) [2 ,3]) = > (1+1) head 1: ones where => 1 ones = 1: ones Stefan Janssen Reduktion RechenStrategien Lazy Evaluation Lage von Redexen in einer Formel Programmieren in Haskell Stefan Janssen Reduktion Ein Redex ist innermost, wenn er keinen weiteren Redex enthält, outermost, wenn er in keinem weiteren Redex enthalten ist, “zwischendrin”, sonst. Man sagt auch “innerst” und “äußerst”, aber äußerst ungern. Es kann ja immer mehrere solche Redexe geben. RechenStrategien Lazy Evaluation Beispiel Programmieren in Haskell Stefan Janssen Reduktion RechenStrategien Lazy Evaluation Auswertungs-Strategien Programmieren in Haskell Eine Auswertungs-Strategie legt fest, wie jeweils der nächste Redex zu wählen ist. leftmost innermost: immer den Redex wählen, der innermost ist und am weitesten links steht. leftmost outermost: immer den Redex wählen, der outermost ist und am weitesten links steht. gemischt: man kann sich viele weitere Strategien vorstellen . . . Haskell rechnet leftmost-outermost + X, Lisp, Scheme, Python rechnen leftmost-innermost Stefan Janssen Reduktion RechenStrategien Lazy Evaluation Menschliches versus maschinelles Rechnen Mensch und Computer halten sich an die Rechengesetze – hier: Gleichungen Computer folgt einer Strategie, die für die Programmiersprache vorgegeben ist Der Mensch rechnet tendenziell innermost, insbesondere der Ungeübte. Der Erfahrene nutzt Abweichungen von innermost: Zusammenfassen von mehreren Schritten in einem (Fehlerquelle!) Verwendung von mathematischen Gesetzen, die keine definierenden Gleichungen sind, zur Vereinfachung; z.B. max (f (x ), f (y )) = f (max (x , y )) wenn f monoton . Er erkennt die Nichtterminierung und kann anhalten oder an anderer Stelle weiterrechnen Das kommt daher, dass der Verstand beim Rechnen weiß, dass er rechnet. Programmieren in Haskell Stefan Janssen Reduktion RechenStrategien Lazy Evaluation if-then-else Besonderheit von if_then_else: Programmieren in Haskell Stefan Janssen Seine definierenden Gleichungen sind Reduktion RechenStrategien if True then x else y =x (1) if False then x else y =y (2) Ein Redex wird in jedem Fall leftmost outermost berechnet. Je nach Ergebnis für C wird entweder A oder B berechnet. Lazy Evaluation if-then-else (2) Programmieren in Haskell Stefan Janssen Diese Regel für if_then_else gilt in allen Programmiersprachen, auch in den imperativen. Ohne sie könnte man keine terminierende Rekursion programmieren: f ( x ) = if x <= 0 then 42 else 2 + f (x -1) Innermost-Strategie führt zu endloser Berechnung des else-Falles. Reduktion RechenStrategien Lazy Evaluation Eigenschaften der Strategien Programmieren in Haskell Strategie = Regel zur Wahl des nächsten Redex. 1 2 Ob eine Rechnung terminiert, hängt i.A. von der Strategie ab. Wenn zwei verschiedene Strategien, angewandt auf die gleiche Formel, terminieren, liefern sie das gleiche Ergebnis. 3 Im Fall (2) kann sich der Rechenaufwand stark unterscheiden. 4 Wenn für eine Formel F irgendeine Strategie terminiert, dann terminiert für F auch “leftmost outermost”. 5 “leftmost innermost” terminiert seltener als “leftmost outermost”. Stefan Janssen Reduktion RechenStrategien Lazy Evaluation Anschauliche Bedeutung Programmieren in Haskell Stefan Janssen Reduktion innermost: Alle Argumente einer Funktion werden ganz ausgerechnet, bevor die Funktion “aufgerufen” wird. outermost: Die Argumente einer Funktion werden immer nur so weit ausgerechnet, wie es die Funktion für ihren nächsten Schritt braucht. RechenStrategien Lazy Evaluation Strikte Sprachen Programmieren in Haskell Die meisten Programiersprachen verwenden für alle Funktionen f , abgesehen vom if_then_else_ die Strategie leftmost innermost. Man nennt sie strikt wegen der folgenden Eigenschaft: f (.., ⊥, ...) = ⊥ Dabei steht ⊥ für einen Ausdruck, dessen Berechnung nicht terminiert. In strikten Sprachen kann man keine unendlichen Datenstrukturen verwenden – sie würden bei der ersten Benutzung komplett ausgerechnet. Stefan Janssen Reduktion RechenStrategien Lazy Evaluation Strikte Sprachen Programmieren in Haskell Die meisten Programiersprachen verwenden für alle Funktionen f , abgesehen vom if_then_else_ die Strategie leftmost innermost. Man nennt sie strikt wegen der folgenden Eigenschaft: f (.., ⊥, ...) = ⊥ Dabei steht ⊥ für einen Ausdruck, dessen Berechnung nicht terminiert. In strikten Sprachen kann man keine unendlichen Datenstrukturen verwenden – sie würden bei der ersten Benutzung komplett ausgerechnet. Man kann aber outermost dank if_then_else_ simulieren (Python, .NET) Stefan Janssen Reduktion RechenStrategien Lazy Evaluation Lazy Evaluation Programmieren in Haskell Stefan Janssen Reduktion Haskell verwendet lazy evaluation (verzögerte Auswertung), das ist leftmost outermost + graph reduction graph reduction ist eine Zusatzregel, die die Duplikation von noch nicht ausgerechneten Formeln vermeidet. RechenStrategien Lazy Evaluation Beispiel zur graph reduction 1 Reine outermost-Strategie hat den Nachteil, dass sie den Rechenaufwand erhöht, wenn Variablen mehrfach benutzt werden: square x = x * x Programmieren in Haskell Stefan Janssen Reduktion RechenStrategien Lazy Evaluation square ↑ outermost (3 + 4) ↑ innermost würde blindlings outermost reduziert zu (3 + 4) ∗ (3 + 4) wo nun der Ausdruck (3 + 4) zweimal berechnet werden müsste. Beispiel zur graph reduction Programmieren in Haskell 1 2 Stattdessen wird reduziert square (3+4) = = > x * x where x = 3+4 worin durch die “Nebenrechnung” 3 + 4 nur einmal berechnet wird. Fazit Lazy evaluation braucht weniger Reduktionen als innermost-Strategien und terminiert von allen Strategien am häufigsten. Allerdings: Lazy evaluation ist aufwendiger zu compilieren als innermost Strategien. Stefan Janssen Reduktion RechenStrategien Lazy Evaluation Einfluss von Lazy Evaluation auf den Aufwand Programmieren in Haskell Stefan Janssen Reduktion RechenStrategien Unter lazy evaluation kann es sein, dass eine Funktion in einem bestimmten Kontext nur einen kleinen Teil der scheinbar benötigten Lösung ausrechnet. Lazy Evaluation Einfluss von Lazy Evaluation auf den Aufwand Programmieren in Haskell Stefan Janssen Reduktion Hier die naheliegende Implementierung von minimum: 1 2 3 4 minimum ( a : as ) = min a as where min a [] = a min a ( b : xs ) = if a <= b then min a xs else min b xs RechenStrategien Lazy Evaluation Programmieren in Haskell Stefan Janssen Hier eine alternative Implementierung, die bequemerweise den Insertion-Sort benutzt minimum ( a : as ) = head ( isort ( a : as )) Spontaner Einwand: Zu aufwändig!! Man muss doch nicht die ganze Liste sortieren, um das Minimum zu finden! Sehen wir genauer hin! Reduktion RechenStrategien Lazy Evaluation Einfluss von Lazy Evaluation auf den Aufwand Programmieren in Haskell Stefan Janssen Reduktion 1 2 3 4 5 RechenDie Definition von isort war: Strategien isort [] = [] Lazy Evaluation isort ( a : as ) = insert a ( isort as ) insert a [] = [ a ] insert a ( b : as ) = if a <= b then a : b : as else b :( insert a as ) Einfluss von Lazy Evaluation auf den Aufwand Lazy Evaluation berechnet in einer Formel immer an der “äußersten” Stelle, an der eine Gleichung anwendbar ist: Programmieren in Haskell Stefan Janssen Zunächst kann immer nur die Gleichung isort.2 angewandt werden, am Ende einmal isort.1 Reduktion RechenStrategien Lazy Evaluation head head isort insert : => a1 insert : a1 a2 a2 insert : an an [] [] Einfluss von Lazy Evaluation auf den Aufwand Programmieren in Haskell Danach wird n-mal insert angewandt, beginnend bei an . Nehmen wir an, a5 ist das kleinste Element. Streng “outermost” entsteht nun => Stefan Janssen Reduktion head RechenStrategien : Lazy Evaluation a5 insert a1 innerhalb des Kastens könnte man weiterrechnen, aber das wäre nicht “outermost”. insert a2 insert a n−1 [a n] Einfluss von Lazy Evaluation auf den Aufwand head braucht nicht mehr als den obersten Konstruktur (:), um sein Ergebnis zu liefern Programmieren in Haskell Stefan Janssen Reduktion head => a5 : a5 Es werden ≈ 2 · n Gleichungen angewandt. Der Rest der Operationen für das komplette Sortieren steckt im Kasten und wird hier nicht benötigt! RechenStrategien Lazy Evaluation Fazit Programmieren in Haskell Stefan Janssen Durch Lazy Evaluation müssen erheblich weniger Reduktionen durchgeführt werden als z.B. durch die leftmost-innermost Strategie. Was “erheblich” ist, siehe A&D Kap. Effizienzanalyse. Reduktion RechenStrategien Lazy Evaluation Fazit Programmieren in Haskell Stefan Janssen Durch Lazy Evaluation müssen erheblich weniger Reduktionen durchgeführt werden als z.B. durch die leftmost-innermost Strategie. Was “erheblich” ist, siehe A&D Kap. Effizienzanalyse. Rechnen mit unendlichen Datenstrukturen ist nur dank Lazy Evaluation möglich. Reduktion RechenStrategien Lazy Evaluation