Universität Bielefeld Programmieren in Haskell Giegerich Programmieren in Haskell WS 2011/2012 Robert Giegerich Universität Bielefeld AG Praktische Informatik November 19, 2013 Reduktion RechenStrategien Wie rechnet Haskell? Universität Bielefeld Programmieren in Haskell Giegerich 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 Definition von Funktionen durch Gleichungen Universität Bielefeld Programmieren in Haskell Giegerich 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 Definition von Funktionen durch Gleichungen Universität Bielefeld Programmieren in Haskell Giegerich 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 Definition von Funktionen durch Gleichungen Universität Bielefeld Programmieren in Haskell Giegerich 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 Grundbegriffe zu Auswertungs-Strategien: Was heißt Anwenden von Gleichungen in Formeln Universität Bielefeld Programmieren in Haskell Giegerich Redex = Stelle in einer Formel, an der die linke Seite Reduktion einer Gleichung “passt” (reducible expression) “passt” = die auf der linken Seite verlangten Konstruktoren der Argumente liegen vor 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”. RechenStrategien Grundbegriffe zu Auswertungs-Strategien: Was heißt Anwenden von Gleichungen in Formeln Universität Bielefeld Programmieren in Haskell Giegerich Redex = Stelle in einer Formel, an der die linke Seite Reduktion einer Gleichung “passt” (reducible expression) “passt” = die auf der linken Seite verlangten Konstruktoren der Argumente liegen vor 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? RechenStrategien Normalform Universität Bielefeld Programmieren in Haskell Irgendwann ist eine Formel auch mal ausgerechnet ... Giegerich Normalform Reduktion Ein Ausdruck ist in Normalform, wenn er keinen Redex enthält. RechenStrategien 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 Terminierung Universität Bielefeld Programmieren in Haskell ... aber nicht bei jeder Rechnung wird eine Normalform erreicht! Terminierung Wenn eine Normalform erreicht wird, terminiert die Rechnung. Wenn die Redexe nie ganz verschwinden, terminiert die Rechnungnicht. Sie divergiert. Mit einer Rechenstrategie hat man u.U. Einfluss auf die Terminierung Giegerich Reduktion RechenStrategien Beispiele 1 head ( x : xs ) = x Universität Bielefeld Programmieren in Haskell Giegerich Reduktion RechenStrategien Beispiele 1 head ( x : xs ) = x Universität Bielefeld Programmieren in Haskell Giegerich 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 Beispiele 1 head ( x : xs ) = x Universität Bielefeld Programmieren in Haskell Giegerich 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 Beispiele 1 head ( x : xs ) = x Universität Bielefeld Programmieren in Haskell Giegerich 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 Beispiele 1 head ( x : xs ) = x Universität Bielefeld Programmieren in Haskell Giegerich 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 Ein Schritt weiter ... Universität Bielefeld Programmieren in Haskell Giegerich 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 Reduktion RechenStrategien Lage von Redexen in einer Formel Universität Bielefeld Programmieren in Haskell Giegerich 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 Beispiel Universität Bielefeld Programmieren in Haskell Giegerich Reduktion RechenStrategien Auswertungs-Strategien Universität Bielefeld Programmieren in Haskell Giegerich 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 Reduktion RechenStrategien 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. Universität Bielefeld Programmieren in Haskell Giegerich Reduktion RechenStrategien if-then-else Universität Bielefeld Besonderheit von if_then_else: Programmieren in Haskell Giegerich 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. if-then-else (2) Universität Bielefeld Programmieren in Haskell Giegerich Reduktion 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. RechenStrategien Eigenschaften der Strategien Universität Bielefeld Programmieren in Haskell Strategie = Regel zur Wahl des nächsten Redex. 1 Ob eine Rechnung terminiert, hängt i.A. von der Strategie ab. 2 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”. Giegerich Reduktion RechenStrategien Anschauliche Bedeutung Universität Bielefeld Programmieren in Haskell Giegerich 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 Strikte Sprachen Universität Bielefeld 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. Giegerich Reduktion RechenStrategien Strikte Sprachen Universität Bielefeld 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) Giegerich Reduktion RechenStrategien Lazy Evaluation Universität Bielefeld Programmieren in Haskell Giegerich 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 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 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. Universität Bielefeld Programmieren in Haskell Giegerich Reduktion RechenStrategien Beispiel zur graph reduction Universität Bielefeld 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. Giegerich Reduktion RechenStrategien Einfluss von Lazy Evaluation auf den Aufwand Universität Bielefeld Programmieren in Haskell Giegerich 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. Einfluss von Lazy Evaluation auf den Aufwand Universität Bielefeld Programmieren in Haskell Giegerich Reduktion Hier die naheliegende Implementierung von minimum: 1 2 3 4 minimum ( a : as ) = min a as where min a [] = a min a ( b : x ) = if a <= b then min a x else min b x RechenStrategien Universität Bielefeld Programmieren in Haskell Giegerich Reduktion 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! RechenStrategien Einfluss von Lazy Evaluation auf den Aufwand Universität Bielefeld Programmieren in Haskell Giegerich Reduktion Rechen- 1 2 3 4 5 Die Definition von isort war: Strategien isort [] = [] 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 Universität Bielefeld Lazy Evaluation berechnet in einer Formel immer an der “äußersten” Stelle, an der eine Gleichung anwendbar ist: Programmieren in Haskell Giegerich Zunächst kann immer nur die Gleichung isort.2 angewandt werden, am Ende einmal isort.1 head head isort insert : => a1 insert : a1 a2 a2 insert : an an [] [] Reduktion RechenStrategien Einfluss von Lazy Evaluation auf den Aufwand Universität Bielefeld Programmieren in Haskell Danach wird n-mal insert angewandt, beginnend bei an . Nehmen wir an, a5 ist das kleinste Element. Streng “outermost” entsteht nun head : => a5 insert a1 innerhalb des Kastens könnte man weiterrechnen, aber das wäre nicht “outermost”. insert a2 insert a n−1 [a n] Giegerich Reduktion RechenStrategien Einfluss von Lazy Evaluation auf den Aufwand head braucht nicht mehr als den obersten Konstruktur (:), um sein Ergebnis zu liefern Universität Bielefeld Programmieren in Haskell Giegerich 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 Fazit Universität Bielefeld Programmieren in Haskell Giegerich 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 Fazit Universität Bielefeld Programmieren in Haskell Giegerich 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 Fazit Universität Bielefeld Programmieren in Haskell Giegerich 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. Denkt nochmal über die Definition von primes nach! Reduktion RechenStrategien