3. Funktionales Programmieren 3.0 Kapitel 3 Funktionales Programmieren ©Arnd Poetzsch-Heffter TU Kaiserslautern 157 3. Funktionales Programmieren 3.0 Übersicht 3. Funktionales Programmieren Grundkonzepte funktionaler Programmierung Zentrale Begriffe und Einführung Rekursive Funktionen Listen und Tupel Benutzerdefinierte Datentypen Ein- und Ausgabe Module Zusammenfassung von 3.1 Algorithmen auf Listen und Bäumen Sortieren Suchen Polymorphie und Funktionen höherer Ordnung Typisierung Funktionen höherer Ordnung Semantik, Testen und Verifikation ©Arnd Poetzsch-Heffter TU Kaiserslautern 158 3. Funktionales Programmieren 3.0 Übersicht (2) Zur Semantik funktionaler Programme Testen und Verifikation ©Arnd Poetzsch-Heffter TU Kaiserslautern 159 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Abschnitt 3.1 Grundkonzepte funktionaler Programmierung ©Arnd Poetzsch-Heffter TU Kaiserslautern 160 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.1 Zentrale Begriffe und Einführung ©Arnd Poetzsch-Heffter TU Kaiserslautern 161 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Zentrale Begriffe und Einführung Funktionale Programmierung im Überblick: • Funktionales Programm: I partielle Funktionen von Eingabe- auf Ausgabedaten I besteht aus Deklarationen von (Daten-)Typen, Funktionen und (Daten-)Strukturen I Rekursion ist eines der zentralen Sprachkonzepte I in Reinform: kein Zustandskonzept, keine veränderlichen Variablen, keine Schleifen, keine Zeiger • Ausführung eines funktionalen Programms: Anwendung einer Funktion auf Eingabedaten • Zusätzliche Programmierkonstrukte, um die Kommunikation mit der Umgebung zu beschreiben ©Arnd Poetzsch-Heffter TU Kaiserslautern 162 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Definition: (partielle Funktion) Ein Funktion heißt partiell, wenn sie nur auf einer Untermenge ihres Argumentbereichs definiert ist. Andernfalls heißt sie total. ©Arnd Poetzsch-Heffter TU Kaiserslautern 163 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (partielle Funktion) 1. Bezeichne Nat die Menge der natürlichen Zahlen (0 und größer) und sei fact :: Nat → Nat wie folgt definiert: ( fact(n) = 1 , für n = 0 fact(n − 1) ∗ n , für n > 0 Dann ist fact wohldefiniert und total. ©Arnd Poetzsch-Heffter TU Kaiserslautern 164 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (partielle Funktion) (2) 2. Bezeichne Float die Menge der auf dem Rechner darstellbaren Gleitkommazahlen. Dann ist die Funktion sqrt :: Float → Float , die die Quadratwurzel (engl. square root) berechnet, partiell. 3. Bezeichne String die Menge der Zeichenreihen. Dann ist die Funktion abschneide2, die die ersten beiden Zeichen einer Zeichenreihe abschneidet partiell (warum?) ©Arnd Poetzsch-Heffter TU Kaiserslautern 165 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Definition: (Funktionsanwendung, -auswertung, Terminierung, Nichtterminierung) Bezeichne f eine Funktion, a ein zulässiges Argument von f . Die Anwendung von f auf a nennen wir eine Funktionsanwendung (engl. function application); meist schreibt man dafür f (a) oder f a. Den Prozess der Berechnung des Funktionswerts nennen wir Auswertung (engl. evaluation). Die Auswertung kann: • nach endlich vielen Schritten terminieren und ein Ergebnis liefern (normale Terminierung, engl. normal termination), • nach endlich vielen Schritten terminieren und einen Fehler melden (abrupte Terminierung, engl. abrupt termination), • nicht terminieren, d.h. der Prozess der Auswertung kommt (von alleine) nicht zu Ende. ©Arnd Poetzsch-Heffter TU Kaiserslautern 166 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Entsprechendes gilt in anderen Programmierparadigmen. • Da Terminierung nicht entscheidbar ist, benutzt man in der Informatik häufig partielle Funktionen. Beispiel: (Zur Entscheidbarkeit der Terminierung) McCarthy’s Funktion: Sei m :: Nat → Nat wie folgt definiert: ( n − 10 , für n > 100 m(n) = m(m(n + 11)) , für n ≤ 100 Ist m für alle Argumente wohldefiniert? • In der Theorie kann man durch Einführen eines Elements „jede“partielle Funktion total machen. Üblicherweise bezeichnet man das Element für „undefiniert “ mit ⊥ (engl. „bottom “). ©Arnd Poetzsch-Heffter TU Kaiserslautern 167 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Wert, Value) Werte (engl. values) in der (reinen) funktionalen Programmierung sind • Elementare Daten (Zahlen, Wahrheitswerte, Zeichen, . . . ), • zusammengesetzte Daten (Listen von Werten, Wertepaare, . . . ), • (partielle) Funktionen mit Werten als Argumenten und Ergebnissen. Also sind auch Listen von Funktionen Werte. ©Arnd Poetzsch-Heffter TU Kaiserslautern 168 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • In anderen Sprachparadigmen gibt es auch Werte, allerdings werden Funktionen nicht immer als Werte betrachtet (z.B. in der objektorientierten Programmierung: „immutable objects“). • Im Mittelpunkt der funktionalen Programmierung steht die Definition von Wertemengen (Datentypen) und Funktionen. Funktionale Programmiersprachen stellen dafür Sprachmittel zur Verfügung. • Wie für abstrakte Objekte oder Begriffe typisch, besitzen Werte I I I I keinen Ort, keine Lebensdauer, keinen veränderbaren Zustand, kein Verhalten. ©Arnd Poetzsch-Heffter TU Kaiserslautern 169 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Typ, engl. type) Ein Typ (engl. type) fasst Werte zusammen, auf denen die gleichen Funktionsanwendungen zulässig sind. Typisierte Sprachen besitzen ein Typsystem, das für jeden Wert festlegt, welchen Typ er hat. In funktionalen Programmiersprachen gibt es drei Arten von Werten bzw. Typen, mit denen man rechnen kann: • Basisdatentypen ( Int, Bool, String, . . . ) • benutzerdef., insbesondere rekursive Datentypen • Funktionstypen, z.B. Int → Bool oder ( Int → Int ) → ( Int → Int ) ©Arnd Poetzsch-Heffter TU Kaiserslautern 170 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Datenstrukturen Eine Struktur fasst Typen und Werte zusammen, insbesondere also auch Funktionen. Datenstrukturen sind Strukturen, die mindestens einen „neuen“Datentyp und alle seine wesentlichen Funktionen bereitstellen. Eine Datenstruktur besteht aus einer oder mehrerer disjunkter Wertemengen zusammen mit den darauf definierten Funktionen. In der Mathematik nennt man solche Gebilde Algebren oder einfach nur Strukturen. In der Informatik spricht man auch von einer Rechenstruktur. ©Arnd Poetzsch-Heffter TU Kaiserslautern 171 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Definition: (Signatur einer Datenstruktur) Die Signatur (T, F) einer Datenstruktur besteht aus • einer endlichen Menge T von Typbezeichnern und • einer endlichen Menge F von Funktionsbezeichnern, wobei für jedes f ∈ F ein Funktionstyp f :: T1 → · · · → Tn → T0 , Ti ∈ T, 0 ≤ i ≤ n, definiert ist. n gibt die Stelligkeit von f an. ©Arnd Poetzsch-Heffter TU Kaiserslautern 172 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Definition: (Datenstruktur mit Signatur) Eine (partielle) Datenstruktur mit Signatur (T, F) ordnet • jedem Typbezeichner T ∈ T eine Wertemenge, • jedem Funktionsbezeichner f ∈ F eine partielle Funktion zu, so dass Argument- und Wertebereich von f den Wertemengen entsprechen, die zu f 0 s Funktionstyp gehören. ©Arnd Poetzsch-Heffter TU Kaiserslautern 173 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Wir betrachten zunächst die Basisdatenstrukturen, wie man sie in jeder Programmier-, Spezifikations- und Modellierungssprache findet. • Die Basisdatenstrukturen (engl. basic / primitive data structures) bilden die Grundlage zur Definition weiterer Typen, Funktionen und Datenstrukturen. • Als Beispiel dienen uns die Basisdatenstrukturen der funktionalen Sprache Haskell. Später lernen wir auch die Basisdatenstrukturen von Java kennen. • Wir benutzen auch Operatorsymbole wie + und * um Funktionen zu bezeichnen. • Nullstellige Funktionen nennen wir Konstanten. ©Arnd Poetzsch-Heffter TU Kaiserslautern 174 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der booleschen Werte: Typ: Bool Funktionen: (==) (/=) (&&) (||) not :: :: :: :: :: Bool → Bool → Bool Bool → Bool → Bool Bool → Bool → Bool Bool → Bool → Bool Bool → Bool Konstanten: True :: Bool False :: Bool ©Arnd Poetzsch-Heffter TU Kaiserslautern 175 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der booleschen Werte: (2) Dem Typbezeichner Bool ist die Wertemenge {True, False} zugeordnet. (==) (/=) (&&) (||) not bezeichnet die Gleichheit auf Wahrheitswerten bezeichnet die Ungleichheit auf Wahrheitsw. bezeichnet das logische Und bezeichnet das logische Oder bezeichnet die logische Negation True False bezeichnet den Wert True bezeichnet den Wert False ©Arnd Poetzsch-Heffter TU Kaiserslautern 176 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Operatorsymbole werden meist mit Infix-Notation verwendet: 34 + 777 , True || False , True && False == True • Ist ein Operatorsymbol, kann man () in Haskell wie einen Funktionsbezeichner verwenden: (+) 34 777 , (||) True False , (==) ((&&) True False) True • Ist f ein (mindestens) zweistelliger Funktionsbezeichner, kann man `f ` in Haskell mit Infix-Notation verwenden: 34 `div` 777 • Im Folgenden unterscheiden wir nur noch dann zwischen Funktionsbezeichner und bezeichneter Funktion, wenn dies aus Gründen der Klarheit nötig ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 177 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der ganzen Zahlen: Die Datenstruktur der ganzen Zahlen erweitert die Datenstruktur der booleschen Werte, d.h. sie umfasst den Typ Bool und die darauf definierten Funktionen. Zusätzlich enthält sie u. a.: Typ: Integer Funktionen: (==), (/=) (<), (<=), (>), (>=) (+), (*), (-) div, mod negate, signum, abs ©Arnd Poetzsch-Heffter :: :: :: :: :: Integer → Integer → Bool Integer → Integer → Bool Integer → Integer → Integer Integer → Integer → Integer Integer → Integer TU Kaiserslautern 178 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der ganzen Zahlen: (2) Konstanten: – in Dezimaldarstellung: 0, 127, -23 – in Hexadezimaldarstellung: 0x0, 0x7F, −0x17 – in Oktaldarstellung: 0o0, 0o177, −0o27 Dem Typbezeichner Integer ist die Menge der ganzen Zahlen als Wertemenge zugeordnet. Die Funktionen der Datenstruktur bezeichnen die üblichen Funktionen auf den ganzen Zahlen; div bezeichent die ganzzahlige Division, mod liefert den Rest der ganzzahligen Division. ©Arnd Poetzsch-Heffter TU Kaiserslautern 179 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der beschränkten ganzen Zahlen: Die Datenstruktur der beschränkten ganzen Zahlen erweitert die Datenstruktur der booleschen Werte. Zusätzlich enthält sie u. a.: Typ: Int Funktionen: (==), (/=) (<), (<=), (>), (>=) (+), (*), (-) div, mod negate, signum, abs :: :: :: :: :: Int → Int → Bool Int → Int → Bool Int → Int → Int Int → Int → Int Int → Int Konstanten: minBound, maxBound :: Int – in Dezimaldarstellung: 0, 127, -23 – in Hexadezimaldarstellung: 0x0, 0x7F, -0x17 – in Oktaldarstellung: 0o0, 0o177, -0o27 ©Arnd Poetzsch-Heffter TU Kaiserslautern 180 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der beschränkten ganzen Zahlen: (2) Dem Typbezeichner Int ist eine rechnerabhängige Wertemenge zugeordnet, die mindestens die ganzen Zahlen von −229 bis 229 − 1 enthalten muss. Innerhalb der Wertemenge sind die Funktionen der Datenstruktur der beschränkten ganzen Zahlen verlaufsgleich mit den Funktionen auf den ganzen Zahlen. Außerhalb der Wertemenge ist ihr Verhalten nicht definiert. Insbesondere können (+), (∗), abs, negate partiell sein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 181 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: Wenn unterschiedliche Funktionen oder andere Programmelemente den gleichen Bezeichner haben, spricht man vom Überladen des Bezeichners (engl. Overloading). Beispiel: (Überladung von Bezeichnern) Wie in den obigen Datenstrukturen gezeigt, können Funktionsbezeichner und Operatorbezeichner in Haskell überladen werden, d.h. in Abhängigkeit vom Typ ihrer Argumente bezeichnen sie unterschiedliche Funktionen. Beispiele: negate, (==), (+) ©Arnd Poetzsch-Heffter TU Kaiserslautern 182 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Gleitkommazahlen: Die Datenstruktur der Gleitkommazahlen erweitert die Datenstruktur der ganzen Zahlen und bietet u. a.: Typ: Float Funktionen: (==), (/=) (<), (<=), (>), (>=) (+), (*), (-), (/) negate, signum, abs fromInteger truncate, round ceiling, floor exp, log, sqrt (**), logBase sin, cos, tan ©Arnd Poetzsch-Heffter :: :: :: :: :: :: :: :: :: :: Float → Float → Bool Float → Float → Bool Float → Float → Float Float → Float Integer → Float Float → Integer Float → Integer Float → Float Float → Float → Float Float → Float TU Kaiserslautern 183 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Gleitkommazahlen: (2) Konstanten: pi :: Float – mit Dezimalpunkt: 0.0, 1000.0, 128.9, -2.897 – mit Exponenten: 0e0, 1e3, 1289e-1, -2897e-3 Dem Typbezeichner Float ist in Haskell eine rechnerabhängige Wertemenge zugeordnet. Entsprechendes gilt für die präzise Bedeutung der Funktionen und Konstanten. ©Arnd Poetzsch-Heffter TU Kaiserslautern 184 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkung: • Die ganzen Zahlen sind in der Programmierung keine Teilmenge der reellen Zahlen! • In Haskell gibt es weitere Zahlentypen (number types): I Double (vordefiniert): doppelt präzise Gleitkommazahlen I Rational (definiert in Standardbibliothek): rationale Zahlen ©Arnd Poetzsch-Heffter TU Kaiserslautern 185 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Zeichen: Die Datenstruktur der Zeichen (engl. character) erweitert die Datenstruktur der beschränkten ganzen Zahlen. Zusätzlich enthält sie u. a.: Typ: Char Funktionen: (==), (/=) :: Char → Char → Bool (<), (<=), (>), (>=) :: Char → Char → Bool succ, pred :: Char → Char toEnum :: Int → Char fromEnum :: Char → Int ©Arnd Poetzsch-Heffter TU Kaiserslautern 186 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Zeichen: (2) Konstanten: – in Zeichendarstellung: 'A', 'a', '0', 'ß', '"' – spezielle Zeichen: '\'', '\n', '\t', '\b', '\\' – in numerischer Darstellung: '\65', '\x41', '\o101' minBound, maxBound :: Char Dem Typbezeichner Char ist die Menge der Unicode-Zeichen zugeordnet. Jedes Unicode-Zeichen besitzt eine Nummer im Bereich von 0 bis 1.114.111 . Die Vergleichsoperationen stützen sich auf die Nummerierung. Die Funktionen succ bzw. pred liefern das Nachfolger- bzw. Vorgängerzeichen entsprechend der Nummerierung. Die Funktionen fromEnum bzw. toEnum liefern die Nummer eines Zeichens bzw. das Zeichen zu einer Nummer. ©Arnd Poetzsch-Heffter TU Kaiserslautern 187 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Zeichenreihen: Zeichenreihen sind in Haskell als Listen von Zeichen realisiert und erweitern die Datenstruktur der Zeichen. Alle auf Listen verfügbaren Funktionen (siehe Datenstruktur der Listen) können für Zeichenreihen verwendet werden, insbesondere: Typ: String oder [Char] Funktionen: (==), (/=) :: String → String → Bool (<), (<=), (>), (>=) :: String → String → Bool head :: String → Char tail :: String → String length :: String → Int (++) :: String → String → String ©Arnd Poetzsch-Heffter TU Kaiserslautern 188 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Zeichenreihen: (2) Konstanten: – Zeichenreihendarstellung in doppelten Hochkommas: "Ich bin ein String!!" "Ich \098\105\110 ein String!!" "" (die leere Zeichenreihe) "Mein Lehrer sagt: \"Nehme die Dinge genau!\"" "String vor Zeilenumbruch \nNach Zeilenumbruch" – Zeichenreihendarstellung als Liste von Zeichen: [ 'H', 'a', 's', 'k', 'e', 'l', 'l'] Dem Typbezeichner String ist die Menge der Zeichenreihen/Listen über der Menge der Zeichen zugeordnet. ©Arnd Poetzsch-Heffter TU Kaiserslautern 189 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Zeichenreihen: (3) Den Vergleichsoperationen liegt die lexikographische Ordnung zugrunde, wobei die Ordnung auf den Zeichen auf deren Nummerierung basiert (siehe Datenstruktur Char). ©Arnd Poetzsch-Heffter TU Kaiserslautern 190 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkung: • Es wird unterschieden zwischen Zeichen und Zeichenreihen der Länge 1. • Jede Programmier-, Modellierungs- und Spezifikationssprache besitzt Basisdatenstrukturen. Die Details variieren aber teilweise deutlich. • Wenn Basisdatenstrukturen implementierungs- oder rechnerabhängig sind, entstehen Portabilitätsprobleme. • Der Trend bei den Basisdatenstrukturen geht zur Standardisierung. ©Arnd Poetzsch-Heffter TU Kaiserslautern 191 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Aufbau funktionaler Programme Im Kern, d.h. wenn man die Modularisierungskonstrukte nicht betrachtet, bestehen funktionale Programme aus: • der Beschreibung von Werten: I z.B. (7+23), 30 • Vereinbarung von Bezeichnern für Werte (einschließlich Funktionen): I x = 7; • der Definitionen von Typen: I type String = [Char] I data MyType = . . . ©Arnd Poetzsch-Heffter TU Kaiserslautern 192 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beschreibung von Werten: • mittels Konstanten oder Bezeichnern für Werte: 23 "Ich bin eine Zeichenreihe " True x • durch direkte Anwendung von Funktionen: abs ( -28382) "Urin" ++ " stinkt " not True ©Arnd Poetzsch-Heffter TU Kaiserslautern 193 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beschreibung von Werten: (2) • durch geschachtelte Anwendung von Funktionen: 45.67 + 6857 * ( -9) floor ( -3.4) * truncate ( -3.4) toEnum ((( fromEnum (last("Urin"++" stinkt ")))+2))::Char • durch Verwendung des bedingten Ausdrucks (engl. conditional expression): if <boolAusdruck > then <Ausdruck > else <Ausdruck > ©Arnd Poetzsch-Heffter TU Kaiserslautern 194 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Ausdruck, expression) Ausdrücke sind das Sprachmittel zur Beschreibung von Werten. Ein Ausdruck (engl. expression) in Haskell ist • eine Konstante, • ein Bezeichner (Variable, Name), • die Anwendung einer Funktion auf einen Ausdruck, • ein bedingter Ausdruck gebildet • oder ist mit Sprachmitteln aufgebaut, die erst später behandelt werden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 195 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Ausdruck, expression) (2) Jeder Ausdruck hat einen Typ: • Der Typ einer Konstanten ergibt sich aus der Signatur. • Der Typ eines Bezeichners ergibt sich aus dem Wert, den er bezeichnet. • Der Typ einer Funktionsanwendung ist der Ergebnistyp der Funktion. • Der Typ eines if-then-else-Ausdrucks ist gleich dem Typ des Ausdruck im then- bzw. else-Zweig. ©Arnd Poetzsch-Heffter TU Kaiserslautern 196 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Präzedenzregeln: Wenn Ausdrücke nicht vollständig geklammert sind, ist im Allg. nicht klar, wie ihr Syntaxbaum aussieht. Beispiele: 3 == 5 == True False == True || True False && True || True ©Arnd Poetzsch-Heffter TU Kaiserslautern 197 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Präzedenzregeln: (2) Präzedenzregeln legen fest, wie Ausdrücke zu strukturieren sind: • Am stärksten binden Funktionsanwendungen in Präfixform. • Regeln für Infix-Operatoren: infixl 7 ∗, /, div, mod infixl 6 +, − infix 4 ==, / =, <, >, <=, >= infixr 3 && infixr 2 || Je höher die Präzedenzzahl, desto stärker binden die Operationen. • Mit “infixl”/“infixr” gelistete Operatoren sind links-/rechtsassoziativ, d.h. sie werden von links/rechts her geklammert. • Mit “infix” gelistete Operatoren müssen geklammert werden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 198 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Deklaration und Bezeichnerbindung: Bisher haben wir Ausdrücke formuliert, die sich auf die vordefinierten Funktions- und Konstantenbezeichner von Haskell gestützt haben. Syntaktisch gesehen heißt Programmierung: • neue Typen, Werte und Funktionen zu definieren, • die neu definierten Elemente unter Bezeichnern zugänglich zu machen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 199 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Vereinbarung, Deklaration, Bindung) In Programmiersprachen dienen Vereinbarungen oder Deklarationen (engl. declaration) dazu, den in einem Programm verwendeten Elementen Bezeichner/Namen zu geben. Dadurch entsteht eine Bindung (n, e) zwischen dem Bezeichner n und dem bezeichneten Programmelement e. An allen Programmstellen, an denen die Bindung sichtbar ist, kann der Bezeichner benutzt werden, um sich auf das Programmelement zu beziehen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 200 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkung: • Die verschiedenen Arten an Programmelementen, die in Deklarationen vorkommen können, hängen von der Programmiersprache ab. • In Haskell sind es im Wesentlichen: 1. Bezeichnervereinbarungen (nur zusammen mit Wert-/Funktionsvereinbarung) 2. Wertvereinbarungen 3. Vereinbarungen (rekursiver) Funktionen 4. Vereinbarungen benutzerdeklarierter Typen • Die Regeln, die die Sichtbarkeit von Bindungen bzw. Bezeichern festlegen, sind ebenfalls sprachabhängig und können sehr komplex sein. Wir führen die Sichtbarkeitsregeln schrittweise ein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 201 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Wertvereinbarungen: • Wertvereinbarungen haben (u.a.) die Form: <Bezeichner > = <Ausdruck > ; • Wertvereinbarungen kann man eine Bezeichnervereinbarung voranstellen, um den Typ des Bezeichners zu deklarieren: <Bezeichner > :: <Typ > ; <Bezeichner > = <Ausdruck > ; Der Typ des Ausdrucks muss gleich dem vereinbarten Typ sein. • Der rechtsseitige Ausdruck darf nur sichtbare Bezeichner enthalten. ©Arnd Poetzsch-Heffter TU Kaiserslautern 202 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Wertvereinbarungen) b = 56 ; a::Int a = 7 sieben sieben flag dkv :: Float ; = 7.0 ; = floor sieben == truncate (- sieben ) = " Deutscher Komiker Verein e.v." ©Arnd Poetzsch-Heffter TU Kaiserslautern 203 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Wertvereinbarungen) (2) Einzeilige Vereinbarungen im Interpreter ghci: let b = 56 ; Mehrzeilige Vereinbarungen im Interpreter ghci: :{ let { a::Int a = 7 sieben sieben flag dkv } :} ©Arnd Poetzsch-Heffter ; ; :: Float ; = 7.0 ; = floor sieben == truncate (- sieben ) ; = " Deutscher Komiker Verein e.v." TU Kaiserslautern 204 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsvereinbarungen: Zwei Probleme: 1. Bisher haben wir keine Ausdrücke, die eine Funktion als Ergebnis liefern (Ausnahme: Funktionsbezeichner) 2. Funktionen können rekursiv sein, d.h. der Funktionsbezeichner kommt im definierenden Ausdruck vor. Lösungen: Zu 1. Erweitere die Menge der Ausdrücke, so dass Ausdrücke Funktionen beschreiben können. Dann kann die obige Wertvereinbarung genutzt werden. Zu 2. Erlaube selbstbezügliche Deklarationen (Haskells Lösung) oder benutze spezielle Syntax für rekursive Funktionsdeklarationen. Genaueres dazu in Unterabschnitt 3.1.2 (Folien 214ff). ©Arnd Poetzsch-Heffter TU Kaiserslautern 205 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionsvereinbarungen) myDivision :: Integer -> Integer -> Interger myDivision = div fac :: Integer -> Integer -- Argument n muss >= 0 sein fac n = if n==0 then 1 else n * fac (n -1) plus2 :: Integer -> Integer plus2 = (+) 2 ©Arnd Poetzsch-Heffter TU Kaiserslautern 206 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Typvereinbarungen: Zwei Probleme: 1. Bisher haben wir keine Ausdrücke, die Typen als Ergebnis liefern. 2. Typen können rekursiv sein, d.h. der vereinbarte Typbezeichner kommt im definierenden Typausdruck vor. Lösungen: Zu 1. Führe “Ausdrücke” für Typen ein (z.B. Int -> Int). Zu 2. Benutze spezielle Syntax für rekursive Typdeklarationen. Genaueres dazu in Unterabschnitt 3.1.4. (Folien 269ff). ©Arnd Poetzsch-Heffter TU Kaiserslautern 207 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Typvereinbarungen) type IntPaar = (Int ,Int) ; type CharList = [Char] ; type Telefonbuch = [(( String ,String ,String ,Int) ,[ String ])] ; type IntegerNachInteger = Integer -> Integer ; fakultaet :: IntegerNachInteger ; -- Argument muss >= 0 sein fakultaet = fac ; ©Arnd Poetzsch-Heffter TU Kaiserslautern 208 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Bezeichnerumgebung) Eine Bezeichnerumgebung ist eine Abbildung von Bezeichnern auf Werte (einschl. Funktionen) und Typen, ggf. auch auf andersartige Programmelemente. Oft spricht man auch von Namensumgebung oder einfach von Umgebung (engl. environment). ©Arnd Poetzsch-Heffter TU Kaiserslautern 209 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Programmiersprachen stellen üblicherweise eine Standard-Umgebung bereit mit den vordefinierten Programmelementen (Werten, Funktionen, Typen, etc.). In Haskell ist die Standard-Umgebung durch das Modul Prelude definiert. • Eine Bezeichnerumgebung wird häufig als Liste von Bindungen modelliert (vgl. Folie 200). • Jede Datenstruktur und jedes Modul definiert eine Bezeichnerumgebung. ©Arnd Poetzsch-Heffter TU Kaiserslautern 210 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Programm) Ein Programm besteht in der Regel aus • einer Menge von Deklarationen und • einer (durch einen besonderen Namen) ausgezeichneten Funktion bzw. Prozedur (oder ähnlichem Konstrukt), die angibt, wie die Auswertung bzw. Ausführung des Programms zu starten ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 211 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (funktionales Programm) import System .IO fac :: Integer -> Integer -- Argument n muss >= 0 sein fac n = if n==0 then 1 else n * fac (n -1) main = do { hSetBuffering stdout NoBuffering ; putStr " Eingabe x (x>=0): "; a <- readLn ; putStr " Ergebnis (fac x): "; print (fac a); } ©Arnd Poetzsch-Heffter TU Kaiserslautern 212 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.2 Rekursive Funktionen ©Arnd Poetzsch-Heffter TU Kaiserslautern 213 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Funktionsabstraktion Eine Funktion kann man durch einen Ausdruck beschreiben, in dem die Argumente der Funktion durch Bezeichner vertreten sind. Um deutlich zu machen, welche Bezeichner für Argumente stehen, werden diese deklariert. Alle anderen Bezeichner des Ausdrucks müssen anderweitig gebunden werden. Diesen Schritt von einem Ausdruck zu der Beschreibung einer Funktion nennt man Funktionsabstraktion oder λ-Abstraktion. ©Arnd Poetzsch-Heffter TU Kaiserslautern 214 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: Funktionsabstraktion 1. Quadratfunktion: Ausdruck: Abstraktion: Haskell-Notation: x ∗x λx.(x ∗ x) \ x -> (x * x) Vereinbarung eines Bezeichners für die Funktion: quadrat = \ x -> (x * x) ©Arnd Poetzsch-Heffter TU Kaiserslautern 215 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: Funktionsabstraktion (2) 2. Volumenberechnung eines Kegelstumpfes: Formel: Sei h die Höhe, rk , rg die Radien; dann ergibt sich das Volumen v zu π∗h ∗ (rk 2 + rk ∗ rg + rg 2 ) 3 Haskell-Ausdruck für die rechte Seite: v= (pi * h) / 3.0 * ( rk **2 + rk*rg + rg **2 ) Abstraktion in Haskell-Syntax und Vereinbarung von v: v = \ h rk rg -> (pi*h)/3.0 * (rk **2+ rk*rg+rg **2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 216 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: Funktionsabstraktion (3) 3. Abstraktion über Funktionsbezeichner: Ausdruck: Abstraktion: f (f x) \ f x -> f (f x) Mit Bezeichnervereinbarung: twice = \ f x -> f (f x) erg = ( twice sqrt) 3.0 Äquivalente Vereinbarung: twice2 = \ f -> \ x -> f (f x) erg = ( twice sqrt) 3.0 ©Arnd Poetzsch-Heffter TU Kaiserslautern 217 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration In Haskell gibt es unterschiedliche syntaktische Formen für die Funktionsdeklaration: 1. mittels direkter Wertvereinbarung: <Funktionsbez > = <Ausdruck von Funktionstyp > Beispiel: fib = \ n -> if n == 0 then 0 else if n == 1 then 1 else fib (n -1) + fib (n -2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 218 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration (2) 2. mittels einem oder mehreren formalen Parametern: <Funktionsbez > <Parameterbez1 > ... = <Ausdruck > Beispiel: fib n = if n == 0 then 0 else if n == 1 then 1 else fib (n -1) + fib (n -2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 219 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration (3) 3. mittels formalen Parametern und Fallunterscheidung über Wächtern: <Funktionsbez > <Parameterbez1 > ... | <boolscher Ausdruck > = <Ausdruck > ... | <boolscher Ausdruck > = <Ausdruck > Die boolschen Ausdrücke in der Deklaration heißen Wächter, engl. guards. Beispiel: fib | | | n n == 0 = 0 n == 1 = 1 otherwise = fib (n -1) + fib (n -2) Das Schlüsselwort otherwise steht hier für True. ©Arnd Poetzsch-Heffter TU Kaiserslautern 220 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration (4) 4. mittels Fallunterscheidung über Mustern: <Funktionsbez > <Parametermuster > ... = ... <Funktionsbez > <Parametermuster > ... = <Ausdruck > <Ausdruck > Muster sind ein mächtiges Programmierkonstrukt, das weiter unten genauer behandelt wird. Beispiel: fib 0 = 0 fib 1 = 1 fib n = fib (n -1) + fib (n -2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 221 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration (5) 5. mittels Kombinationen der Formen 3 und 4. Beispiel: fib fib | | 0 = 0 n n==1 = 1 otherwise = fib (n -1) + fib (n -2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 222 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Jeder Funktionsdeklaration sollte die Funktionssignatur vorangestellt werden und ein Kommentar, der mindestens die Voraussetzungen an die Parameter beschreibt. Beispiel: fib :: Integer -> Integer -- fib k verlangt : k >= 0 fib 0 = 0 fib 1 = 1 fib n = fib (n -1) + fib (n -2) • Die Form einer Funktionsdeklaration sollte so gewählt werden, dass die Deklaration gut lesbar ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 223 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (rekursive Funktionsdeklaration) 1. Einzelne rekursive Funktionsdeklaration: rpow :: Float -> Integer -> Float -- rpow r m verlangt : m >= 0 rpow r n = if n == 0 then 1.0 else r * rpow r (n -1) 2. Verschränkt rekursive Funktionsdeklaration: gerade :: Integer -> Bool ungerade :: Integer -> Bool -- Bedingung an Parameter n bei beiden Funktionen : -- n >= 0 gerade n = (n == 0) || ungerade (n -1) ungerade n = if n == 0 then False else gerade (n -1) ©Arnd Poetzsch-Heffter TU Kaiserslautern 224 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Deklaration rekursiver Funktionen Begriffsklärung: (rekursive Definition) Eine Definition oder Deklaration nennt man rekursiv, wenn der definierte Begriff bzw. das deklarierte Programmelement im definierenden Teil verwendet wird. Bemerkung: • Rekursive Definitionen finden sich in vielen Bereichen der Informatik und Mathematik, aber auch in anderen Wissenschaften und der nichtwissenschaftlichen Sprachwelt. • Wir werden hauptsächlich rekursive Funktions- und Datentypdeklarationen betrachten. ©Arnd Poetzsch-Heffter TU Kaiserslautern 225 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Definition: (rekursive Funktionsdekl.) Eine Funktionsdeklaration heißt direkt rekursiv, wenn der definierende Ausdruck eine Anwendung der definierten Funktion enthält. Eine Menge von Funktionsdeklarationen heißt verschränkt rekursiv oder indirekt rekursiv (engl. mutually recursive), wenn die Deklarationen gegenseitig voneinander abhängen. Eine Funktionsdeklaration heißt rekursiv, wenn sie direkt rekursiv ist oder Element einer Menge verschränkt rekursiver Funktionsdeklarationen ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 226 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (rekursive Funktion) Eine Funktion heißt rekursiv, wenn es rekursive Funktionsdeklarationen gibt, mit denen sie definiert werden kann. Bemerkungen: • Die Menge der rekursiven Funktionen ist berechnungsvollständig. • Rekursive Funktionsdeklarationen können als eine Gleichung mit einer Variablen verstanden werden, wobei die Variable von einem Funktionstyp ist: Beispiel: Gesucht ist die Funktion f , die folgende Gleichung für alle n ∈ Nat erfüllt: f n = if n = 0 then 1 else n ∗ f (n − 1) ©Arnd Poetzsch-Heffter TU Kaiserslautern 227 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Zur Auswertung von Funktionsanwendungen: Sei f x = A[x] ; Eine Funktionsanwendungen f e kann nach unterschiedlichen Strategien durch Verwendung der Deklarationsgleichungen ausgewertet werden, zum Beispiel call-by-value: • Werte Ausdruck e aus; Ergebnis nennt man den aktuellen Parameter z. • Ersetze x in A[x] durch z . • Werte den resultierenden Ausdruck A[z] aus. Haskell benutzt die Auswertungsstrategie call-by-need (siehe 3.4). Beispiele: (Rekursion) siehe Vorlesung ©Arnd Poetzsch-Heffter TU Kaiserslautern 228 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (lineare/repetitive Rekursion) Vereinfachend betrachten wir hier nur Funktionsdeklarationen, bei denen die Fallunterscheidung „außen“ und die rekursiven Aufrufe in den Zweigen der Fallunterscheidung stehen. • Eine rekursive Funktionsdeklaration heißt linear rekursiv, wenn in jedem Zweig der Fallunterscheidung höchstens eine rekursive Anwendung erfolgt (Beispiel: Definition von fac). • Eine rekursive Funktionsdeklaration heißt repetitiv (rekursiv), wenn sie linear rekursiv ist und die rekursiven Anwendungen in den Zweigen der Fallunterscheidung an äußerster Stelle stehen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 229 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: • Die übliche Definition von fac ist nicht repetitiv, da im Zweig der rekursiven Anwendung die Multiplikation an äußerster Stelle steht. • Die folgende Funktion facrep ist repetitiv: facrep :: Integer -> Integer -> Integer -- facrep n res verlangt : n >= 0 && res >= 1 facrep n res = if n == 0 then res else facrep (n -1) (res*n) fac n = facrep n 1 ©Arnd Poetzsch-Heffter TU Kaiserslautern 230 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Geschachtelte Rekursion) • Eine rekursive Funktionsdeklaration für f heißt geschachtelt rekursiv, wenn sie Teilausdrücke der Form f (. . . f (. . . ) . . . ) enthält. • Eine rekursive Funktionsdeklaration für f heißt kaskadenartig rekursiv, wenn sie Teilausdrücke der Form h(. . . f (. . . ) . . . f (. . . ) . . . ) enthält. ©Arnd Poetzsch-Heffter TU Kaiserslautern 231 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (kaskadenartige Rekursion) Berechne: Wie viele Kaninchen-Pärchen leben nach n Jahren, wenn man • am Anfang mit einem neu geborenden Pärchen beginnt, • jedes neu geborene Pärchen nach zwei Jahren und dann jedes folgende Jahr ein weiteres Pärchen Nachwuchs erzeugt und • die Kaninchen nie sterben. ©Arnd Poetzsch-Heffter TU Kaiserslautern 232 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (kaskadenartige Rekursion) (2) Die Anzahl der Pärchen stellen wir als Funktion ibo von n dar: • vor dem 1. Jahr: ibo(0) = 1 • nach dem 1. Jahr: ibo(1) = 1 • nach dem 2. Jahr: ibo(2) = 2 • nach dem n. Jahr: die Anzahl aus dem Jahr vorher plus die Anzahl der im n. Jahr Geborenen; und die ist gleich der Anzahl vor zwei Jahren, also: ibo n = ibo(n − 1) + iob(n − 2) für n > 1. Insgesamt ergibt sich folgende kaskadenartige Funktionsdeklaration: ibo n = ©Arnd Poetzsch-Heffter if n<=1 then 1 else ibo (n -1) + ibo (n -2) TU Kaiserslautern 233 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkung: • Aus Beschreibungssicht spielt die Form der Rekursion keine Rolle; wichtig ist eine möglichst am Problem orientierte Beschreibung. • Aus Programmierungssicht spielt Auswertungseffizienz eine wichtige Rolle, und diese hängt von der Form der Rekursion ab. Beispiel: Kaskadenartige Rekursion führt im Allg. zu einer exponentiellen Anzahl von Funktionsanwendungen (z.B. bei ibo 30 bereits 1.664.079 Anwendungen). ©Arnd Poetzsch-Heffter TU Kaiserslautern 234 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.3 Listen und Tupel ©Arnd Poetzsch-Heffter TU Kaiserslautern 235 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Listen Eine Liste über einem Typ T ist eine total geordnete Multimenge mit Elementen aus T (bzw. eine Folge, d.h. eine Abb. Nat -> T ). Eine Liste heißt endlich, wenn sie nur endlich viele Elemente enthält. Haskell stellt standardmäßig eine Datenstruktur für Listen bereit, die bzgl. des Elementtyps parametrisiert ist. Typparameter werden üblicherweise geschrieben als a, b, ... ©Arnd Poetzsch-Heffter TU Kaiserslautern 236 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Listen (2) Typ: [a] , a ist Typparameter Funktionen: (==), (/=) (:) (++) head, last tail, init null length (!!) take, drop :: :: :: :: :: :: :: :: :: [a] → [a] → Bool a → [a] → [a] [a] → [a] → [a] [a] → a [a] → [a] [a] → Bool [a] → Int [a] → Int → a Int → [a] → [a] wenn (==) auf a definiert Konstanten: [] ©Arnd Poetzsch-Heffter :: [a] TU Kaiserslautern 237 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Listen (3) Dem Typ [a] ist als Wertemenge die Menge aller Listen über Elementen vom Typ a zugeordnet. Notation: In Haskell gibt es eine vereinfachende Notation für Listen: statt x1 : x2 : ... : xn : [] kann man schreiben: [ x1 , x2 , ..., xn ] ©Arnd Poetzsch-Heffter TU Kaiserslautern 238 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionen auf Listen) 1. Addiere alle Zahlen einer Liste vom Typ [Int] mit neutralem Element 0: foldplus :: [Int] -> Int foldplus xl = if null xl then 0 else (head xl) + foldplus (tail xl) foldplus [1 ,2 ,3 ,4 ,5 ,6] 2. Prüfen einer Liste von Zahlen auf Sortiertheit: ist_sortiert :: [Int] -> Bool ist_sortiert xl = if null xl || null (tail xl) then True else if (head xl)<=(head (tail xl)) then ist_sortiert (tail xl) else False ©Arnd Poetzsch-Heffter TU Kaiserslautern 239 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionen auf Listen) (2) 3. Zusammenhängen zweier Listen (engl. append): append :: [a] -> [a] -> [a] append l1 l2 = if l1 == [] then l2 else (head l1):( append (tail l1) l2) 4. Umkehren einer Liste: rev :: [a] -> [a] rev xl = if null xl then [] else append (rev (tail xl)) [head xl] ©Arnd Poetzsch-Heffter TU Kaiserslautern 240 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionen auf Listen) (3) 5. Zusammenhängen der Elemente einer Liste von Listen: concat :: [[a]] -> [a] concat xl = if null xl then [] else append (head xl) ( concat (tail xl)) 6. Wende eine Liste von Funktionen vom Typ Int -> Int nacheinander auf eine ganze Zahl an: seqappl :: [( Int -> Int)] -> Int -> Int seqappl xl i = if null xl then i else seqappl (tail xl) (( head xl) i) ©Arnd Poetzsch-Heffter TU Kaiserslautern 241 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Rekursive Funktionsdeklaration sind bei Listen angemessen, weil Listen rekursive Datenstrukturen sind. • Mit Mustern lassen sich die obigen Deklaration noch eleganter fassen (s. unten). ©Arnd Poetzsch-Heffter TU Kaiserslautern 242 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstrukturen der Paare Wir betrachten zunächst Paare und verallgemeinern dann auf n-Tupel: Paare oder 2-Tupel sind die Elemente des kartesischen Produktes zweier ggf. verschiedener Mengen oder Typen. Der Typ der Paare ist also ein Produkttyp. Als Typkonstruktor wird (a,b) in Mixfix-Schreibweise benutzt. Haskell stellt standardmäßig eine Datenstruktur für Paare bereit, die bzgl. der Elementtypen parametrisiert ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 243 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstrukturen der Paare (2) Typ: (a,b) , a, b sind Typparameter Funktionen: (==), (/=) :: (a, b) → (a, b) → Bool wenn (==) auf a und b definiert (_,_) :: a → b → (a, b) fst :: (a, b) → a snd :: (a, b) → b Konstanten: keine Dem Typ (a,b) ist die Menge der geordneten Paare mit Elementen vom Typ a und b zugeordnet. ©Arnd Poetzsch-Heffter TU Kaiserslautern 244 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Funktionen auf Paaren) Transformiere eine Liste von Paaren in ein Paar von Listen: unzip :: [(a, b)] -> ([a], [b]) unzip xl = if null xl then ([] , []) else ( (fst (head xl)):(fst ( unzip (tail xl))), (snd (head xl)):(snd ( unzip (tail xl))) ) it = unzip [(1 , 2) , (3, 4) , (9, 10)] (auch das geht erheblich schöner mit Mustern) ©Arnd Poetzsch-Heffter TU Kaiserslautern 245 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der n-Tupel Haskell unterstützt n-Tupel für alle n ≥ 3: Typ: (a,b,...) , a, b, ... sind Typparameter Funktionen: (==), (/=) :: (a, b, ...) → (a, b, ...) → Bool wenn (==) auf a, b, ... definiert (_,_,...) :: a → b → ... → (a, b, ...) Konstanten: keine Seien n ≥ 3 und a1 , . . . , an Typen mit Wertemenge w(a1 ), . . . , w(an ); dann ist dem Tupeltyp (a1 , . . . , an ) das kartesische Produkt w(a1 ) × · · · × w(an ) als Wertemengen zugorndet; also eine Menge geordneter n-Tupel, wobei das i-te Element vom Typ ai ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 246 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Es gibt keine Funkionen, um Elemente aus einem Tupel zu selektieren. Dafür benötigt man Muster (siehe unten). • Paare sind wie 2-Tupel, auf ihnen sind aber die Selektorfunktionen fst und snd definiert. • Es gibt keine 1-Tupel: Klammern um Ausdrücke dienen nur der Strukturierung und haben darüber hinaus keine Bedeutung; d.h. wenn e ein Ausdruck ist, ist ( e ) gleichbedeutend mit e. ©Arnd Poetzsch-Heffter TU Kaiserslautern 247 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Einheit Haskell unterstützt eine Datenstruktur mit einem definierten Wert: Typ: () Funktionen: (==), (/=) :: () → () → Bool Konstanten: () :: () Dem Typbezeichner () ist eine einelementige Wertemenge zugeordnet. Der Wert wird als Einheit (engl. unity) bezeichnet. Bemerkung: Die Einheit wird oft als Ergebnis verwendet, wenn es keine relevanten Ergebniswerte gibt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 248 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Geschachtelte Tupel) Mit der Tupelbildung lassen sich „baumstrukturierte“Werte, sogenannte Tupelterme, aufbauen. So entspricht der Tupelterm: ( (8,True), (("Tupel", "sind", "toll"), "aha")) dem Baum: 8 True "aha" "Tupel" ©Arnd Poetzsch-Heffter "sind" TU Kaiserslautern "toll" 249 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Funktionen auf n-Tupeln) 1. Flache ein Paar von Paaren in ein 4-Tupel aus: ausflachen :: ((a, b) ,(c, d)) -> (a, b, c, d) -- nimmt ein Paar von Paaren und liefert 4- Tupel ausflachen pp = ( fst (fst pp), snd (fst pp), fst (snd pp), snd (snd pp) ) it = ausflachen ( (True ,7) , (’x’ ,5.6) ) Alternative Deklaration mit Mustern: ausflachen ((a, b) ,(c, d)) = (a, b, c, d) 2. Funktion zur Paarbildung: paarung :: a -> b -> (a,b) paarung lk rk = (lk ,rk) ©Arnd Poetzsch-Heffter TU Kaiserslautern 250 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionstypen: Eine zweistellige Funktion f mit Argumenten vom Typ a und Typ b und Ergebnistyp c kann in Haskell auf zwei Arten typisiert werden: 1. Gecurryte Form: f :: a -> b -> c Nach den Präzedenzregeln für -> ist das a -> (b ->c), also eine Funktion, die ein Wert vom Typ a nimmt und eine Funktion vom Typ b -> c liefert. Ist x::a und y::b, dann sind (f x) y oder gleichbedeutend f x y korrekte Anwendungen. 2. Tupel-Form: f :: (a,b) -> c In diesem Fall ist für x::a und y::b, f (x,y) eine korrekte Anwendungen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 251 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: Die Additionsoperation (+) auf Typ Int hat in Haskell den Typ (+) :: Int -> Int -> Int In der Mathematik typisiert man die Additionsoperation plus üblichweise mit: plus :: (Int ,Int) -> Int Diese Variante kann man in Haskell wie folgt definieren: plus ip = (fst ip) + (snd ip) Oder eleganter mit Mustern: plus (m,n) = m + n ©Arnd Poetzsch-Heffter TU Kaiserslautern 252 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Muster in Deklarationen und Ausdrücken Muster sind ein Sprachkonstrukt um strukturierte Werte einfacher handhaben zu können (siehe Funktion ausflachen). Ein Wert heißt hier strukturiert, wenn er mittels Konstruktoren aus anderen Werten zusammengebaut wurde. Konstruktoren sind spezielle Haskell-Funktionen. Bisher behandelte Konstruktoren: • der Listkonstruktor (:) (daher der Name “cons”) • die Tupelbildung durch (_,...,_) In 3.1.4 werden wir benutzerdefinierte Konstruktoren kennen lernen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 253 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Muster in Haskell) Muster (engl. Pattern) in Haskell sind Ausdrücke gebildet über Bezeichnern, Konstanten und Konstruktoren. Alle Bezeichner in einem Muster müssen verschieden sein. Ein Muster M mit Bezeichnern b1 , . . . , bk passt auf einen strukturierten Wert w (engl.: a pattern matches a value w), wenn es eine Substitution der Bezeichner bj in M durch Werte vj gibt, in Zeichen M[v1 /b1 , . . . , vk /bk ], so dass M[v1 /b1 , . . . , vk /bk ] = w ©Arnd Poetzsch-Heffter TU Kaiserslautern 254 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (ML-Muster, Passen) 1. (x,y) passt auf (4,5) mit Substitution x=4, y=5. 2. (erstesElem,zweitesElem) passt auf (-47,(True,"dada")) mit erstesElem =-47, zweitesElem =(True,"dada") . 3. x:xs passt auf 7:8:9:[] mit x = 7 und xs = 8:9:[] , d.h. xs = [8,9]. 4. x1:x2:xs passt auf 7:8:9:[] mit x1 = 7, x2 = 8, xs = [9] . ©Arnd Poetzsch-Heffter TU Kaiserslautern 255 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (ML-Muster, Passen) (2) 5. first:rest passt auf ["computo","ergo","sum"] mit first ="computo" und rest =["ergo", "sum"] . 6. ((8,x), (y,"aha")) passt auf ((8,True), (("Tupel","sind","toll"), "aha")) mit x = True und y = ("Tupel","sind","toll"). 8 ©Arnd Poetzsch-Heffter True y TU Kaiserslautern "aha" 256 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Wertvereinbarungen mit Mustern Muster können in Haskell-Wertvereinbarungen verwendet werden: <Muster > = <Ausdruck > ; Wenn das Muster auf den Wert des Ausdrucks passt und σ die zugehörige Substitution ist, werden die Bezeichner im Muster gemäß σ an die zugehörige Werte gebunden. Wenn das Muster auf den Wert des Ausdrucks nicht passt, wird eine Ausnahme erzeugt, sobald auf einen der deklarierten Bezeichner zugegriffen wird. Beispiel: (Wertvereinbarung mit Muster) (x, y) = ©Arnd Poetzsch-Heffter (4, 5); TU Kaiserslautern 257 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsvereinbarung mit Mustern Muster können in Haskell-Funktionsdeklarationen verwendet werden (vgl. Folie 221): <Funktionsbez > <Parametermuster > ... = ... <Funktionsbez > <Parametermuster > ... = <Ausdruck > <Ausdruck > Bei der Funktionsanwendung wird der Reihe nach geprüft, auf welches Parametermuster der aktuelle Parameter passt (vgl. Folie 228). Die Gleichung zum ersten passenden Fall wird verwendet. ©Arnd Poetzsch-Heffter TU Kaiserslautern 258 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionsdeklaration mit Mustern) 1. Deklaration von foldplus ohne Muster: foldplus :: [Int] -> Int foldplus xl = if null xl then 0 else (head xl) + foldplus (tail xl) Deklaration von foldplus mit Muster: foldplus :: [Int] -> Int foldplus [] = 0 foldplus (x:xl) = x + foldplus xl ©Arnd Poetzsch-Heffter TU Kaiserslautern 259 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionsdeklaration mit Mustern) (2) 2. Deklaration von ist_sortiert::[Int] ->Bool mit drei Mustern: ist_sortiert [] = True ist_sortiert (x:[]) = True ist_sortiert (x1:x2:xs) = if x1 <= x2 then ist_sortiert (x2:xs) else False Deklaration mit drei Mustern und Wächtern: ist_sortiert [] ist_sortiert (x:[]) ist_sortiert (x1:x2:xs) | x1 <= x2 | otherwise ©Arnd Poetzsch-Heffter = = True True = = ist_sortiert (x2:xs) False TU Kaiserslautern 260 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionsdeklaration mit Mustern) (3) Deklaration von ist_sortiert::[Int]->Bool mit zwei Mustern und Wächtern: ist_sortiert (x1:x2:xs) | x1 <= x2 = | otherwise = ist_sortiert x = ist_sortiert (x2:xs) False True 3. Deklaration von append ::[a]->[a]->[a]: append [] xl2 = xl2 append (x:xl) xl2 = x : ( append xl xl2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 261 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionsdeklaration mit Mustern) (4) 4. Verwendung geschachtelter Muster: unzip :: [(a, b)] -> ([a],[b]) unzip [] = ([] , []) unzip ((x,y):ps) = ( (x : (fst ( unzip ps))), (y : (snd ( unzip ps))) ) ©Arnd Poetzsch-Heffter TU Kaiserslautern 262 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung let-Ausdruck Der Mustermechanismus kann auch innerhalb von Ausdrücken eingesetzt werden. Syntax des let-Ausdrucks: let <Liste von Deklarationen > in <Ausdruck > Die aus den Deklarationen resultierenden Bindungen sind nur im let-Ausdruck gültig. D.h. sie sind sichtbar im let-Ausdruck an den Stellen, an denen sie nicht verdeckt sind. ©Arnd Poetzsch-Heffter TU Kaiserslautern 263 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (let-Ausdruck) a = let a = 2*3 in a*a b = let a = 2*3 in let (b,c) = (a,a+1) in a*b*c unzip :: [(a, b)] -> ([a], [b]) unzip [] = ([] , []) unzip ((x,y):ps) = let (xs , ys) = unzip ps in ((x:xs), (y:ys)) ©Arnd Poetzsch-Heffter TU Kaiserslautern 264 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung case-Ausdruck Syntax des case-Ausdrucks: case <Ausdruck0 > of <Muster1 > -> <Ausdruck1 > ... <MusterN > -> <AusdruckN > Prüfe der Reihe nach, ob der resultierende Wert von <Ausdruck0> auf eines der Muster passt. Passt er auf ein Muster, nehme die entsprechenden Bindungen vor und werte den zugehörigen Ausdruck aus (die Bindungen sind nur in dem zugehörigen Ausdruck gültig). ©Arnd Poetzsch-Heffter TU Kaiserslautern 265 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (case-Ausdruck) ist_sortiert xl = case xl of [] -> True (x:[]) -> True (x1:x2:xs) -> if x1 <= x2 then ist_sortiert (x2:xs) else False ©Arnd Poetzsch-Heffter TU Kaiserslautern 266 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Das Verbot von gleichen Bezeichnern in Mustern hat im Wesentlichen den Grund, dass nicht für alle Werte/Typen die Gleichheitsoperation definiert ist. mal2 = twotimes = (a,a) = \x -> 2*x \x -> x+x (mal2 , twotimes ) • Wenn keines der angegebenen Muster passt, wird eine Ausnahme erzeugt (abrupte Terminierung). ©Arnd Poetzsch-Heffter TU Kaiserslautern 267 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.4 Benutzerdefinierte Datentypen ©Arnd Poetzsch-Heffter TU Kaiserslautern 268 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Benutzerdefinierte Datentypen Fast alle modernen Spezifikations- und Programmiersprachen gestatten es dem Benutzer, „neue“ Typen zu definieren. Übersicht: • Vereinbarung von Typbezeichnern • Deklaration neuer Typen • Summentypen • Rekursive Datentypen ©Arnd Poetzsch-Heffter TU Kaiserslautern 269 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Vereinbarung von Typbezeichnern Haskell erlaubt es, Bezeichner für Typen zu deklarieren (vgl. F. 208): type IntPaar = (Int ,Int) ; type CharList = [Char] ; type Telefonbuch = [(( String ,String ,String ,Int) ,[ String ])] ; type IntegerNachInteger = Integer -> Integer ; fakultaet :: IntegerNachInteger ; -- Argument muss >= 0 sein fakultaet = fac ; Dabei wird kein neuer Typ definiert, sondern nur ein “neuer” Bezeichner an einen bekannten Typ gebunden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 270 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: (Typvereinbarungen) • Typvereinbarungen können zur Abkürzung oder zur Verdeutlichung benutzt werden (siehe Typ Telefonbuch). • Zwei unterschiedliche Bezeichner können den gleichen Typ bezeichnen; z.B.: type IntTriple = type Date = (Int ,Int ,Int) (Int ,Int ,Int) kalenderwoche :: Date -> Int -- Parameter muss existierenden Kalendertag sein kalenderwoche (tag ,monat ,jahr) = ... kalenderwoche (11 ,12 ,2003) ©Arnd Poetzsch-Heffter TU Kaiserslautern 271 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Deklaration neuer Typen Neue Typen werden in Haskell mit der datatype-Deklaration definiert, die im Folgenden schrittweise erläutert wird. Definition eines neuen Typs und Konstruktors: data <NeuerTyp > = <Konstruktor > <Typ1 > ... <TypN > Die obige Datatypdeklaration definiert: • einen neuen Typ und bindet ihn an <NeuerTyp> • eine Konstruktorfunktion mit Signatur <Konstruktor >:: <Typ1 > -> ... -> <TypN > -> <NeuerTyp > Die Konstruktorfunktion ist injektiv. Typ- und Konstruktorbezeichner müssen mit einem Großbuchstaben beginnen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 272 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Definition von Typ, Konstruktor, Selektoren) data Person = Student String String Int Int definiert den neuen Typ Person und den Konstruktor Student :: String -> String -> Int -> Int -> Person Wir definieren dazu folgende Selektorfunktionen: vorname :: Person -> String vorname ( Student v n g m) = v name :: Person -> String name ( Student v n g m) = n geburtsdatum :: Person -> Int geburtsdatum ( Student v n g m) = g matriknr :: Person -> Int matriknr ( Student v n g m) ©Arnd Poetzsch-Heffter = m TU Kaiserslautern 273 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: Jede Datentypdeklaration definiert einen neuen Typ, d.h. insbesondere: • die Werte des neuen Typs sind inkompatibel mit allen anderen Typen; • auch Werte strukturgleicher benutzerdefinierter Typen sind inkompatibel. ©Arnd Poetzsch-Heffter TU Kaiserslautern 274 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Typkompatibilität) 1. Der Typ Person ist inkompatibel mit dem Tupeltyp type Person2 = (String ,String ,Int ,Int) Insbesondere ist vorname ("Niels","Bohr",18851007,221) nicht typkorrekt. 2. Person ist inkompatibel mit dem strukturgleichen Typ Adresse: data Adresse = Wohnung String String Int Int Insbesondere ist name ( Wohnung " Casimirring " " Lautern " 27 67663 ) nicht typkorrekt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 275 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkung: Den Konstruktor kann man sich als eine Markierung der Werte seines Argumentbereichs vorstellen. Dabei werden Werte mit unterschiedlicher Markierung als verschieden betrachtet. Konstruktoren erlauben es in gewisser Weise neue Produkttypen zu definieren. ©Arnd Poetzsch-Heffter TU Kaiserslautern 276 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Summentypen Ein Summentyp stellt die disjunkte Vereinigung der Elemente anderen Typen zu einem neuen Typ dar. Die meisten modernen Programmiersprachen unterstützen die Deklaration von Summentypen. In Haskell definiert man Summentypen durch Angabe von Alternativen bei der datatype-Deklaration: data <NeuerTyp > = <Konstruktor1 > | <Konstruktor2 > ... | <KonstruktorM > ©Arnd Poetzsch-Heffter <Typ1_1 > ... <Typ1_N1 > <Typ2_1 > ... <Typ2_N2 > <TypM_1 > ... <TypM_NM > TU Kaiserslautern 277 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Summentypen) 1. Ein anderer Datentyp zur Behandlung von Personen: data Person2 = Student String String Int Int | Mitarbeiter String String Int Int | Professor String String Int Int String ©Arnd Poetzsch-Heffter TU Kaiserslautern 278 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Summentypen) (2) 2. Eine benutzerdefinierte Datenstruktur für Zahlen: data MyNumber = Intc | Floatc Int Float isInt :: MyNumber -> Bool isInt (Intc m) = True isInt ( Floatc r) = False isFloat :: MyNumber -> Bool isFloat (Intc m) = False isFloat ( Floatc r) = True neg :: MyNumber -> MyNumber neg (Intc m) = Intc (-m) neg ( Floatc r) = Floatc (-r) ©Arnd Poetzsch-Heffter TU Kaiserslautern 279 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Summentypen) (3) plus :: MyNumber -> MyNumber -> MyNumber plus (Intc m) (Intc n) = Intc (m+n) plus (Intc m) ( Floatc r) = Floatc (( fromInteger ( toInteger m))+r) plus ( Floatc r) (Intc m) = Floatc (r+( fromInteger ( toInteger m))) plus ( Floatc r) ( Floatc q) = Floatc (r+q) ©Arnd Poetzsch-Heffter TU Kaiserslautern 280 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: Konstruktorfunktionen oder Konstruktoren liefern Werte des neu definierten Datentyps. Sie können in Mustern verwendet werden (z.B.: Student, Intc). Diskriminatorfunktionen oder Diskriminatoren prüfen, ob der Wert eines benutzerdefinierten Datentyps zu einer bestimmten Alternative gehört (Beispiel: isInt). Selektorfunktionen oder Selektoren liefern Komponenten von Werten des definierten Datentyps (z.B.: vorname, name, . . . ). Bemerkung: In funktionalen Sprachen kann man meist auf Selektorfunktionen verzichten. Man verwendet stattdessen Muster/Pattern. ©Arnd Poetzsch-Heffter TU Kaiserslautern 281 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Weitere Operationen auf neu deklarierten Typen: Konstruktoren und Selektoren erlauben das Aufbauen und Zerlegen der Werte neu deklarierter Typen. Durch den Zusatz: deriving (Eq ,Show) liefert Haskell auch eine standardmäßige Gleichheit und die Möglichkeit, Werte des neuen Typs mittels print auszugeben. Zum Beipiel: data MyNumber = Intc Int | Floatc Float deriving (Eq ,Show) Bemerkung: Haskell ermöglich es dem Benutzer auch, die Gleichheit oder andere Operationen auf neu definierten Typen selbst zu definieren. ©Arnd Poetzsch-Heffter TU Kaiserslautern 282 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Weitere Anwendungen der datatype-Deklaration: Die datatype-Deklaration kann auch verwendet werden, um Aufzählungstypen zu definieren, indem nur null-stellige Konstruktoren benutzt werden. Die Wertemenge eines Aufzählungstyps ist eine endliche Menge (von Namen). ©Arnd Poetzsch-Heffter TU Kaiserslautern 283 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Aufzählungstypen) data Wochentag = Montag | Dienstag | Mittwoch | Donnerstag | Freitag | Samstag | Sonntag deriving (Eq ,Show) istMittwoch :: Wochentag -> Bool istMittwoch Mittwoch = True istMittwoch _ = False Oder knapper: istMittwoch w ©Arnd Poetzsch-Heffter = (w== Mittwoch ) TU Kaiserslautern 284 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Konstruktoren mit beliebiger Stelligkeit In einer Datentypdeklaration können Konstruktoren mit beliebiger Stelligkeit kombiniert werden; z.B.: data MaybeInt = | Nothing Just Int Haskell sieht dafür im Prelude den folgenden parametrisierten Typ vor (vgl. 3.3): data Maybe a ©Arnd Poetzsch-Heffter = | Nothing Just a TU Kaiserslautern 285 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Rekursive Datentypen Von großer Bedeutung in allen Paradigmen der Programmierung sind rekursive Datentypen. Sie erlauben es insbesondere: • Listen beliebiger Länge • Bäume beliebiger Höhe behandeln zu können. ©Arnd Poetzsch-Heffter TU Kaiserslautern 286 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Definition: (rekursive Datentypen) Eine Datentypdeklaration heißt direkt rekursiv, wenn der neu definierte Typ in einer der Alternativen der Datentypdeklaration vorkommt. Wie bei Funktionen gibt es auch verschränkt rekursive Datentypdeklarationen. Eine Datentypdeklaration heißt rekursiv, wenn sie direkt rekursiv ist oder Element einer Menge verschränkt rekursiver Datentypdeklarationen ist. Ein Datentyp heißt rekursiv, wenn er mit einer rekursiven Datentypdeklaration definiert wurde. ©Arnd Poetzsch-Heffter TU Kaiserslautern 287 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Listendatentypen) 1. Ein Datentyp für Integer-Listen: data Intlist = Nil | Cons Int Intlist 2. Ein Datentyp für homogene Listen mit Elementen von beliebigem Typ: data List a | ©Arnd Poetzsch-Heffter = Nil Cons a (List a) TU Kaiserslautern 288 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Baumartige Datenstrukturen: Ottmann, Widmayer: „Bäume gehören zu den wichtigsten in der Informatik auftretenden Datenstrukturen“. ©Arnd Poetzsch-Heffter TU Kaiserslautern 289 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärungen: (zu Bäumen) • In einem endlich verzweigten Baum hat jeder Knoten endlich viele Kinder. • Üblicherweise sagt man, die Kinder sind von links nach rechts geordnet. • Einen Knoten ohne Kinder nennt man ein Blatt, einen Knoten mit Kindern einen inneren Knoten oder Zweig. • Den Knoten ohne Elter nennt man Wurzel. • Ein Baum heißt markiert, wenn jeder Knoten k eine Markierung m(k ) besitzt. • In einem Binärbaum hat jeder Knoten maximal zwei Kinder. • Zu jedem Knoten k gehört ein Unterbaum, nämlich der Baum der k als Wurzel hat. ©Arnd Poetzsch-Heffter TU Kaiserslautern 290 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Datentyp für markierte Binärbäume: data IntBBaum = Blatt Int | Zweig Int IntBBaum IntBBaum deriving (Eq ,Show) einbaum = Zweig 7 ( Zweig 3 ( Blatt 2) ( Blatt 4)) ( Blatt 5) mark :: IntBBaum -> Int mark (Blatt n) = n mark (Zweig n lk rk) = n ©Arnd Poetzsch-Heffter TU Kaiserslautern 291 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Definition: (Sortiertheit markierter Binärbäume) Ein mit ganzen Zahlen markierter Binärbaum heißt sortiert, wenn für alle Knoten k gilt: • Alle Markierungen der linken Nachkommen von k sind kleiner als m(k ). • Alle Markierungen der rechten Nachkommen von k sind größer als m(k ). ©Arnd Poetzsch-Heffter TU Kaiserslautern 292 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Prüfung von Sortiertheit Aufgabe: Prüfe, ob ein Baum vom Typ IntBBaum sortiert ist. Idee: Berechne zu jedem Unterbaum die minimale und maximale Markierung und prüfe rekursiv die Sortiertheitseigenschaft für alle Knoten/Unterbäume. maxmark , minmark :: IntBBaum -> Int maxmark ( Blatt n) = n maxmark ( Zweig n lk rk) = n `max` ( maxmark lk `max` maxmark rk) minmark ( Blatt n) = n minmark ( Zweig n lk rk) = n `min` ( minmark lk `min` minmark rk) ©Arnd Poetzsch-Heffter TU Kaiserslautern 293 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Prüfung von Sortiertheit (2) istsortiert :: IntBBaum istsortiert ( Blatt n) istsortiert ( Zweig n lk istsortiert lk && ( maxmark lk)<n && -> Bool = True rk) = istsortiert rk && n <( minmark rk) result = istsortiert einbaum Wenig effiziente Lösung! Besser ist es, die Berechnung von Minima und Maxima mit der Sortiertheitsprüfung zu verschränken. Idee: Entwickle eine Funktion istsortiert3 mit drei Ergebniswerten: • Angabe, ob Baum sortiert • minimale Markierung des Baums • maximale Markierung des Baums ©Arnd Poetzsch-Heffter TU Kaiserslautern 294 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Prüfung von Sortiertheit (3) istsortiert3 :: IntBBaum -> (Bool ,Int ,Int) -- Sei istsortiert3 b == (srt ,mn ,mx) ; dann ist -srt das Ergebnis der Sortiertheitspruefung von b -mn die minimale Markierung von b -mx die maximale Markierung von b istsortiert3 ( Blatt n) = (True , n, n) istsortiert3 ( Zweig n lk rk) = let (lsrt , lmn , lmx) = istsortiert3 lk (rsrt , rmn , rmx) = istsortiert3 rk in (( lsrt && rsrt && lmx <n && n<rmn), lmn , rmx) istsortiert b = let (srtflag ,_ ,_) = ( istsortiert3 b) in srtflag ©Arnd Poetzsch-Heffter TU Kaiserslautern 295 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Weitere Begriffe zu Bäumen) Der leere Baum ist ein Baum ohne Knoten. Die Tiefe eines Knotens in einem Baum ist sein Abstand zur Wurzel. Der Wurzelknoten hat die Tiefe 0. Für alle anderen Knoten k gilt: tiefe(k ) = tiefe(elternknoten(k )) + 1 Die Knoten gleicher Tiefe t nennt man das Niveau t. Die Höhe des leeren Baumes ist 0. Die Höhe eines nicht-leeren Baumes b ist die maximale Knotentiefe plus 1: höhe( b ) = max { tiefe(k ) | k Knoten von b } + 1. Die Größe eines Baums ist die Anzahl seiner Knoten. ©Arnd Poetzsch-Heffter TU Kaiserslautern 296 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Datentyp möglicherweise leerer Binärbäume: data IntBBaum2 = Leer | Knoten Int IntBBaum2 IntBBaum2 ©Arnd Poetzsch-Heffter TU Kaiserslautern 297 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bäume mit variabler Kinderzahl: Bäume mit variabler Kinderzahl lassen sich realisieren: • durch mehrere Alternativen für Zweige (begrenzte Anzahl von Kindern) • durch Listen von Unterbäumen: data VBaum = Kn Int [ VBaum ] deriving (Eq ,Show) Der Rekursionsanfang ergibt sich durch Knoten mit leerer Unterbaumliste. ©Arnd Poetzsch-Heffter TU Kaiserslautern 298 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bäume mit variabler Kinderzahl: (2) zaehleKnVBaum :: VBaum -> Int zaehleKnVBaumLst :: [ VBaum ] -> Int zaehleKnVBaum (Kn _ xs) = 1 + ( zaehleKnVBaumLst xs) zaehleKnVBaumLst [] = 0 zaehleKnVBaumLst (x:xs) = ( zaehleKnVBaum x) + ( zaehleKnVBaumLst xs) ©Arnd Poetzsch-Heffter TU Kaiserslautern 299 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Bäume mit variabler Kinderzahl lassen sich z.B. zur Repräsentation von Syntaxbäumen verwenden, indem das Terminal- bzw. Nichtterminalsymbol als Markierung verwendet wird. Besser ist es allerdings, die Information über die Symbole mittels Konstruktoren auszudrücken (vgl. nächstes Beispiel). • Bäume mit variabler Kinderzahl werden auch zur Repräsentation von strukturierten oder semi-strukturierter Daten verwendet (z.B. XML, ...) ©Arnd Poetzsch-Heffter TU Kaiserslautern 300 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (abstrakte Syntaxbäume) Der abstrakte Syntaxbaum eines Programms repräsentiert die Programmstruktur unter Verzicht auf Schlüsselworte und Trennzeichen. Rekursive Datentypen eignen sich sehr gut zur Beschreibung von abstrakter Syntax. Als Beispiel betrachten wir die abstrakte Syntax von Femto: data Programm deriving data Wertdekl deriving data Ausdruck = Prog [ Wertdekl ] Ausdruck (Eq ,Show) = Dekl String Ausdruck (Eq ,Show) = Bzn String | Zahl Int | Add Ausdruck Ausdruck | Mul Ausdruck Ausdruck deriving (Eq ,Show) ©Arnd Poetzsch-Heffter TU Kaiserslautern 301 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (abstrakte Syntaxbäume) (2) Das Femto-Programm a = 73; main = print ( a + 12 ) Wird durch folgenden Baum repräsentiert: Prog [Dekl "a" (Zahl 73)] (Add (Bzn "a") (Zahl 12)) Die Baumrepräsentation eignet sich besser als die Zeichenreihenrepräsentation zur weiteren Verarbeitung von Programmen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 302 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Verschränkte Datentypdeklarationen: Haskell unterstützt verschränkt rekursive Datentypdeklarationen. Die Datentypdeklaration werden einfach hintereinander geschreiben (wie verschränkt rekursiven Funktionsdeklarationen). Bei abstrakten Syntaxbäumen wird häufig verschränkte Rekursion der Datentypen benötigt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 303 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (verschränkte Datentypen) Als Beispiel betrachten wir die abstrakte Syntax einer Erweiterung von Femto um let-Ausdrücke: data Programm deriving data Wertdekl deriving data Ausdruck = Prog [ Wertdekl ] Ausdruck (Eq ,Show) = Dekl String Ausdruck (Eq ,Show) = Bzn String | Zahl Int | Add Ausdruck Ausdruck | Mul Ausdruck Ausdruck | Let Wertdekl Ausdruck deriving (Eq ,Show) Die Deklaration von Wertdekl benutzt Ausdruck; die Deklaration von Ausdruck benutzt Wertdekl. ©Arnd Poetzsch-Heffter TU Kaiserslautern 304 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Unendliche Datenobjekte: Zu einer nicht-leeren endlichen Liste kann man sich das erste Element und eine Liste als Rest geben lassen. Hat die endliche Liste xl die Länge n > 0, dann hat (tail xl) die Länge n − 1. Eine unendliche Liste besitzt keine natürlichzahlige Länge und wird durch Anwendung von tail nicht kürzer. Haskell unterstützt unendliche Liste und andere unendliche Datenobjekte. ©Arnd Poetzsch-Heffter TU Kaiserslautern 305 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Strom) Potenziell unendliche Listen werden meist als Ströme bezeichnet. In Haskell kann man den Listendatentyp zur Realisierung von Strömen verwenden. Typische Operationen: • Lesen des ersten Elements (head) • Entfernen des ersten Elements (tail) • Prüfen, ob noch Elemente im Strom vorhanden sind Beispiel: Strom von Eingabedaten ©Arnd Poetzsch-Heffter TU Kaiserslautern 306 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (unendliche Liste) Eine vorstellbare unendliche Liste ist die Liste der natürlichen Zahlen [0,1,2,3,4,5,...]. In Haskell lässt sich diese Liste wie folgt definieren: incr :: [ Integer ] -> [ Integer ] incr [] = [] incr (x:xs) = (x+1):(incr xs) natlist = 0:(incr natlist ) natlist2 = [0 ..] ©Arnd Poetzsch-Heffter TU Kaiserslautern 307 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.5 Ein- und Ausgabe ©Arnd Poetzsch-Heffter TU Kaiserslautern 308 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Ein- und Ausgabe Der Haskell Prelude stellt Funktionen zur Verfügung, um • Werte von der Standardeingabe zu lesen, • Werte auf der Standardausgabe auszugeben und • aus Dateien zu lesen und in Dateien zu schreiben. Problem: In einer rein funktionalen Sprache wie Haskell haben Funktionen keine Seiteneffekte. Deshalb spielt auch die Reihenfolge des Aufrufs eine weniger wichtige Rolle. Ein- und Ausgabe erzeugen einen Seiteneffekt auf die Programmumgebung. Sie müssen in der richtigen Reihenfolge ausgeführt werden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 309 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Der Typ IO a Um Funktionen mit Ein- und Ausgabe von Funktionen ohne Ein- und Ausgabe zu trennen und die richtige Reihenfolge zu garantieren, benutzt Haskell den vordefinierten parametrischen Typ IO a Zum Beispiel hat die Funktion putStr zur Ausgabe einer Zeichenreihe die Signatur: putStr :: String -> IO () putStr nimmt eine Zeichenreihe als Argument, schreibt die Zeichenreihe in die Standardausgabe (erzeugt also einen IO-Seiteneffekt) und liefert die Einheit als Ergebnis. ©Arnd Poetzsch-Heffter TU Kaiserslautern 310 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Der Typ IO a (2) Wir nennen Funktionen und Konstanten mit Ergebnistyp IO a IO-Aktionen. Zum Beispiel ist die Konstante/null-stellige Funktion getLine eine IO-Aktion: getLine :: IO String Ausführung der Aktion getLine liest von der Standardeingabe (erzeugt also einen IO-Seiteneffekt) und liefert eine Zeichereihe. ©Arnd Poetzsch-Heffter TU Kaiserslautern 311 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Sequentielle Ausführung von IO-Aktionen Für die sequentielle Ausführung von IO-Aktionen stellt Haskell die do-Notation zur Verfügung: do { Anw1 ; ... AnwN ; IO - Aktion } do Anw1 ... AnwN IO - Aktion Eine Anweisung Anw hat die Form Bezeichner <- IO - Aktion wobei der Bezeichner und Zuweisungpfeil “<-” entfallen können, wenn das Ergebnis der IO-Aktion nicht gebraucht wird. ©Arnd Poetzsch-Heffter TU Kaiserslautern 312 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Anweisungssequenz) main = do { hSetBuffering stdout NoBuffering ; -- import System .IO putStr "Gib eine Zeichenreihe ein: "; s <- getLine ; putStr " Revertierte Zeichereihe : "; putStrLn ( reverse s) } Erläuterung: • Die Anweisung s <- getLine bindet den gelesenen Wert an s. • Da die letzte IO-Aktion den Typ IO () hat, hat auch main den Typ IO (). ©Arnd Poetzsch-Heffter TU Kaiserslautern 313 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Anweisungssequenz) (2) Ausgabe aller Elemente einer String-Liste: printStringList :: [ String ] -> IO () printStringList [] = putStr "" printStringList (s:xs) = do putStr s printStringList xs ©Arnd Poetzsch-Heffter TU Kaiserslautern 314 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Parametrisierte IO-Aktionen Bisher haben wir nur IO-Aktion für Zeichenreihen kennen gelernt. Haskell unterstützt im Prelude und Bibliotheken viele weitere IO-Aktionen. Z. B.: getChar :: IO Char putChar :: Char -> IO () Und für einen lesbaren und anzeigbaren Typ a die parametrischen IO-Aktionen: readLn :: IO a print :: a -> IO () ©Arnd Poetzsch-Heffter TU Kaiserslautern 315 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Dateiein- und -ausgabe Zum Lesen aus und Schreiben in Dateien stellt Haskell u. a. die IO-Aktionen: readFile :: FilePath -> IO String writeFile :: FilePath -> String -> IO () Dabei ist FilePath eine Zeichenreihe, die einen Dateinamen bezeichnet: type FilePath = String ©Arnd Poetzsch-Heffter TU Kaiserslautern 316 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.6 Module ©Arnd Poetzsch-Heffter TU Kaiserslautern 317 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Modulsystem) Ein Programmmodul fasst mehrere Deklarationen zusammen und stellt sie unter einem Namen zur Verfügung. Ein Programmmodul sollte Programmierern eine Modul-Schnittstelle bereitstellen, die unabhängig von der Implementierung dokumentiert und benutzbar ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 318 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Module in Haskell Vereinfacht dargestellt, hat ein Haskell-Modul die Form: module <Modulname > ( <kommagetrennte Liste exportierter Programmelemente > ) where import <Modul1 > ... import <Moduln > <Deklarationen des Moduls > Dabei gilt: • Die Liste der importierten und exportierten Programmelemente kann leer sein. • Fehlt die Exportliste einschließlich der Klammern, werden alle in dem Modul deklarierten Programmelemente exportiert. ©Arnd Poetzsch-Heffter TU Kaiserslautern 319 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Programm, die 2.) Ein Haskell-Programm (vgl. F. 211) besteht aus einer Menge von Modulen, wobei ein Modul • den Namen Main haben muss und • in diesem Modul der Name main deklariert sein muss. Die Programmausführung beginnt mit der Ausführung der Deklaration von main. ©Arnd Poetzsch-Heffter TU Kaiserslautern 320 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Haskell-Module) module Alster where avalue = 7; data Art = Painting | Design | Sculpture ache Design = False ache _ = True ©Arnd Poetzsch-Heffter TU Kaiserslautern 321 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Haskell-Module) (2) module Breg where import Alster data Broterwerb = Designer | Maler | Bildhauer beruf beruf beruf beruf :: Art -> Design Painting Sculpture Broterwerb = Designer = Maler = Bildhauer bflag = (ache Painting ) ©Arnd Poetzsch-Heffter TU Kaiserslautern 322 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Haskell-Module) (3) module Main where import Breg main = print bflag Beachte: Programmelemente aus Alster, z.B. avalue, sind nicht sichtbar in Modul Main. ©Arnd Poetzsch-Heffter TU Kaiserslautern 323 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Modul-Schnittstelle) module Environment (Env , emptyEnv , insertI , insertB , lookUp , delete , IBValue (Intv , Boolv , None) ) where emptyEnv :: Env -- leere Bezeichnerumbegung insertI :: String -> Int -> Env -> Env -- ( insertI bez i e) traegt die Bindung (bez ,i) -- in die Umgebung e ein insertB :: String -> Bool -> Env -> Env -- ( insertB bez b e) traegt die Bindung (bez ,b) -- in die Umgebung e ein ©Arnd Poetzsch-Heffter TU Kaiserslautern 324 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Modul-Schnittstelle) (2) lookUp :: String -> Env -> IBValue -- ( lookUp bez e) liefert den Wert v der ersten -- gefundenen Bindung (bez ,v) mit Bezeichner bez delete :: String -> Env -> Env -- ( delete bez e) loescht alle Bindungen (bez ,_) -- mit Bezeichner bez ©Arnd Poetzsch-Heffter TU Kaiserslautern 325 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Modul-Implementierung) -- Modulimplementierung ( Fortsetzung von Environment ) data IBValue = Intv Int | Boolv Bool | None deriving (Eq , Show) type Env = [ (String , IBValue ) ] emptyEnv = [] insertI bez i e = (bez ,Intv i):e insertB bez b e = (bez , Boolv b):e ©Arnd Poetzsch-Heffter TU Kaiserslautern 326 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Modul-Implementierung) (2) lookUp bez [] = None lookUp bez ((bz ,val):e) | bez == bz = val | otherwise = lookUp bez e delete bez [] = [] delete bez ((bz ,val):e) | bez == bz = delete bez e | otherwise = (bz ,val):( delete bez e) ©Arnd Poetzsch-Heffter TU Kaiserslautern 327 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.7 Zusammenfassung von 3.1 ©Arnd Poetzsch-Heffter TU Kaiserslautern 328 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Zusammenfassung von 3.1 Begriffe und Sprachmittel wie Ausdruck, Bezeichner, Vereinbarung, Wert, Typ, Muster, Modul, . . . . Wichtige Programmier- und Modellierungskonzepte: • Basisdatenstrukturen • rekursive Funktionen • rekursive Datentypen (insbesondere Listen) • Ein- und Ausgabe ©Arnd Poetzsch-Heffter TU Kaiserslautern 329 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Abschnitt 3.2 Algorithmen auf Listen und Bäumen ©Arnd Poetzsch-Heffter TU Kaiserslautern 330 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Algorithmen auf Listen und Bäumen Sortieren und Suchen sind elementare Aufgaben, die in den meisten Programmen anfallen. Verfahren zum Suchen und Sortieren spielen eine zentrale Rolle in der Algorithmik. ©Arnd Poetzsch-Heffter TU Kaiserslautern 331 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Lernziele: • Intuitiver Algorithmusbegriff • Kenntnis wichtiger/klassischer Algorithmen und Algorithmenklassen • Zusammenhang Algorithmus und Datenstruktur • Wege vom Problem zum Algorithmus • Implementierungstechniken für Datenstrukturen und Algorithmen (vom Algorithmus zum Programm) Bemerkung: Wir führen in den Bereich Algorithmen und Datenstrukturen ausgehend vom Problem ein. Andere Möglichkeit wäre gemäß der benutzten Datenstrukturen (Listen, Bäume, etc.). ©Arnd Poetzsch-Heffter TU Kaiserslautern 332 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Unterabschnitt 3.2.1 Sortieren ©Arnd Poetzsch-Heffter TU Kaiserslautern 333 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Sortieren Sortieren ist eine Standardaufgabe, die Teil vieler speziellerer, umfassenderer Aufgaben ist. Untersuchungen zeigen, dass „mehr als ein Viertel der kommerziell verbrauchten Rechenzeit auf Sortiervorgänge entfällt“ (Ottmann, Widmayer: Algorithmen und Datenstrukturen, Kap. 2). ©Arnd Poetzsch-Heffter TU Kaiserslautern 334 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Begriffsklärung: (Sortierproblem) Gegeben ist eine Folge s1 , . . . , sN von sogenannten Datensätzen. Jeder Satz sj hat einen Schlüssel kj . Wir gehen davon aus, dass die Schlüssel ganzzahlig sind. Aufgabe des Sortierproblems ist es, eine Permutation π zu finden, so dass die Umordnung der Sätze gemäß π folgende Reihenfolge auf den Schlüsseln ergibt: kπ(1) ≤ kπ(2) ≤ · · · ≤ kπ(N) ©Arnd Poetzsch-Heffter TU Kaiserslautern 335 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: Offene Aspekte der Formulierung des Sortierproblem: • Was heißt, eine Folge ist „gegeben“? • Ist der Bereich der Schlüssel bekannt? • Welche Operationen stehen zur Verfügung, um π zu bestimmen? • Was genau heißt „Umordnung“? ©Arnd Poetzsch-Heffter TU Kaiserslautern 336 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Aufgabenstellung: Wir benutzen Datensätze folgenden Typs type Dataset = (Int , String ) mit Vergleichsfunktion: leq:: Dataset -> Dataset -> Bool leq (kx , dx) (ky , dy) = (kx <=ky) ©Arnd Poetzsch-Heffter TU Kaiserslautern 337 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Aufgabenstellung: (2) Entwickle eine Funktion sort :: [ Dataset ] -> [ Dataset ] so dass das Ergebnis von sort xl für alle Eingaben xl aufsteigend sortiert ist und die gleichen Elemente enthält wie xl (mehrere Einträge mit gleichem Schlüssel sind nicht ausgeschlossen). ©Arnd Poetzsch-Heffter TU Kaiserslautern 338 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Aufgabenstellung: (3) Wir betrachten: • Sortieren durch Auswahl (engl. selection sort) • Sortieren durch Einfügen (engl. insertion sort) • Bubblesort • Sortieren durch rekursives Teilen (quick sort) • Sortieren durch Mischen (merge sort) • Heapsort ©Arnd Poetzsch-Heffter TU Kaiserslautern 339 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Sortieren durch Auswahl (selection sort) Algorithmische Idee: • Entferne einen minimalen Eintrag min aus der Liste. • Sortiere die Liste, aus der min entfernt wurde. • Füge min als ersten Element an die sortierte Liste an. ©Arnd Poetzsch-Heffter TU Kaiserslautern 340 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Sortieren durch Auswahl (selection sort) (2) select :: Dataset -> [ Dataset ] -> Dataset -- Hilfsfunktion : liefert einen minimalen Eintrag der -- Liste bzw. x, falls x minimal select x [] = x select x (y:yl) = if x `leq` y then select x yl else select y yl ©Arnd Poetzsch-Heffter TU Kaiserslautern 341 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Sortieren durch Auswahl (selection sort) (3) delete :: Dataset -> [ Dataset ] -> [ Dataset ] -- Hilfsfunktion : loescht ein Vorkommen von x aus der -- Liste , falls solches vorhanden delete x [] = [] delete x (y:yl) = if (x==y) then else ©Arnd Poetzsch-Heffter TU Kaiserslautern yl y:( delete x yl) 342 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Sortieren durch Auswahl (selection sort) (4) selectionsort :: [ Dataset ] -> [ Dataset ] -- Sortieren durch Auswahl -mnm: ein minimaler Eintrag in Liste xl -rest: die Liste xl ohne min selectionsort [] = [] selectionsort (x:xl) = let mnm = select x xl rest = delete mnm (x:xl) in mnm : ( selectionsort rest) ©Arnd Poetzsch-Heffter TU Kaiserslautern 343 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Sortieren durch Einfügen (insertion sort) Algorithmische Idee: • Sortiere zunächst den Rest der Liste. • Füge dann den ersten Eintrag in die sortierte Liste ein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 344 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Sortieren durch Einfügen (insertion sort) (2) insert :: Dataset -> [ Dataset ] -> [ Dataset ] -- Hilfsfunktion : fuegt Argument in sortierte Liste ein -- Ergebnis : sortierte Liste insert x [] = insert x (y:yl) = ©Arnd Poetzsch-Heffter [x] if (x `leq` y) then x : (y:yl) else y : ( insert x yl) TU Kaiserslautern 345 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Sortieren durch Einfügen (insertion sort) (3) insertionsort :: [ Dataset ] -> [ Dataset ] -- Sortieren durch Einfuegen insertionsort [] = [] insertionsort (x:xl) = insert x ( insertionsort xl) ©Arnd Poetzsch-Heffter TU Kaiserslautern 346 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bubblesort Algorithmische Idee: • Schiebe einen Eintrag nach rechts heraus: I Beginne dazu mit dem ersten Eintrag x. I Wenn Schieben von x auf einen gleichen oder größeren Eintrag y stößt, schiebe y weiter. I Ergebnis: maximaler Eintrag mxe und Liste ohne mxe • Sortiere die Liste ohne mxe und hänge mxe an. ©Arnd Poetzsch-Heffter TU Kaiserslautern 347 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bubblesort (2) bubble :: [ Dataset ] -> Dataset -> [ Dataset ] -> ([ Dataset ], Dataset ) -- Hilfsfunktion : liefert einen maximalen Eintrag der -- Liste und die Liste ohne den maximalen Eintrag bubble rl x [] = (rl ,x) bubble rl x (y:yl) = if (x `leq` y) then bubble (rl ++[x]) y yl else bubble (rl ++[y]) x yl ©Arnd Poetzsch-Heffter TU Kaiserslautern 348 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bubblesort (3) bubblesort :: [ Dataset ] -> [ Dataset ] -- Sortieren durch Herausschieben der maximalen -Elemente bubblesort [] = [] bubblesort (x:xl) = let (rl ,mxe) = bubble [] x xl in ( bubblesort rl)++[ mxe] ©Arnd Poetzsch-Heffter TU Kaiserslautern 349 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Quicksort: Sortieren durch Teilen Algorithmische Idee: • Wähle einen beliebigen Datensatz mit Schlüssel k aus, das sogenannte Pivotelement. • Teile die Liste in zwei Teile: I 1. Teil enthält alle Datensätze mit Schlüsseln < k I 2. Teil enthält die Datensätze mit Schlüsseln ≥ k • Wende quicksort rekursiv auf die Teillisten an. • Hänge die resultierenden Listen und das Pivotelement zusammen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 350 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Quicksort: Sortieren durch Teilen (2) split :: Dataset -> [ Dataset ] -> ([ Dataset ],[ Dataset ]) -- Hilfsfkt .: teilt Liste in zwei Listen (below , above ) -- below : alle Elemente in kleiner p -- above : alle Elemente groesser gleich p split p split p let in [] = ([] ,[]) (x:xr) = (blw ,abv) = split p xr if p `leq` x then (blw ,x:abv) else (x:blw ,abv) ©Arnd Poetzsch-Heffter TU Kaiserslautern 351 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Quicksort: Sortieren durch Teilen (3) qsort :: [ Dataset ] -> [ Dataset ] -- Sortieren nach der Strategie ``Teile und Herrsche ’’ qsort [] = [] qsort (p:rest) = let (below , above ) = split p rest in ( qsort below ) ++ [p] ++ ( qsort above ) ©Arnd Poetzsch-Heffter TU Kaiserslautern 352 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: Quicksort ist ein typischer Algorithmus gemäß der Divide-and-Conquer-Strategie: • Zerlege das Problem in Teilprobleme. • Wende den Algorithmus auf die Teilprobleme an. • Füge die Ergebnisse zusammen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 353 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Sortieren durch Mischen: Algorithmische Idee: • Hat die Liste mehr als ein Element, berechne die Länge der Liste div 2 (halfsize). • Teile die Liste in zwei Teile der Länge halfsize (+1). • Sortiere die Teile. • Mische die Teile zusammen. Bemerkung: Mergesort ist auch effizient für das Sortieren von Datensätzen, die auf externen Speichermedien liegen und nicht vollständig in den Hauptspeicher geladen werden können. ©Arnd Poetzsch-Heffter TU Kaiserslautern 354 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: merge :: [ Dataset ] -> [ Dataset ] -> [ Dataset ] -- Hilfsfunktion : mischt zwei sortierte Listen -- zu einer sortierten Liste zusammen merge merge merge merge [] [] [] yl xl [] (x:xl) (y:yl) ©Arnd Poetzsch-Heffter = = = = [] yl xl if (x `leq` y) then x : ( merge xl (y:yl)) else y : ( merge (x:xl) yl) TU Kaiserslautern 355 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: (2) mergesort :: [ Dataset ] -> [ Dataset ] -- Sortieren durch Mischen -halfsize : Haelfte der Listenlaenge -front : Vordere Haelfte der Liste -back : Hintere Haelfte der Liste mergesort [] = [] mergesort (x:[]) = [x] mergesort xl = let halfsize = ( length xl) `div` 2 front = take halfsize xl back = drop halfsize xl in merge ( mergesort front ) ( mergesort back) ©Arnd Poetzsch-Heffter TU Kaiserslautern 356 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Heapsort Heapsort verfeinert die Idee des Sortierens durch Auswahl: • Minimum bzw. Maximum wird nicht durch lineare Suche gefunden, • sondern mit logarithmischem Aufwand durch Verwendung einer besonderen Datenstruktur, dem sogenannten Heap. Algorithmische Idee: • 1. Schritt: Erstelle den Heap zur Eingabeliste. • 2. Schritt: I Entferne Maximumelement aus Heap (konstanter Aufwand) und hänge es an die Ausgabeliste. I Stelle Heap-Bedingung wieder her (logarithmischer Aufwand). I Fahre mit Schritt 2 fort bis der Heap leer. ©Arnd Poetzsch-Heffter TU Kaiserslautern 357 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: • Ziele des Vorgehens: I Beispiel für komplexe, abstrakte Datenstruktur I Zusammenhang der algorithmischen Idee und der Datenstruktur. • Der Begriff „Heap“ist in der Informatik überladen. Auch der Speicher für zur Laufzeit angelegte Variablen wird im Englischen „heap“genannt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 358 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Begriffsklärung: (zu Bäumen) Wir betrachten im Folgenden Binärbäume, die • entweder leer sind oder • aus einem markierten Knoten mit zwei Unterbäumen bestehen. Ein Blatt ist dann ein Knoten mit zwei leeren Unterbäumen. Ein Binärbaum heißt strikt, wenn jeder Knoten ein Blatt ist oder zwei nicht-leere Unterbäume besitzt. Ein Binärbaum der Höhe h heißt vollständig, wenn er strikt ist und alle Blätter die Tiefe h − 1 haben. ©Arnd Poetzsch-Heffter TU Kaiserslautern 359 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Begriffsklärung: (zu Bäumen) (2) Ein Binärbaum der Höhe h heißt fast vollständig, wenn • jedes Blatt die Tiefe h − 1 oder h − 2 hat, • jeder Knoten mit einer Tiefe kleiner h − 2 zwei nicht-leere Unterbäume hat, • für die Knoten K des Niveaus h − 2 gilt: 1. Hat K 2 nicht-leere Unterbäume, dann auch alle linken Nachbarn von K . 2. Ist K ein Blatt, dann sind auch alle rechten Nachbarn von K Blätter. 3. Es gibt maximal ein K mit genau einem nicht-leeren Unterbaum und der ist links. Ein Baum der Größe n heißt indiziert, wenn man seine Knoten mittels der Indizes 0, . . . , n − 1 ansprechen kann. ©Arnd Poetzsch-Heffter TU Kaiserslautern 360 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: Im Folgenden gehen wir bei indizierten Bäumen immer davon aus, dass die Reihenfolge der Indices einem Breitendurchlauf folgt (siehe Beispiel). Beispiel: (Fast vollst., indizierter und markierter Binärbaum) ©Arnd Poetzsch-Heffter TU Kaiserslautern 361 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Modulschnittstelle für fast vollständige, markierte, indizierte Binärbäume: module FvBintree (FVBintree ,create ,size ,get ,swap , removeLast ,hasLeft ,hasRight ,left , right ) where create :: [ Dataset ] -> FVBintree -- Erzeugt fast vollstaendigen Binaerbaum , wobei -- die Listenelemente zu Markierungen werden size :: FVBintree -> Int -- Anzahl der Knoten des Baums get :: FVBintree -> Int -> Dataset -- Liefert Markierung am Knoten mit Index i, -- 0 <= i < size b ©Arnd Poetzsch-Heffter TU Kaiserslautern 362 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Modulschnittstelle für fast vollständige, markierte, indizierte Binärbäume: (2) swap :: FVBintree -> Int -> Int -> FVBintree -- Vertauscht Markierungen der Knoten mit Index i und j -- Modifiziert fbt; 0 <= i,j < size b removeLast :: FVBintree -> FVBintree -- Entfernt letzten Knoten hasLeft :: FVBintree -> Int -> Bool -- Knoten mit Index i hat linkes Kind -- 0 <= i < size b hasRight :: FVBintree -> Int -> Bool -- Knoten mit Index i hat rechtes Kind -- 0 <= i < size b ©Arnd Poetzsch-Heffter TU Kaiserslautern 363 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Modulschnittstelle für fast vollständige, markierte, indizierte Binärbäume: (3) left :: FVBintree -> Int -> Int -- Liefert Index des linken Kinds von Knoten mit Index i -- 0 <= i < size b && hasLeft i right :: FVBintree -> Int -> Int -- Liefert Index des rechten Kinds von Knoten mit Index i -- 0 <= i < size b && hasRight i ©Arnd Poetzsch-Heffter TU Kaiserslautern 364 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Vorgehen • Wir gehen im Folgenden davon aus, dass wir eine Implementierung von FvBintree haben (steht zum Testen bereit) und realisieren heapsort damit; d.h. ohne die Struktur/Implementierung zu kennen. • Im Zusammenhang mit der objektorientierten Programmierung werden wir dann eine sehr effiziente Implementierung für fast vollständige Binärbäume kennen lernen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 365 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: • Wir benutzen die Datenstruktur ohne die Implementierung zu kennen; man sagt die Datenstruktur ist für den Nutzer abstrakt und spricht von abstrakter Datenstruktur. • Abstrakte Datenstrukturen werden über ihre Schnittstelle benutzt. Entwicklung und Benutzung von solchen Schnittstellen ist ein zentraler Bestandteil der SW-Entwicklung. • Eine abstrakte Datenstruktur kann unterschiedliche Implementierungen haben. Implementierungen können ausgetauscht werden, ohne dass der Nutzer seine Programme ändern muss! ©Arnd Poetzsch-Heffter TU Kaiserslautern 366 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Begriffsklärung: (Heap) Ein markierter, fast vollständiger, indizierter Binärbaum mit n Knoten heißt ein Heap der Größe n, wenn die folgende Heap-Eigenschaft erfüllt ist: Ist M ein Knoten und N ein Kind von M mit Markierungen kM und kN , dann gilt: kM ≥ kN Bei einem Heap sind die Knoten entsprechend einem Breitendurchlauf indiziert (siehe Beispiel unten). ©Arnd Poetzsch-Heffter TU Kaiserslautern 367 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Beispiel: (Heap) Heap der Größe 8: ©Arnd Poetzsch-Heffter TU Kaiserslautern 368 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: Die Heap-Eigenschaft garantiert, dass der Schlüssel eines Knotens M größer gleich aller Schlüssel in den Unterbäumen von M ist. Insbesondere steht in der Wurzel ein Element mit einem maximalen Schlüssel. ©Arnd Poetzsch-Heffter TU Kaiserslautern 369 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Herstellen der Heap-Eigenschaft: Sei ein markierter, fast vollständiger Binärbaum gegeben, der die Heap-Eigenschaft nur an der Wurzel verletzt. Die Heap-Eigenschaft kann hergestellt werden, indem man den Wurzelknoten M rekursiv in dem Unterbaum mit dem größeren Schlüssel versickern lässt: • Gibt es kein Kind, ist nichts zu tun. • Gibt es genau ein Kind N, dann ist dies links und kinderlos: Ist kM < kN , vertausche die Markierungen. • Gibt es zwei Kinder und ist N das Kind mit dem größeren Schlüssel: Ist kM < kN , vertausche die Markierungen und fahre rekursiv mit dem Unterbaum zu N fort. ©Arnd Poetzsch-Heffter TU Kaiserslautern 370 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Beispiel: (Versickern lassen) ©Arnd Poetzsch-Heffter TU Kaiserslautern 371 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Beispiel: (Versickern lassen) (2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 372 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Beispiel: (Versickern lassen) (3) ©Arnd Poetzsch-Heffter TU Kaiserslautern 373 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Funktion heapify formuliert auf Basis von FvBintree: heapify :: FVBintree -> Int -> FVBintree -- Stelle Heap - Eigenschaft im Knoten ix her -- Annahme : die Kinder von ix erfuellen die -- Heap - Eigenschaft heapify b ix = let ds = get b ix in if hasLeft b ix && not ( hasRight b ix) then let lx = left b ix in if get b lx `leq` ds then b else swap b ix lx -- else ... ©Arnd Poetzsch-Heffter TU Kaiserslautern 374 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Funktion heapify formuliert auf Basis von FvBintree: (2) else -- hat linken und -- oder ist Blatt if hasRight b ix then let lx = rx = largerKid = rechten Unterbaum left b ix right b ix if get b lx `leq` get b rx then rx else lx in if get b largerKid `leq` ds then b else heapify (swap b ix largerKid ) largerKid else b ©Arnd Poetzsch-Heffter TU Kaiserslautern 375 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Konkretisierung des Heapsort-Algorithmus: 1. Schritt: • Erzeuge Binärbaum-Repräsentation aus Eingabefolge. • Stelle Heap-Eigenschaft her, indem heapify ausgehend von den Blättern für jeden Knoten aufgerufen wird. Es reicht, nur Knoten mit Kindern zu berücksichtigen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 376 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Konkretisierung des Heapsort-Algorithmus: (2) 2. Schritt: • Schreibe den Wurzel-Datensatz in die Ausgabe. • Schreibe den Datensatz des letzten Elementes in den Wurzelknoten (swap) • Entferne das letzte Element. • Stelle die Heap-Eigenschaft wieder her. • Fahre mit Schritt 2 fort, solange die Größe > 0. Lemma: In einem fast vollständigen Binärbaum der Größe n sind die Knoten mit den Indizes (n div 2) bis n − 1 Blätter. ©Arnd Poetzsch-Heffter TU Kaiserslautern 377 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Heapsort: Abstrakte Version Wir betrachten zunächst Heapsort auf Basis des abstrakten Datentyps für markierte, fast vollständige, indizierte Binärbaume: heapifyAll :: FVBintree -> FVBintree hpfyEmb :: FVBintree -> Int -> FVBintree -- Hilfsfunktionen fuer den ersten Schritt heapifyAll b = hpfyEmb b (( size b) `div` 2) hpfyEmb b 0 then b else heapify b 0 hpfyEmb b ix = hpfyEmb ( heapify b ix) (ix -1) ©Arnd Poetzsch-Heffter = if size b == 0 TU Kaiserslautern 378 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Heapsort: Abstrakte Version (2) -- heapsort : sortiert gegebene Liste heapsort :: [ Dataset ] -> [ Dataset ] heapsort xl = reverse ( sortheap ( heapifyAll ( create xl))) sortheap :: FVBintree -> [ Dataset ] sortheap hp = if size hp == 0 then [] else let maxds = get hp 0 hp1 = swap hp 0 (size hp - 1) hp2 = removeLast hp1 hp3 = heapify hp2 0 in maxds : ( sortheap hp3) ©Arnd Poetzsch-Heffter TU Kaiserslautern 379 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkungen: • Wie wir in Kapitel 4 zeigen, profitiert Heapsort davon, dass sich fast vollständige, markierte, indizierte Binärbäume sehr effizient mit Feldern realisieren lassen. • Zu einem algorithmischen Problem (hier Sortieren) gibt es im Allg. viele Lösungen. • Algorithmische Lösungen unterscheiden sich in: I der Laufzeiteffizienz (messbar) I der Speichereffizienz (messbar) I der „Komplexität“ der Verfahrensidee (im Allg. nicht messbar). ©Arnd Poetzsch-Heffter TU Kaiserslautern 380 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkungen: (2) In den folgenden Kapiteln werden wir demonstrieren, • wie einige der obigen Algorithmen in anderen Programmierparadigmen formuliert werden können; • wie der Effizienz/Komplexitätsbegriff präzisiert werden kann. ©Arnd Poetzsch-Heffter TU Kaiserslautern 381 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Unterabschnitt 3.2.2 Suchen ©Arnd Poetzsch-Heffter TU Kaiserslautern 382 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Suchen Die Verwaltung von Datensätzen basiert auf drei grundlegenden Operationen: • Einfügen eines Datensatzes in eine Menge von Datensätzen; • Suchen eines Datensatzes mit Schlüssel k ; • Löschen eines Datensatzes mit Schlüssel k . Bemerkung: Weitere oft gewünschte Operationen sind: Sortierte Ausgabe, Suchen aller Datensätze mit bestimmten Eigenschaften, Bearbeiten von Daten ohne eindeutige Schlüssel, etc. ©Arnd Poetzsch-Heffter TU Kaiserslautern 383 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Schnittstell zum Suchen Wir betrachten die folgende Schnittstelle: module Dictionary (Dict ,emptyDict ,get ,put , remove ) where type Dict = STree -- type des Dictionarys emptyDict :: Dict -- leeres Dictionary get :: Dict -> Int -> (Bool , String ) -- Nachschauen des Eintrags zu Schluessel i put :: Dict -> Int -> String -> Dict -- Einfuegen des Eintrags (i,s), -- Ueberschreibt ggf. alten Eintrag zu i remove :: Dict -> Int -> Dict -- Loeschen des Eintrags zu Schluessel i ©Arnd Poetzsch-Heffter TU Kaiserslautern 384 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: In der Literatur zur funktionalen Programmierung wir „get“ oft „lookup“ oder „search“, „put“ oft „insert“ und „remove“ oft „delete“genannt. Um den Zusammenhang zu OO-Schnittstellen augenfälliger zu machen, benutzen wir die dort üblichen Namen. Ziel ist es, Datenstrukturen zu finden, bei denen der Aufwand für obige Operationen gering ist. Wir betrachten hier die folgenden Dictionary-Realisierungen: • lineare Datenstrukturen (Übung) • (natürliche) binäre Suchbäume (Vorlesung) ©Arnd Poetzsch-Heffter TU Kaiserslautern 385 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Begriffsklärung: (Binärer Suchbaum) Ein markierter Binärbaum B ist ein natürlicher binärer Suchbaum (kurz: binärer Suchbaum), wenn die Suchbaum-Eigenschaft gilt, d.h. wenn für jeden Knoten K in B gilt: • Alle Schlüssel im linken Unterbaum von K sind echt kleiner als der Schlüssel von K . • Alle Schlüssel im rechten Unterbaum von K sind echt größer als der Schlüssel von K . ©Arnd Poetzsch-Heffter TU Kaiserslautern 386 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkung: • „Natürlich“ bezieht sich auf das Entstehen der Bäume in Abhängigkeit von der Reihenfolge der Einfüge-Operationen (Abgrenzung zu balancierten Bäumen). • In einem binären Suchbaum gibt es zu einem Schlüssel maximal einen Knoten mit entsprechender Markierung. ©Arnd Poetzsch-Heffter TU Kaiserslautern 387 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Datenstruktur für Suchbäume: Wir stellen Dictionaries als Binärbäume mit Markierungen vom Typ Dataset dar: data STree = Node Dataset STree STree | Empty deriving (Eq , Show) emptyDict = Empty Die Konstante emptyDict repräsentiert das leere Dictionary. ©Arnd Poetzsch-Heffter TU Kaiserslautern 388 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Invariante für Suchbäume: Binärbäume, die als Dictionary verwendet werden, müssen die Suchbaum-Eigenschaft erfüllen. Alle Funktionen, die Dictionaries als Parameter bekommen, gehen davon aus, dass die Suchbaum-Eigenschaft für die Parameter gilt. Die Funktionen müssen garantieren, dass die Eigenschaft auch für Ergebnisse gilt. Man sagt: Die Suchbaum-Eigenschaft ist eine Datenstrukturinvariante von Dictionaries. Wir guarantieren die Datenstrukturinvariante u.a. dadurch, dass wir Nutzern des Moduls Dictionary keinen Zugriff auf die Konstruktoren geben. ©Arnd Poetzsch-Heffter TU Kaiserslautern 389 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Suchen eines Eintrags: Wenn kein Eintrag zum Schlüssel existiert, liefere (False,""); sonst liefere (True,s), wobei s der String zum Schlüssel ist: get get | | | Empty k = (False ,"") (Node (km ,s) l r) k k < km = get l k km < k = get r k otherwise = (True ,s) ©Arnd Poetzsch-Heffter TU Kaiserslautern 390 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Einfügen eines Eintrags: Algorithmisches Vorgehen: • Neue Knoten werden immer als Blätter eingefügt. • Die Position des Blattes wird durch den Schlüssel des neuen Eintrags festgelegt. • Beim Aufbau eines Baumes ergibt der erste Eintrag die Wurzel. • Ein Knoten wird I in den linken Unterbaum der Wurzel eingefügt, wenn sein Schlüssel kleiner ist als der Schlüssel der Wurzel; I in den rechten, wenn er größer ist. Dieses Verfahren wird rekursiv fortgesetzt, bis die Einfügeposition bestimmt ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 391 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Beispiel: Einfügen von 33: ©Arnd Poetzsch-Heffter TU Kaiserslautern 392 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Implementierung von put: Die algorithmische Idee lässt sich direkt umsetzen. Beachte aber, dass das Dictionary nicht verändert wird, sondern ein neues erzeugt und abgeliefert wird: put Empty k s = Node (k,s) put (Node (km ,sm) l r) k s | k == km = Node (k,s) l | k < km = Node (km ,sm) | otherwise = Node (km ,sm) ©Arnd Poetzsch-Heffter Empty Empty r (put l k s) r l (put r k s) TU Kaiserslautern 393 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkungen: • Die Reihenfolge des Einfügens bestimmt das Aussehen des binären Suchbaums: Reihenfolgen: 2;3;1 ©Arnd Poetzsch-Heffter 1;3;2 TU Kaiserslautern 1;2;3 394 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Bemerkungen: (2) • Es gibt sehr viele Möglichkeiten, aus einer vorgegebenen Schlüsselmenge einen binären Suchbaum zu erzeugen. • Bei sortierter Einfügereihenfolge entartet der binäre Suchbaum zur linearen Liste. • Der Algorithmus zum Einfügen ist schnell, insbesondere weil keine Ausgleichs- oder Reorganisationsoperationen vorgenommen werden müssen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 395 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Löschen: Löschen ist die schwierigste Operation, da • ggf. innere Knoten entfernt werden und dabei • die Suchbaum-Eigenschaft erhalten werden muss. Algorithmisches Vorgehen: • Die Position eines zu löschenden Knotens K mit Schlüssel X wird nach dem gleichen Verfahren wie beim Suchen eines Knotens bestimmt. • Dann sind drei Fälle zu unterscheiden: ©Arnd Poetzsch-Heffter TU Kaiserslautern 396 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Löschen: (2) 1. Fall: K ist ein Blatt. Lösche K : Entsprechend, wenn Knoten mit Schlüssel X in rechtem Unterbaum. ©Arnd Poetzsch-Heffter TU Kaiserslautern 397 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Löschen: (3) 2. Fall: K mit Schlüssel X hat genau einen Unterbaum. K wird im Eltern-Knoten durch sein Kind ersetzt und gelöscht: Die anderen links-rechts-Varianten entsprechend. ©Arnd Poetzsch-Heffter TU Kaiserslautern 398 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Löschen: (4) 3. Fall: K mit Schlüssel X hat genau zwei Unterbäume. Problem: Wo werden die beiden Unterbäume nach dem Löschen von K eingehängt? Hier gibt es 2 symmetrische Lösungsvarianten: • Ermittle den Knoten KR mit dem kleinsten Schlüssel im rechten Unterbaum, Schlüssel von KR sei XR. • Speichere XR und die Daten von KR in K . • Lösche KR gemäß Vorgehen zu Fall 1 bzw. 2, möglich da für KR einer der Fälle zutrifft. (andere Variante: größten Schlüssel im linken UB) ©Arnd Poetzsch-Heffter TU Kaiserslautern 399 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Löschen: (5) ©Arnd Poetzsch-Heffter TU Kaiserslautern 400 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Umsetzung in Haskell • Die Fälle 1 und 2 lassen sich direkt behandeln. • Für Fall 3 realisiere Hilfsfunktion removemin, die I nichtleeren binären Suchbaum b als Parameter nimmt; I ein Paar (mnm, br ) als Ergebnis liefert, wobei I I ©Arnd Poetzsch-Heffter mnm der kleinste Datensatz in b ist und br der Baum ist, der sich durch Löschen von mnm aus b ergibt. TU Kaiserslautern 401 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Umsetzung in Haskell (2) removemin :: STree -> (Dataset , STree ) -- Parameter : nichtleerer binaerer Suchbaum b. -- Liefert Eintrag d mit kleinstem Schluessel in b -- und Baum nach Loeschen von d in b removemin (Node d Empty r) = (d,r) removemin (Node d l r) = let (mnm ,ll) = removemin l in (mnm , Node d ll r) ©Arnd Poetzsch-Heffter TU Kaiserslautern 402 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Umsetzung in Haskell (3) remove Empty k = Empty remove (Node (km ,s) l r) k | k < km = Node (km ,s) ( remove l k) r | km < k = Node (km ,s) l ( remove r k) | l == Empty = r | r == Empty = l | otherwise = -- k == km && l /= Empty /= r let (mnm ,rr) = removemin r in Node mnm l rr ©Arnd Poetzsch-Heffter TU Kaiserslautern 403 3. Funktionales Programmieren 3.2 Algorithmen auf Listen und Bäumen Diskussion: Der Aufwand für die Grundoperationen Einfügen, Suchen und Löschen eines Knotens ist proportional zur Tiefe des Knotens, bei dem die Operation aus- geführt wird. Ist h die Höhe des Suchbaumes, ist der Aufwand der Grundoperationen im ungünstigsten Fall also O(h), wobei log(N + 1) ≤ h ≤ N für Knotenanzahl N. Folgerung: Bei degenerierten natürlichen Suchbäumen kann linearer Aufwand für alle Grundoperationen entstehen. Im Mittel verhalten sich Suchbäume aber wesentlich besser. Zusätzlich versucht man durch gezielte Reorganisation eine gute Balancierung zu erreichen (siehe Kapitel 5). ©Arnd Poetzsch-Heffter TU Kaiserslautern 404 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Abschnitt 3.3 Polymorphie und Funktionen höherer Ordnung ©Arnd Poetzsch-Heffter TU Kaiserslautern 405 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Abstraktion mittels Polymorphie und Funktionen höherer Ordnung Überblick: • Grundbegriffe der Typisierung • Polymorphie als Abstraktionsmittel • Typsysteme und Typinferenz • Einführung in Funktionen höherer Ordnung • Wichtige Funktionen höherer Ordnung ©Arnd Poetzsch-Heffter TU Kaiserslautern 406 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Unterabschnitt 3.3.1 Typisierung ©Arnd Poetzsch-Heffter TU Kaiserslautern 407 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Typisierung Inhalte: • Was ist ein Typ? • Ziele der Typisierung • Polymorphie und parametrische Typen • Typsystem von Haskell und Typinferenz Fast alle modernen Spezifikations- und Programmiersprachen besitzen ein Typsystem. ©Arnd Poetzsch-Heffter TU Kaiserslautern 408 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Was ist ein Typ? Ein Typ beschreibt Eigenschaften von Elementen der Modellierung oder Programmierung: • Elementare Typen stehen häufig für die Eigenschaft, zu einer bestimmten Wertemenge zu gehören (Bool, Int, Char, String, ... ). • Zusammengesetzte Typen beschreiben die genaue Struktur ihrer Elemente ( Tupel-, Listen- und Funktionstypen). ©Arnd Poetzsch-Heffter TU Kaiserslautern 409 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Was ist ein Typ? (2) • Parametrische Typen beschreiben bestimmte Eigenschaften und lassen andere offen; z.B.: I Elemente vom Typ [a] sind homogene Listen; man kann also null, head, tail anwenden. Offen bleibt z.B. der Ergebnistyp von head. I Elemente vom Typ (a,[a]) sind Paare, so dass die Funktionen für Paare angewendet werden können. Außerdem besitzen sie die Eigenschaft, dass die zweite Komponente immer eine Liste ist, deren Elemente vom selben Typ sind wie die erste Komponente: somefun :: ( a, [a] ) -> [a] somefun p = let (fstc ,sndc) = p in fstc : sndc ©Arnd Poetzsch-Heffter TU Kaiserslautern 410 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Ziele der Typisierung Die Typisierung verfolgt drei Ziele: • Automatische Erkennung von Programmierfehlern (durch Übersetzer, Interpreter); • Verbessern der Lesbarkeit von Programmen; • Ermöglichen effizienterer Implementierungen. Wir konzentrieren uns hier auf das erste Ziel. ©Arnd Poetzsch-Heffter TU Kaiserslautern 411 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Zentrale Idee: • Für jeden Ausdruck und jede Funktion wird ein Typ festgelegt. • Prüfe, ob die Typen der aktuellen Parameterausdrücke mit der Signatur der angewendeten Funktion übereinstimmen. Beispiele: (Typprüfung von Ausdrücken) f :: Int -> Int dann sind: f 7, f (head [1,2,3]), f (f 78)+ 9 typkorrekt; f True, [f,5.6], f head nicht typkorrekt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 412 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Bemerkung: Typisierung war lange Zeit nicht unumstritten. Hauptgegenargumente sind: • zusätzlicher Schreib- und Entwurfsaufwand • Einschränkung der Freiheit: I inhomogene Listen I Nutzen der Repräsentation von Daten im Rechner ©Arnd Poetzsch-Heffter TU Kaiserslautern 413 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiel: Es gibt viele Programme, die nicht typkorrekt sind, sich aber trotzdem zur Laufzeit gutartig verhalten; z.B: Aufgabe 1: Schreibe eine Funktion frp: • Eingabe: Liste von Paaren entweder vom Typ (Bool,Int) oder (Bool,Float) • Zulässige Listen: Wenn 1. Komponente True, dann 2. Komponente vom Typ Int, sonst vom Typ Float • Summiere die Listenelemente und liefere ein Paar mit beschriebener Eigenschaft. ©Arnd Poetzsch-Heffter TU Kaiserslautern 414 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiel: (2) Realisierung in Haskell-Notation (kein Haskell-Programm, da nicht typkorrekt!): frp :: ( Bool , ? ) -> ? frp [ ] = (True , 0) frp ((True ,n):xs) = case frp xs of (True ,k) -> (True , k+n ) (False ,q) -> (False , q+( fromInteger n)) frp (( False ,r):xs) = case frp xs of (True ,k) -> (False , ( fromInteger k)+r ) (False ,q) -> (False , q+r ) ©Arnd Poetzsch-Heffter TU Kaiserslautern 415 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiel: (3) Aufgabe 2: Schreibe eine Funktion, • die ein n-Tupel (n≥2) nimmt und • die erste Komponente des Tupels liefert. Kann in Haskell nicht definiert werden: arbitraryfst :: (a,b,.. .) -> a arbitraryfst (n,.. .) = n ©Arnd Poetzsch-Heffter TU Kaiserslautern 416 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Polymorphie und parametrische Typen Programmierer möchten vom Typsystem nicht weiter eingeengt werden als nötig. Ziel: • mächtige und flexible Typsysteme • insbesondere Polymorphie und Parametrisierung Im Allg. bedeutet Polymorphie Vielgestaltigkeit. In der Programmierung bezieht sich Polymorphie auf die Typisierung bzw. das Typsystem. ©Arnd Poetzsch-Heffter TU Kaiserslautern 417 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Begriffsklärung: (polymorphes Typsystem) Das Typsystem einer Sprache S beschreibt, • welche Typen es in S gibt bzw. wie neue Typen deklariert werden; • wie den Ausdrücken von S ein Typ zugeordnet wird; • welche Regeln typisierte Ausdrücke erfüllen müssen. Ein Typsystem heißt polymorph, wenn Ausdrücke zu Werten bzw. Objekten unterschiedlichen Typs ausgewertet werden können. ©Arnd Poetzsch-Heffter TU Kaiserslautern 418 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Bemerkung: • Man unterscheidet: I Parametrische Polymorphie I Subtyp-Polymorphie (vgl. Typisierung in Java) • Oft spricht man im Zusammenhang mit der Überladung von Funktions- oder Operatorsymbolen von Ad-hoc-Polymorphie. Beispiel: Dem +-Operator könnte man in Haskell den Typ Int ->Int ->Int oder Float ->Float ->Float geben. ©Arnd Poetzsch-Heffter TU Kaiserslautern 419 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Bemerkung: (2) • In polymorphen Typsystemen gibt es meist eine Relation „ist_spezieller_als“ zwischen Typen T1, T2: T1 heißt spezieller als T2, wenn die Eigenschaften, die T1 garantiert, die Eigenschaften von T2 implizieren (umgekehrt sagt man: T2 ist allgemeiner als T1). Beispiel: Der Typ [Int] ist spezieller als der parametrische Typ [a] . Insbesondere gilt: Jeder Wert vom Typ [Int] kann überall dort benutzt werden, wo ein Wert vom Typ [a] erwartet wird. ©Arnd Poetzsch-Heffter TU Kaiserslautern 420 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Typsystem von Haskell und Typinferenz Typen werden in Haskell durch Typausdrücke beschrieben: • Typkonstanten sind die Basisdatentypen: Bool, Char, Int, Integer, Float, Double • Typvariablen: a, meineTypvar, gTyp • Ausdrücke gebildet mit Typkonstruktoren: Seien TA, TA1, TA2, TA3, ... Typausdrücke, dann sind die folgenden Ausdrücke auch Typausdrücke: ( TA1, TA2 ) Typ der Paare ( TA1, TA2, TA3 ) Typ der Triple ... [ TA ] Listentyp TA1 -> TA2 Funktionstyp Einem Typausdruck TA kann ein Typconstraint für Typvariablen a vorangestellt werden: (Eq a) => TA ©Arnd Poetzsch-Heffter TU Kaiserslautern 421 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiele: (Typausdrücke) Int , a, Bool b (Int ,Bool) , (Int ,a) , (Int ,a,b,a) [ Integer ] , [a] , [( Float , b, a)] Int -> Int , [a] -> [( Float , b, a)] (Char -> Char) -> (Int -> Int) (Eq a) => a -> [a] -> Bool Hinweis: -> ist rechtsassoziativ. ©Arnd Poetzsch-Heffter TU Kaiserslautern 422 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Begriffserklärung: (Vergleich von Typen) Seien TA und TB Typausdrücke und var (TA) die Menge der Typvariablen, die in TA vorkommen. Eine Variablensubstitution ist eine Abbildung β von var (TA) auf die Menge der Typausdrücke. Bezeichne TAβ den Typausdruck, den man aus TA erhält, wenn man alle Variablen v in TA konsistent durch β(v ) ersetzt. TB ist spezieller als TA, wenn es eine Variablensubstitution gibt, so dass TAβ = TB. TA und TB bezeichnen den gleichen Typ, wenn TA spezieller als TB ist und TB spezieller als TA. ©Arnd Poetzsch-Heffter TU Kaiserslautern 423 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiele: [Int] ist spezieller als [a] [a] ist spezieller als [b] a -> a ist spezieller als a -> b und umgekehrt ([Int],b) und ([c],Bool) sind nicht vergleichbar, d.h. der erste Ausdruck ist nicht spezieller als der zweite und der zweite nicht spezieller als der erste. (Eq a) => TA ©Arnd Poetzsch-Heffter ist spezieller als TA TU Kaiserslautern 424 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Typen in Haskell Bezeichne TE die Menge der Typausdrücke in Haskell. • Die „ist_spezieller_als“ Relation auf TE × TE ist reflexiv und transitiv, aber nicht antisymmetrisch. • Identifiziert man alle Typausdrücke, die den gleichen Typ bezeichnen, erhält man die Menge T der Typen. ( T , ist_spezieller_als ) ist eine partielle Ordnung. Damit ist gesagt, was Typen in Haskell sind. Bemerkung: Das Typsystem von Haskell ist feiner als hier dargestellt (Typklassen und benutzerdefinierte Typconstraints). ©Arnd Poetzsch-Heffter TU Kaiserslautern 425 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiel: foldplus :: (Num a) => [a] -> a -- Addiert alle Elemente einer Liste . -- Listenelemente muessen alle vom gleichen Zahltyp sein foldplus [] = foldplus (x:xs) = ©Arnd Poetzsch-Heffter 0 x + foldplus xs TU Kaiserslautern 426 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Typregeln in Haskell: Wir nennen eine Ausdruck A typannotiert, wenn A und allen Teilausdrücken von A ein Typ zugeordnet ist. Beispiel: Die Typannotationen von ist abs (-2) ((abs::Int->Int)(-2::Int))::Int Die Typregeln legen fest, wann ein typannotierter Ausdruck typkorrekt ist. In Hakell muss gelten: • Die Typen der formalen Parameter müssen gleich den Typen der aktuellen Parameter sein. • Bedingte Ausdrücke müssen korrekt typisiert sein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 427 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Begriffserklärung: (Typinferenz) Typinferenz bedeutet das Ableiten der Typannotation für Ausdrücke aus den gegebenen Deklarationsinformationen. Bemerkung: In Programmiersprachen mit parametrischem Typsystem kann Typinferenz sehr komplex sein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 428 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiele: 1. Leere Liste: x = [ ] Inferierter Typ für x :: [a] 2. Enthalten sein in Liste: enthalten enthalten | | p1 [] = p1 (x:xs) p1 == x = otherwise = False True enthalten p1 xs Inferierter Typ für enthalten :: (Eq a) => a -> [a] -> Bool ©Arnd Poetzsch-Heffter TU Kaiserslautern 429 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiele: (2) 3. Einsortieren in geordnete Liste mit Vergleichsfunktion: einsortieren vop p1 [] = [p1] einsortieren vop p1 (x:xs) | p1 `vop` x = p1:x:xs | otherwise = x : ( einsortieren vop p1 xs) Inferierter Typ für einsortieren :: (t -> t -> Bool) -> t -> [t] -> [t] Bemerkung: Bei der Typinferenz versucht man immer den allgemeinsten Typ herauszufinden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 430 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Parametrische markierte Binärbaumtypen: 1. Alle Markierungen sind vom gleichen Typ: data BBaum a = Blatt a | Zweig a ( BBaum a) ( BBaum a) 2. Blatt- und Zweigmarkierungen sind möglicherweise von unterschiedlichen Typen: data BBaum a b = Blatt a | Zweig b ( BBaum a b) ( BBaum a b) ©Arnd Poetzsch-Heffter TU Kaiserslautern 431 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Parametrische markierte Binärbaumtypen: (2) 3. Und was passiert hier? data BBaum a b = Blatt a | Zweig b ( BBaum b a) ( BBaum a b) ©Arnd Poetzsch-Heffter TU Kaiserslautern 432 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Unterabschnitt 3.3.2 Funktionen höherer Ordnung ©Arnd Poetzsch-Heffter TU Kaiserslautern 433 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Funktionen höherer Ordnung Überblick: • Einführung in Funktionen höherer Ordnung • Wichtige Funktionen höherer Ordnung ©Arnd Poetzsch-Heffter TU Kaiserslautern 434 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Einführung Funktionen höherer Ordnung sind Funktionen, die • Funktionen als Argumente nehmen und/oder • Funktionen als Ergebnis haben. Selbstverständlich sind auch Listen oder Tupel von Funktionen als Argumente oder Ergebnisse möglich. Eine Funktion F, die Funktionen als Argumente nimmt und als Ergebnis liefert, nennt man häufig auch ein Funktional. Funktionale, die aus der Schule bekannt sind, sind • Differenzial • unbestimmtes Integral ©Arnd Poetzsch-Heffter TU Kaiserslautern 435 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Sprachliche Aspekte: Alle wesentlichen Sprachmittel zum Arbeiten mit Funktionen höherer Ordnung sind bereits bekannt: • Funktionsabstraktion • Funktionsdeklaration • Funktionsanwendung • Funktionstypen ©Arnd Poetzsch-Heffter TU Kaiserslautern 436 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Konzeptionelle Aspekte: Zwei konzeptionelle Aspekte liegen der Anwendung von Funktionen höherer Ordnung in der Software- Entwicklung zugrunde: 1. Abstraktion und Wiederverwendung 2. Metaprogrammierung, d.h. das Entwickeln von Programmen, die Programme als Argumente und Ergebnisse haben. Wir betrachten im Folgenden den ersten Aspekt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 437 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiel: (Abstraktion, Wiederverwendung) Aufgabe: Sortiere eine Liste xl von Zahlen durch Einfügen (insertion sort) Rekursionsidee: • Sortiere zunächst den Rest der Liste. • Das ergibt eine sortierte Liste xs. • Sortiere das erste Element von xl in xs ein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 438 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiel: (Abstraktion, Wiederverwendung) (2) einsortieren :: Int -> [Int] -> [Int] einsortieren p1 [] = [p1] einsortieren p1 (x:xs) | p1 <= x = p1:x:xs | otherwise = x : ( einsortieren p1 xs) sort [] = [] sort (x:xr) = einsortieren x (sort xr) ©Arnd Poetzsch-Heffter TU Kaiserslautern 439 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Beispiel: (Abstraktion, Wiederverwendung) (3) Frage: Was ist zu tun, damit sort auch Werte der Typen Char, String, Float, etc. sortieren kann? Antwort: Abstraktion des Algorithmus: Führe die Vergleichsoperation als weiteren Parameter ein. einsortieren :: (t -> t -> Bool) -> t -> [t] -> [t] einsortieren vop p1 [] = [p1] einsortieren vop p1 (x:xs) | p1 `vop` x = p1:x:xs | otherwise = x : ( einsortieren vop p1 xs) sort vop [] = [] sort vop (x:xr) = einsortieren vop x (sort vop xr) ©Arnd Poetzsch-Heffter TU Kaiserslautern 440 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Anwendung: l1 l2 l3 l4 = = = = sort sort sort sort (<=) [ 2,3, 968 , -98 ,34 ,0 ] (>=) [ 2,3, 968 , -98 ,34 ,0 ] ((>=):: Float -> Float -> Bool) [1.0 ,1e -4] (>=) [1.0 ,1e -4] strcmp :: String -> String -> Bool strcmp = (<=) l5 = sort strcmp [" Abbay ","Abba","Ara","ab"] ©Arnd Poetzsch-Heffter TU Kaiserslautern 441 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Bemerkung: Polymorphe Funktionen können häufig auch Funktionen als Parameter nehmen. Beispiele: 1. Funktion cons auf Listen : abs :fac :[] 2. Identitätsfunktion: id = (x-> x)(x->x) ©Arnd Poetzsch-Heffter TU Kaiserslautern 442 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Wichtige Funktionen höherer Ordnung Dieser Abschnitt betrachtet einige Beispiele für Funktionen höherer Ordnung und diskutiert das Arbeiten mit solchen Funktionen. Applikationsoperator: ($) :: (a -> b) -> a -> b rechtsassoziativ; erlaubt andere Schreibweise/Klammersetzung: fac $ fac $ n+1 statt fac (fac (n+1)) Funktionskomposition: (.) :: (b -> c) -> (a -> b) -> a -> c (f.g) x = f (g x) ©Arnd Poetzsch-Heffter TU Kaiserslautern 443 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Wichtige Funktionen höherer Ordnung (2) Map: Anwendung einer Funktion auf die Elemente einer Liste: map f [x1,x2,x3] == [f x1, f x2, f x3] map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = (f x): map f xs Beispiele: map length [" Schwerter ","zu"," Pflugscharen "] double n = 2*n map (map double ) ©Arnd Poetzsch-Heffter [ [1 ,2] , [34829] ] TU Kaiserslautern 444 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Currying und Schönfinkeln: Funktionen mit einem Argumenttupel kann man die Argumente auch sukzessive geben. Dabei entstehen Funktionen höherer Ordnung. Beispiele: times :: Integer -> Integer -> Integer times m n = m * n double :: Integer -> Integer double = times 2 double 5 ©Arnd Poetzsch-Heffter TU Kaiserslautern 445 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Zwei Varianten von map im Vergleich: 1. Die beiden Argumente als Paar: mapp :: (b -> a, [b]) -> [a] mapp (f ,[]) = [] mapp (f,x:xs) = (f x): mapp (f,xs) 2. Die Argumente nacheinander („gecurryt“): map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = (f x): map f xs ©Arnd Poetzsch-Heffter TU Kaiserslautern 446 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Bemerkung: Die gecurryte Fassung ist flexibler: Sie kann nicht nur auf ein vollständiges Argumententupel angewendet werden, sondern auch zur Definition neuer Funktionen mittels partieller Anwendung benutzt werden. Beispiele: double :: Integer -> Integer double = times 2 intListSort :: [Int] -> [Int] intListSort = sort ((<=)::Int ->Int ->Bool) doublelist :: [Int] -> [Int] doublelist = map double ©Arnd Poetzsch-Heffter TU Kaiserslautern 447 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Curryen von Funktionen: Die Funktion curry liefert zu einer Funktion auf Paaren die zugehörige gecurryte Funktion: curry :: ((a, b) -> c) -> a -> b -> c curry f x y = f (x,y) ©Arnd Poetzsch-Heffter TU Kaiserslautern 448 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Prüfen und Filtern von Listen: any :: (a -> Bool) -> [a] -> Bool -- Pruefe , ob Liste ein Element enthaelt , das -- das gegebene Praedikat erfuellt any pred [ ] = False any pred (x:xs) = pred x || any pred xs all :: (a -> Bool) -> [a] -> Bool -- Pruefe , ob jedes Listenelement das gegebene -- Praedikat erfuellt all pred [ ] = True all pred (x:xs) = pred x ©Arnd Poetzsch-Heffter && all pred xs TU Kaiserslautern 449 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Prüfen und Filtern von Listen: (2) filter :: (a -> Bool) -> [a] -> [a] -- Liste alle Elemente , die das gegebene Praedikat -- erfuellen filter pred [ ] = [] filter pred (x:xs) | pred x = x : ( filter pred xs) | otherwise = ( filter pred xs) Beispiele: ismember x xs = any (\y-> x==y) xs split p xs = ( filter (\y-> y<p) xs , filter (\y-> p<=y) xs) ©Arnd Poetzsch-Heffter TU Kaiserslautern 450 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Punktweise Veränderung von Funktionen: Die "Veränderung" einer Funktion an einem Punkt des Argumentbereichs: update :: (Eq a) => (a -> b) -> a -> b -> a -> b update f x v y | x == y = | otherwise = ©Arnd Poetzsch-Heffter v f y TU Kaiserslautern 451 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Falten von Listen: Eine häufig benötigte Funktion ist das Falten einer Liste mittels einer binären Funktion und einem neutralen Element bzw. Anfangselement: foldr ⊗ n [e1, e2, . . . ,en] = e1 ⊗ (e2 ⊗ (...(en ⊗ n) . . . )) Deklaration von foldr: foldr :: (a -> b -> b) -> b -> [a] -> b foldr f n [ ] = n foldr f n (x:xs) = f x ( foldr f n xs) ©Arnd Poetzsch-Heffter TU Kaiserslautern 452 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Falten von Listen: (2) Üblicherweise steht auch eine Funktion für das Falten von links zur Verfügung: foldl ⊗ n [e1, e2, . . . ,en] = (. . . ((n ⊗ e1) ⊗ e2) ... ⊗ en) Deklaration von foldl: foldl :: (b -> a -> b) -> b -> [a] -> b foldl f n [ ] = n foldl f n (x:xs) = foldl f (f n x) xs ©Arnd Poetzsch-Heffter TU Kaiserslautern 453 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Falten von Listen: (3) Mittels der Faltungsfunktionen foldr und foldl lassen sich viele Listenfunktionen direkt, d.h. ohne Rekursion definieren: sum , product :: (Num a) => [a] -> a sum = foldr (+) 0 product = foldr (*) 1 (++) :: [a] -> [a] -> [a] (++) l1 l2 = foldr (:) l2 l1 ©Arnd Poetzsch-Heffter TU Kaiserslautern 454 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Falten von Listen: (4) Bäume mit variabler Kinderzahl lassen sich allgemeiner und kompakter behandeln (vgl. Folien 298f): data VBaum = Kn Int [ VBaum ] deriving (Eq ,Show) zaehleKnVBaum :: VBaum -> Int zaehleknVBaum (Kn (_,xs)) = foldr (+) 1 (map zaehleknVBaum xs) ©Arnd Poetzsch-Heffter TU Kaiserslautern 455 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Bemerkungen: (Funktionen höherer Ordnung) Die Programmentwicklung mittels Funktionen höherer Ordnung (funktionale Programmierung) ist ein erstes Beispiel für: • das Zusammensetzen komplexerer Bausteine, • programmiertechnische Variationsmöglichkeiten, • die Problematik der Wiederverwendung: I die Bausteine müssen bekannt sein, I die Bausteine müssen ausreichend generisch sein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 456 3. Funktionales Programmieren 3.3 Polymorphie und Funktionen höherer Ordnung Bemerkungen: (zur Haskell-Einführung) • Ziel des Kapitels war es nicht, eine umfassende Haskell-Einführung zu geben. Haskell dient hier vor allem als Hilfsmittel, wichtige Konzepte zu erläutern. • Die meisten zentralen Konstrukte wurden behandelt. • Es fehlt insbesondere: I Fehlerbehandlung I Typklassen I Aspekte imperativer und interaktiver Programmierung • Viele Aspekte der Programmierumgebung wurden nicht näher erläutert. ©Arnd Poetzsch-Heffter TU Kaiserslautern 457 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Abschnitt 3.4 Semantik, Testen und Verifikation ©Arnd Poetzsch-Heffter TU Kaiserslautern 458 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Übersicht • Einführung in Semantik von Programmiersprachen • Testen und Verifikation ©Arnd Poetzsch-Heffter TU Kaiserslautern 459 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Unterabschnitt 3.4.1 Zur Semantik funktionaler Programme ©Arnd Poetzsch-Heffter TU Kaiserslautern 460 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Zur Semantik funktionaler Programme Lernziele in diesem Unterabschnitt: • Was bedeutet Auswertungssemantik? • Wie sieht sie im Falle von Haskell aus? • Welche Bedeutung haben Bezeichnerumgebungen dabei? ©Arnd Poetzsch-Heffter TU Kaiserslautern 461 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Zur Semantik funktionaler Programme (2) In erster Näherung definiert eine Funktionsdeklaration eine partielle Funktion. Gründe für Partialität: 1. Der Ausdruck, der die Funktion definiert, ist bereits partiell: division dd dr hd x:xs = x = dd `div` dr 2. Behandlung rekursiver Deklarationen: a. Insgesamt unbestimmt: f :: a -> a f x = f x b. Teilweise unbestimmt (hier für negative Zahlen): fac :: Integer -> Integer fac n = if n==0 then 1 else n * fac(n -1) ©Arnd Poetzsch-Heffter TU Kaiserslautern 462 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Ziel: Ordne jeder syntaktisch korrekten Funktionsdeklaration eine partielle Funktion zu. Die Semantik beschreibt diese Zuordnung. Wir unterscheiden hier denotationelle und operationelle Semantik. Statt operationeller Semantik spricht man häufig von Auswertungssemantik. ©Arnd Poetzsch-Heffter TU Kaiserslautern 463 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (denotationelle Semantik) Eine Semantik, die jeder Funktionsdeklaration explizit eine partielle Funktion als Bedeutung zuordnet, d.h. eine Abbildung von Funktionsdeklarationen auf partielle Funktionen definiert, nennen wir denotationell. ©Arnd Poetzsch-Heffter TU Kaiserslautern 464 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) Eine denotationelle Semantik würde der obigen Funktionsdeklaration von fac eine Funktion f f : Z⊥ → Z⊥ zuordnen, wobei Z⊥ = { x | x ist Wert vom Typ Integer } ∪ {⊥} Diese Funktion muss die Gleichung für fac erfüllen. Das Symbol ⊥ steht dabei für ündefiniertünd wird häufig als bottom bezeichnet. ©Arnd Poetzsch-Heffter TU Kaiserslautern 465 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) (2) Zwei mögliche Lösungen f1 und f2 : ( f1 (k ) = ⊥ , falls k =⊥ oder k < 0 k ! , sonst ⊥ , falls k =⊥ 0 ,k < 0 f2 (k ) = k ! , sonst ©Arnd Poetzsch-Heffter TU Kaiserslautern 466 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) (3) Wir zeigen, dass f2 eine Lösung der Gleichung ist: n =⊥: links: f2 (⊥) =⊥ rechts: if ⊥= 0 then 1 else ⊥ ∗f2 (⊥ −1) = ⊥ n < 0: links: rechts: f2 (n) = 0 if n = 0 then 1 else n ∗ f2 (n − 1) = n ∗ 0 = 0 n = 0: links: rechts: f2 (0) = 0! = 1 if 0 = 0 then 1 else 0 ∗ f2 (0 − 1) = 1 n > 0: links: rechts: f2 (n) = n! if n = 0 then 1 else n ∗ f2 (n − 1) = n ∗ (n − 1)! = n! Genauso lässt sich zeigen, dass f1 eine Lösung ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 467 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (denotationelle Semantik) (4) Die denotationelle Semantik muss sicherstellen, • dass es für jede Funktionsdeklaration mindestens eine Lösung gibt, und • eine Lösung auszeichnen, wenn es mehrere gibt. In den meisten Programmiersprachen wählt man die Lösung, die an den wenigsten Stellen definiert ist, und betrachtet nur so genannte strikte Funktionen als Lösung: ©Arnd Poetzsch-Heffter TU Kaiserslautern 468 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (strikte Funktionen) Eine n-stellige Funktion oder Operation heißt strikt, wenn sie ⊥ als Ergebnis liefert, sobald eines der Argumente ⊥ ist. Beispiele: (nicht-strikte Funktionen) • Die dreistellige “Funktion” if-then-else und die boolschen Operatoren && und || sind in fast allen Programmiersprachen nicht strikt. • In Haskell deklarierte Funktionen sind im Allg. nicht strikt: ite :: Bool -> a -> a -> a ite b x y = if b then x else y Prelude > 45 ©Arnd Poetzsch-Heffter ite False (4 `div` 0) 45 TU Kaiserslautern 469 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkungen: • Denotationelle Semantik basiert auf einer Theorie partieller strikter Funktionen und Fixpunkttheorie. I I Vorteil: Für Beweise besser geeignet. Nachteil: Theoretisch aufwendiger zu handhaben. • ⊥ steht für undefiniert, unabhängig davon, welcher der Gründe für Partialität vorliegt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 470 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (operationelle Semantik) Eine Semantik, die erklärt, wie eine Funktion oder ein Programm auszuwerten ist, nennen wir operationell oder Auswertungssemantik . Wir erläutern • eine Auswertungsstrategie für funktionale Programme, • welche Rolle Bezeichnerumgebungen dabei spielen, und • führen wichtige Begriffe ein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 471 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (formaler/aktueller Parameter) Ein Bezeichner, der in einer Funktionsdeklaration einen Parameter bezeichnet, wird formaler Parameter genannt. Der Ausdruck oder Wert, der einer Funktion bei einer Anwendung übergeben wird, wird aktueller Parameter genannt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 472 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (Auswertungsstrategie) Die Auswertungsstrategie legt fest, • in welchen Schritten die Ausdrücke ausgewertet werden und • wie die Parameterübergabe geregelt ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 473 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiele: (Parameterübergabeverfahren) Parameterübergabe: 1. Call-by-Value: I I I Werte die aktuellen Parameter aus. Benutze die Ergebnisse anstelle der formalen Parameter im definierenden Ausdruck/Rumpf. Werte den Rumpf aus. 2. Call-by-Name: I I Ersetze alle Vorkommen der formalen Parameter durch die (unausgewerteten) aktuellen Parameterausdrücke. Werte den Rumpf aus. Unterschiedliche Auswertungsstrategien führen im Allg. zu unterschiedlichen Ergebnissen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 474 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Auswertungsstrategien Betrachte: f (x,y) = if x==0 then 1 else f (x-1,f(x-y,y)) Werte den Ausdruck f (1,0) aus: 1. Call-by-Value: f (1 ,0) = = = if 1==0 then 1 else f(1-1,f(1 -0 ,0)) if False then 1 else f(1-1,f(1 -0 ,0)) f (1-1, f(1 -0 ,0) ) ©Arnd Poetzsch-Heffter TU Kaiserslautern 475 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Auswertungsstrategien (2) = = = = = = f (0, f(1 -0 ,0) ) f (0, f(1 ,0) ) f (0, if 1==0 then 1 else f(1-1,f(1 -0 ,0))) .. .. f (0, f(0, f (1 ,0) )) .. .. Diese Auswertung kommt nicht zum Ende, d.h. sie terminiert nicht. ©Arnd Poetzsch-Heffter TU Kaiserslautern 476 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Auswertungsstrategien (3) 2. Call-by-Name: = = = = f (1 ,0) if 1==0 then 1 else f(1-1,f(1 -0 ,0)) if False then 1 else f(1-1,f(1 -0 ,0)) f( 1-1, f(1 -0 ,0) ) if 1-1==0 then else ©Arnd Poetzsch-Heffter 1 f(1-1-1,f(1-1-f(1-0, 0) ,f(1 -0 ,0))) TU Kaiserslautern 477 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Auswertungsstrategien (4) = = = if 1-1==0 then else 1 f(1-1-1,f(1-1-f(1-0, 0) ,f(1 -0 ,0))) if True then else 1 f(1 -1 -1,f(1-1-f(1 -0 ,0) ,f(1 -0 ,0))) 1 Mit Call-by-Name terminiert die Auswertung von f(1,0). ©Arnd Poetzsch-Heffter TU Kaiserslautern 478 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (Normalform) Der Ergebnisausdruck einer terminierenden Auswertung wird Normalform genannt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 479 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Informelle Auswertungssemantik von Haskell Haskell benutzt Call-by-Need zur Parameterübergabe und Auswertung. Call-by-Need ist eine verfeinerte Form von Call-by-Value, bei der ein aktueller Parameter, wenn er mehrfach benötigt wird, nur einmal ausgewertet wird. In einer Sprache ohne Seiteneffekte wie Haskell unterscheiden sich Call-by-Need und Call-by-Value aber nicht im Ergebnis, sondern nur in der Effizienz der Auswertung. Die Ausdrücke werden von • von links nach rechts (engl. leftmost), • von außen nach innen (engl. outermost) und • nur, wenn sie gebraucht werden, ausgewertet. ©Arnd Poetzsch-Heffter TU Kaiserslautern 480 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Eine Teilsprache von Haskell data Exp = Cond Exp Exp Exp | Ident String | Binary Op Exp Exp | Lambda String Exp | Appl Exp Exp | Let String Exp Exp | BConst Bool | IConst Integer | Closure String Exp Env deriving (Eq , Show) data Op = Plus | Mult | Eq deriving (Eq , Show) ©Arnd Poetzsch-Heffter TU Kaiserslautern 481 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispielprogramme -- let a = 5 -- in let b = a + 7 -in let a = 0 in b letx = Let "a" ( IConst 5) (Let "b" ( Binary Plus ( Ident "a") ( IConst 7)) (Let "a" ( IConst 0) ( Ident "b"))) ©Arnd Poetzsch-Heffter TU Kaiserslautern 482 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispielprogramme (2) -- let fac = \n -> if n==0 then 1 else n * fac(n+( -1)) -- in fac 10 facx = Let "fac" ( Lambda "n" (Cond ( Binary Eq ( Ident "n") ( IConst 0)) ( IConst 1) ( Binary Mult ( Ident "n") (Appl ( Ident "fac") ( Binary Plus ( Ident "n")( IConst ( -1) )) ) ) ) ) (Appl ( Ident "fac") ( IConst 10)) ©Arnd Poetzsch-Heffter TU Kaiserslautern 483 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispielprogramme (3) -- let o = \f -> \g -> \x -> f (g x) in -- in let fac = \n->if n==0 then 1 else n*fac(n+( -1)) -in (fac `o` fac) 5 compx = Let "o" ( Lambda "f" ( Lambda "g" ( Lambda "x" (Appl ( Ident "f") (Appl ( Ident "g") ( Ident "x")))))) (Let "fac" ... -- wie oben (Appl (Appl (Appl ( Ident "o") ( Ident "fac")) ( Ident "fac")) ( IConst 5)) ) ©Arnd Poetzsch-Heffter TU Kaiserslautern 484 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Auswertungsbeispiel = = = = = = = eval (let a=5 in let b=a+7 in let a=0 in b) eval (let b=a+7 in let a=0 in b) [] [ a=(5 ,[]) ] eval (let a=0 in b) [ b=(a+7 ,[a=(5 ,[]) ]) , a=(5 ,[]) ] eval b [ a=(0 ,[ .. .]) , b=(a+7 ,[a=(5 ,[]) ]) , a=(5 ,[]) ] eval (a+7) (eval a [ a=(5 ,[]) ] [ a=(5 ,[]) ]) + (eval 7 [ a=(5 ,[]) ]) (eval 5 []) + 7 5 + 7 = ©Arnd Poetzsch-Heffter 12 TU Kaiserslautern 485 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Datentyp für Bezeicherumgebung data Env = Ec [ (String ,(Exp ,Env)) ] deriving (Eq , Show) emptyEnv :: Env -- leere Bezeichnerumbegung insert :: String -> (Exp ,Env) -> Env -> Env -- ( insert bez xe e) traegt die Bindung (bez ,xe) -- in die Umgebung e ein lookUp :: String -> Env -> (Exp ,Env) -- ( lookUp bez e) liefert das Paar xe der ersten -- gefundenen Bindung (bez ,xe) mit Bezeichner bez ©Arnd Poetzsch-Heffter TU Kaiserslautern 486 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Funktionsabschlüsse: Fragen: Wie wird das Ergebnis eines funktionswertigen Ausdrucks dargestellt? Wie wird eine benutzerdeklarierte Funktion in der Bezeichnerumgebung dargestellt? ©Arnd Poetzsch-Heffter TU Kaiserslautern 487 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Funktionsabschlüsse: (2) Vorgehen: Das Ergebnis eines funktionswertigen Ausdrucks wird als Triple Closure s r e dargestellt, den sogenannten Funktionsabschluss (engl. Closure): • s bezeichnet den formalen Parameter • r bezeichnet den Funktionsrumpf • e bezeichnet die aktuell gültige Umgebung. ©Arnd Poetzsch-Heffter TU Kaiserslautern 488 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: = = = = = = eval (let a = 6 in let ida = \x -> (x+a) in ida 9) eval (let ida = \x -> (x+a) in ida 9) eval (ida 9) [] [ a=(6 ,[]) ] [ ida = (\x->(x+a) ,[a=(6 ,[]) ]) ,a=(6 ,[])] wende (eval ida [ida=(\x->(x+a) ,[a=(6 ,[]) ]) ,a=(6 ,[]) ]) auf 9 mit e = [ida=(\x->(x+a) ,[a=(6 ,[]) ]) , a=(6 ,[])] an wende (eval (\x -> (x+a)) [a=(6 ,[]) ]) auf 9 mit e an wende ( Closure x (x+a) [a=(6 ,[])] ) auf 9 mit e an eval (x+a) [ x=(9,e), a=(6 ,[]) ] = (eval x [x=(9,e),a=(6 ,[]) ]) + (eval a [x=(9,e),a=(6 ,[]) ]) ©Arnd Poetzsch-Heffter TU Kaiserslautern 489 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Auswertungssemantik für die Haskell-Teilsprache: eval :: Exp -> Env -> Exp eval (Cond bx tx ex) e = let BConst b = eval bx e in if b then eval tx e else eval ex e eval (Ident s ) e = let (xv ,ev) = ( lookUp s e) in eval xv ev eval ( Binary bo lx rx) e = let IConst li = eval lx e IConst ri = eval rx e in evalOp bo li ri eval ( Lambda s bx ) e = Closure s bx e eval (Appl fx px ) e = let Closure s b ce = eval fx e in eval b ( insert s (px ,e) ce) eval (Let s dx bx ) e = let en = ( insert s (dx ,en) e) in eval bx en eval x e = x ©Arnd Poetzsch-Heffter TU Kaiserslautern 490 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Auswertungssemantik für die Haskell-Teilsprache: (2) evalOp :: Op -> Integer -> Integer -> Exp evalOp Plus li ri = IConst (li+ri) evalOp Mult li ri = IConst (li*ri) evalOp Eq li ri = BConst (li==ri) ©Arnd Poetzsch-Heffter TU Kaiserslautern 491 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkungen: • Wir haben die Auswertungssemantik von Haskells Teilsprache in Haskell selbst definiert, weil wir keine andere Beschreibungstechnik kennen. • Üblicherweise wird man einen anderen Beschreibungsformalismus wählen. • Mit Ausnahme der Gleichung für die rekursiven Definitionen von Let-Ausdrücken lassen sich alle Gleichungen als Ersetzungsregeln benutzen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 492 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Spezifikation der zulässigen Parameter Bei jeder (partiellen) Funktion muss man sich überlegen und dokumentieren, welche aktuellen Parameter bei einer Anwendung zulässig sein sollen. Der Anwender der Funktion hat dann die Verantwortung, dass die Komponente nie mit unzulässigen Parametern angewendet wird. Üblicherweise sollte die Komponente für zulässige Parameter normal terminieren. Ggf. sind möglicherweise auftretende Ausnahmen zu dokumentieren. Entsprechendes gilt für andere parametrisierte Softwarekomponenten. ©Arnd Poetzsch-Heffter TU Kaiserslautern 493 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkungen: Es ist eine Entwurfsentscheidung, • welche Parameter zulässig und welche unzulässig sind; • wie man mit unzulässigen Paramtern umgeht. Beispiel: Varianten einer Funktion foo: 1. foo(m,n) = if m<n then m `div` n else foo(m-n,n) Zulässigkeitsbereich (m < n && n /=0)|| n > 0 2. foo(m,n) = if m < 0 || n <= 0 then -1 else if m<n then m `div` n else foo(m-n,n) Zulässkeitsbereich (m >=0 && n > 0), vervollständigt durch -1. ©Arnd Poetzsch-Heffter TU Kaiserslautern 494 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkungen: (2) 3. foo(m,n) = if (m <= 0 || n==0) && n <= 0 then undefined else if m<n then m `div` n else foo (m-n,n) Zulässigkeitsbereich (m < n && n /=0)|| n > 0; Fehlermeldung, wenn Zulässigkeitsbereich verlassen wird. Das Abprüfen der Zulässigkeit von Parametern (defensive Programmierung) führt zu besserer Stabilität, allerdings oft auf Kosten der Lesbarkeit und Effizienz der Softwarekomponente. ©Arnd Poetzsch-Heffter TU Kaiserslautern 495 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Design by Contract: Entwerfe Programmteile zusammen mit genauen Spezifikationen ihrer Schnittstellen. Insbesondere spezifiziere bei Funktionen, welche Parameter zulässig sind und was das Ergebnis im Zulässigkeitsbereich ist. Die Spezifikation kann als Vertrag zwischen • dem Anwender der Funktion/Komponente (client), • demjenigen, der die Funktion/Komponente realisiert (provider) verstanden werden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 496 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Spezifikation) Spezifikation der Schnittstelle der Funktion heapifiy: heapify :: FVBintree -> Int -> FVBintree Vorbedingung, die ein Anwender von heapify b ix erfüllen sollte: • ix ist ein Index von b sein, d.h. 0 ≤ ix < size b • die Kinder von ix in b erfüllen die Heap-Eigenschaft Nachbedingung an das Ergebnis e von heapify b ix umfasst: • size e =size b • die Markierungen der Knoten, die sich nicht im Unterbaum von ix befinden, sind in e und b gleich; • die Menge der Markierungen der Knoten, die sich im Unterbaum von ix befinden, sind in e und b gleich; • der Knoten mit Index ix und alle seine Kinder in b erfüllen die Heap-Eigenschaft. ©Arnd Poetzsch-Heffter TU Kaiserslautern 497 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Unterabschnitt 3.4.2 Testen und Verifikation ©Arnd Poetzsch-Heffter TU Kaiserslautern 498 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Testen und Verifikation Ein zentraler Teil der Software-Entwicklung besteht darin zu prüfen, ob die entwickelte Software auch den gestellten Anforderungen entspricht. Überblick: • Einführende Bemerkungen zur Qualitätssicherung • Testen • Verifikation von Programmeigenschaften ©Arnd Poetzsch-Heffter TU Kaiserslautern 499 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Qualitätssicherung: Kleine Einführung Bei der Qualitätssicherung in der Softwareentwicklung spielen zwei Fragen eine zentrale Rolle: • Wird das richtige System entwickelt? • Wird das System richtig entwickelt? ©Arnd Poetzsch-Heffter TU Kaiserslautern 500 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Qualitätssicherung: Kleine Einführung (2) Validation hat die Beantwortung der ersten Frage zum Ziel. Beispielsweise ist zu klären, ob • die benutzten Anforderungen die Vorstellungen des Auftraggebers richtig wiedergeben, • die Anforderungen von allen Beteiligten gleich interpretiert werden, • Unterspezifizierte Aspekte richtig konkretisiert wurden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 501 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Qualitätssicherung: Kleine Einführung (3) Validation prüft also die Übereinstimmung von Software mit den Vorstellungen der Auftraggeber/ Benutzer bzw. mit der Systemumgebung, in der die Software eingesetzt wird. Unter Verifikation verstehen wir den Nachweis, dass Software bestimmte, explizit beschriebene Eigenschaften besitzt. Beispiele: • Mit Testen kann man prüfen, ob ein Programm zu gegebenen Eingaben die erwarteten Ausgaben liefert (Beschreibung: Testfälle). • Mittels mathematischen Beweisen kann man zeigen, dass ein Programm für alle Eingaben ein bestimmtes Verhalten besitzt (Beschreibung: boolesche Ausdrücke, logische Formeln). • Nachweis, dass ein Programm einen gegebenen Entwurf und die darin festgelegten Eigenschaften besitzt (Entwurfsbeschreibung). ©Arnd Poetzsch-Heffter TU Kaiserslautern 502 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkungen: • Liegen die beschriebenen Eigenschaften in einer formalen Sprache vor, kann die Verifikation automatisiert werden. • Zu prüfende Eigenschaften bei Funktionen: I Terminierung für zulässige Parameter I Verhalten wie im Entwurf festgelegt • Grundsätzlich können sich Validation und Verifikation auf alle Phasen der Softwareentwicklung beziehen. • Wir betrachten im Folgenden Testen und Verifikation durch Beweis anhand einfacher Beispiele im Kontext der funktionalen Programmierung. Eine systematischere Betrachtung von Aspekten der Qualitätssicherung ist Gegenstand von SE 2. ©Arnd Poetzsch-Heffter TU Kaiserslautern 503 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Begriffsklärung: (Testen) Testen bedeutet die Ausführung eines Programms oder Programmteils mit bestimmten Eingabedaten. Testen kann sowohl zur Validation als auch zur Verifikation dienen. Bei funktionalen Programmen bezieht sich Testen überwiegend auf Eingabe- und Ausgabeverhalten von Funktionen. Wir betrachten: • Testen mit Testfällen • Testen durch dynamisches Prüfen ©Arnd Poetzsch-Heffter TU Kaiserslautern 504 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Testen mit Testfällen • Beschreibe das “Soll-Verhalten” der Software durch eine (endliche) Menge von Eingabe-Ausgabe-Paaren. • Prüfe, ob die Software zu den Eingaben der Testfälle die entsprechenden Ausgaben liefert. ©Arnd Poetzsch-Heffter TU Kaiserslautern 505 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: Testen der folgenden Funktionsdeklaration: fac :: Int -> Int -- Berechnet fuer n in [0 .. 12] die Fakultaet fac 0 = 1 fac n = if 0<n || n <= 12 then n * fac (n -1) else undefined ©Arnd Poetzsch-Heffter TU Kaiserslautern 506 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Testfälle: 0 12 13 -1 -1073741824 ©Arnd Poetzsch-Heffter → → → → → 1 479001600 Fehler: undefiniert/unzulaessiger Parameter Fehler: undefiniert/unzulaessiger Parameter Fehler: undefiniert/unzulaessiger Parameter TU Kaiserslautern 507 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beobachtetes Verhalten: 0 12 13 -1 -1073741824 ©Arnd Poetzsch-Heffter → → → → → 1 479001600 1932053504 *** Exception: stack overflow *** Exception: stack overflow TU Kaiserslautern 508 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkung: • Das Verhalten von Funktionen mit unendlichem Argumentbereich kann durch Testen nur teilweise verifiziert werden. Testen kann im Allg. nicht die Abwesenheit von Fehlern zeigen. • Wichtig ist die Auswahl der Testfälle. Sie sollten die “relevanten” Argumentbereiche abdecken. ©Arnd Poetzsch-Heffter TU Kaiserslautern 509 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Testen durch dynamisches Prüfen • Beschreibe Eigenschaften von Zwischen- oder Ergebniswerten mit den Mitteln der Programmiersprache (meist boolsche Ausdrücke); d.h. implementiere Prüfprädikate. • Rufe die Prüfprädikate an den dafür vorgesehenen Stellen im Programm auf. • Lasse die Prüfprädikate in der Testphase des zu testenden Programms auswerten. Bei negativem Prüfergebnis muss ein Fehler erzeugt werden. Anders als beim Testen mit Testfällen wird also das Verhalten des Programms an bestimmten Stellen automatisch während der Auswertung geprüft. ©Arnd Poetzsch-Heffter TU Kaiserslautern 510 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Prüfungen: 1. Prüfung der Zulässigkeit von Parametern beim Aufruf 2. Prüfung durch Ergebniskontrolle Bemerkung: Viele moderne Programmiersprachen bieten spezielle Sprachkonstrukte für das Testen durch dynamische Prüfung an. ©Arnd Poetzsch-Heffter TU Kaiserslautern 511 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Verifikation durch Beweis Verifikation im engeren Sinne meint meist Verifikation durch Beweis. Hier verwenden wir den Begriff in diesem engeren Sinne. Im Gegensatz zum Testen erlaubt Verifikation (durch Beweis) die Korrektheit zu zeigen, d.h. insbesondere die Abwesenheit von Fehlern. Wir betrachten hier nur Programmverifikation, d.h. den Nachweis, dass ein Programm eine spezifizierte Eigenschaft besitzt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 512 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Verifikation durch Beweis (2) Die Spezifikation kommt üblicherweise aus dem Entwurf bzw. den Anforderungen. Zwei zentrale Eigenschaften: • Programm liefert die richtigen Ergebnisse, wenn es terminiert (partielle Korrektheit). • Programm terminiert für die zulässigen Eingaben. Beide Eigenschaften zusammen ergeben totale Korrektheit. ©Arnd Poetzsch-Heffter TU Kaiserslautern 513 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiele: (Spezifikation) Spezifikation: Eine Funktion ggt soll implementiert werden. Für m, n mit m ≥ 0, n ≥ 0, m, n nicht beide null, soll gelten: ggt m n = max { k | k teilt m und n } ©Arnd Poetzsch-Heffter TU Kaiserslautern 514 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Implementierung) Euklidscher Algorithmus: ggt :: Integer -> Integer -> Integer -- m, n >= 0, nicht beide gleich 0 ggt m n = if m==0 then n else ggt (n `mod` m) m ©Arnd Poetzsch-Heffter TU Kaiserslautern 515 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beweisverfahren: Bei funktionalen Programmen spielen zwei Beweisverfahren eine zentrale Rolle: 1. Strukturelle Induktion oder Parameterinduktion 2. Berechnungsinduktion (computational induction) Wir stellen nur die Paramterinduktion/strukturelle Induktion vor. Bei der Parameterinduktion werden die Eigenschaften einer Funktion für alle Parameter gezeigt, indem man eine Induktion über die Menge der zulässigen Parameter führt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 516 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Verifikation von ggt) Vorüberlegung: Für n ≥ 0, m > 0 gilt: k teilt m und n ⇔ k teilt m und k teilt (n mod m) Induktion über den Parameterbereich: Wir zeigen: a) ggt ist korrekt für m = 0 und beliebiges n. b) Vorausgesetzt: ggt ist korrekt für alle Paare (k , n) mit k ≤ m und n beliebig; dann auch für alle Paare (m + 1, n) mit n beliebig. ©Arnd Poetzsch-Heffter TU Kaiserslautern 517 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Verifikation von ggt) (2) • Ad (a) – Induktionsanfang: = = = ggt 0 n n max{ k | k teilt n} max{ k | k teilt 0 und n} ©Arnd Poetzsch-Heffter TU Kaiserslautern 518 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (Verifikation von ggt) (3) • Ad (b) – Induktionsschritt: Voraussetzung: Sei m gegeben. Für alle Paare (k , n) mit k ≤ m gilt: ggt ist korrekt für (k , n) Zeige: Für alle n gilt: ggt ist korrekt für (m + 1, n)! ggt (m+1) n = (* Deklaration von ggt *) ggt (n mod (m+1)) (m+1) = (* n mod (m+1) ≤ m und Induktionsvoraussetzung *) max { k | k teilt (n mod (m+1)) und (m+1) } = (* Vorueberlegung *) max { k | k teilt (m+1) und n} QED. ©Arnd Poetzsch-Heffter TU Kaiserslautern 519 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkung: So wie Testen Testfälle oder Prüfprädikate voraussetzt, so benötigt Verifikation mit Beweis eine Spezifikation oder andere Beschreibung der zu zeigenden Eigenschaften. ©Arnd Poetzsch-Heffter TU Kaiserslautern 520 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Äquivalente Funktionsdeklarationen: Wir haben gesehen, dass unterschiedliche Funktionsdeklarationen das gleiche Ein- und Ausgabeverhalten haben können. Zum Beispiel kann eine Deklaration in einer “aufwendigeren” Rekursionsformen einfacher zu lesen sein, aber eine entsprechende lineare oder repetitive Funktionsdeklaration performanter sein. Transformiert man die eine in die andere Form ist es wichtig, die Äquivalenz zu zeigen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 521 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkung: • Semantisch äquivalente Programme können große Unterschiede bei der Effizienz aufweisen. • Bedeutungserhaltende Transformationen spielen in der Programmoptimierung und dem Refactoring von Software eine wichtige Rolle. Korrektheit von Transformationen: Wir transformieren die rekursive Funktionsdeklarationen in einfachere Deklarationen und zeigen Äquivalenz. ©Arnd Poetzsch-Heffter TU Kaiserslautern 522 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (linear → repetitiv) Wir betten die Fakultätsfunktion fac :: Integer -> Integer fac n = if n == 0 then 1 else n * fac (n -1) in eine Funktion mit einem weiteren Argument ein, das Zwischenergebnisse aufsammelt: facrep :: Integer -> Integer -> Integer facrep n res = if n==0 then res else facrep (n -1) (res*n) Damit lässt sich die Fakultät auch definieren als: fac1 n = facrep n 1 Dadurch wurde eine lineare Rekursion in eine repetitive Form gebracht. ©Arnd Poetzsch-Heffter TU Kaiserslautern 523 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Lemma: Für die obigen Deklarationen von fac und facrep gilt: ∀n mit n ≥ 0, r mit r ≥ 0 : insbesondere: ∀n mit n ≥ 0 : ©Arnd Poetzsch-Heffter (fac n) ∗ r = facrep n r fac n = fac1 n TU Kaiserslautern 524 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beweis: mittels Parameterinduktion nach n Induktionsanfang: Zu zeigen: ∀r mit r ≥ 0 : (fac 0) ∗ r = facrep 0 r = = = = (fac 0) * r (* Deklaration von fac *) (if 0==0 then 1 else 0 * (fac (0 -1))) * r (* Ausdrucksauswertung *) r (* Ausdrucksauswertung *) if 0==0 then r else facrep (0 -1) (r*0) (* Deklaration von facrep *) facrep 0 r ©Arnd Poetzsch-Heffter TU Kaiserslautern 525 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beweis: (Forts.) Induktionsschritt: k → k + 1 Induktionsvoraussetzung: Für k ≥ 0 : ∀r mit r ≥ 0 : (fac k)*r =facrep k r Zu zeigen: fac(k+1)* r = facrep (k+1)r fac(k+1) * r = (* Deklaration von fac *) (if k+1==0 then 1 else (k+1) * fac (k+1 -1)) * r = (* Ausdrucksumformung *) (k+1) * (fac k) *r ©Arnd Poetzsch-Heffter TU Kaiserslautern 526 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beweis: (Forts.) (k+1) * (fac k) *r = (* Kommutativitaet + Assoziativitaet der Multipl . *) (( fac k)* r) * (k+1) = (* Induktionsvoraussetzung *) ( facrep k r) * (k+1)) = (* Ausdrucksumformung *) if = k+1==0 then r else facrep k (r*(k+1)) (* Deklaration von facrep *) facrep (k+1) r ©Arnd Poetzsch-Heffter TU Kaiserslautern 527 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiel: (kaskadenartig → linear) Tranformation der Fibonacci-Funktion fib in eine lineare Form. Idee: Führe zwei zusätzliche Parameter ein, in denen ausgehend von 0 und 1 die Zwischenergebnisse berechnet werden. Es soll also gelten: fib1 n = fibemb n 0 1 Wir definieren fibemb zu: fibemb 0 letzt res = 0 fibemb 1 letzt res = res fibemb n letzt res = fibemb (n -1) res ( letzt +res) Beweis: mittels Parameterinduktion nach n. ©Arnd Poetzsch-Heffter TU Kaiserslautern 528 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Terminierung Zentrale Eigenschaft einer Funktionsdeklaration ist, dass ihre Anwendung auf die zulässigen Parameter terminiert. Diese Eigenschaft gilt für alle nicht-rekursiven Funktionsdeklarationen, die sich nur auf terminierende Funktionen abstützen. Bei rekursiven Funktionsdeklarationen muss die Terminierung nachgewiesen werden. Idee: Die Parameter sollten bei jedem rekursiven Aufruf “kleiner” werden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 529 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiele: ("kleiner werdende" Parameter) 1. foldrplus [ ] = foldrplus (x:xs) = 0 x + foldrplus xs 2. einfuegen [] z ix = einfuegen xs z 0 = einfuegen (x:xs) z ix = [z] z:xs einfuegen xs z (ix -1) 3. foo m n = if m<n then m `div` n else foo (m-n) n ©Arnd Poetzsch-Heffter TU Kaiserslautern 530 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Definition: (Ordnung) Eine Teilmenge R von M × N heißt eine (binäre) Relation. Gilt M = N, dann nennt man R homogen. Eine homogene Relation heißt: • reflexiv, wenn für alle x ∈ M gilt: (x, x) ∈ R • antisymmetrisch, wenn für alle x, y ∈ M gilt: wenn (x, y ) ∈ R und (y , x) ∈ R, dann x = y • transitiv, wenn für alle x, y , z ∈ M gilt: wenn (x, y ) ∈ R und (y , z) ∈ R, dann (x, z) ∈ R ©Arnd Poetzsch-Heffter TU Kaiserslautern 531 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Definition: (Ordnung) (2) Eine reflexive, antisymmetrische und transitive homogene Relation auf M × M heißt eine (partielle) Ordnungsrelation. Eine Menge M mit einer Ordnungsrelation R heißt eine (partielle) Ordnung. Meist benutzt man Infixoperatoren wie ≤ (oder ⊆) zur Darstellung der Relation und schreibt x ≤ y statt (x, y ) ∈ R ©Arnd Poetzsch-Heffter TU Kaiserslautern 532 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Definition: (Kette, noethersche Ordnung) Sei (M, ≤) eine Ordnung. Eine Folge ϕ : N → M heißt eine (abzählbar unendliche) aufsteigende Kette, wenn für alle i ∈ N gilt: ϕ(i) ≤ ϕ(i + 1) (absteigende Kette: entsprechend). Eine Kette ϕ wird stationär, falls es ein j ∈ N gibt, so dass ϕ(j) = ϕ(j + k ) für alle k ∈ N ©Arnd Poetzsch-Heffter TU Kaiserslautern 533 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Definition: (Kette, noethersche Ordnung) (2) Sei N eine Teilmenge von M. x ∈ N heißt: • größtes Element von N, wenn ∀y ∈ N gilt: y ≤ x. • kleinstes Element von N, wenn ∀y ∈ N gilt: x ≤ y . • maximales Element von N, wenn ∀y ∈ N gilt: x ≤ y impliziert x = y . • minimales Element von N, wenn ∀y ∈ N gilt: y ≤ x impliziert x = y . Eine Ordnung (M, ≤) heißt noethersch, wenn jede nicht-leere Teilmenge von M ein minimales Element besitzt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 534 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Lemma: Eine Ordnung ist genau dann noethersch, wenn jede absteigende Kette stationär wird. Beweis: (siehe Theorievorlesung) ©Arnd Poetzsch-Heffter TU Kaiserslautern 535 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Terminierungskriterium: Sei f : S → T eine rekursive Funktionsdeklaration mit formalem Parameter n und sei P die Menge der zulässigen Parameter von f . Jede Anwendung von f auf Elemente von P terminiert, • wenn es eine noethersche Ordnung (M, ≤) • und eine Abb. δ : P → M gibt, • so dass für jede rekursive Anwendung f (G(n)) im Rumpf der Deklaration gilt: i) G(n) ist ein zulässiger Parameter, d.h. G(n) ∈ P. ii) Die aktuellen Parameter werden echt kleiner, d.h. δ(G(n)) < δ(n) ©Arnd Poetzsch-Heffter TU Kaiserslautern 536 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkung: • Da die aktuellen Parameter nur endlich oft echt kleiner werden können (und dann stationär werden), garantiert das obige Kriterium die Terminierung. • Um die Terminierung nachzuweisen, muss man also eine geeignete noethersche Ordnung und eine geeignete Abbildung δ finden. • Ist der Argumentbereich bereits noethersch geordnet, kann δ selbstverständlich auch die Identität sein. ©Arnd Poetzsch-Heffter TU Kaiserslautern 537 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiele: (Terminierungsbeweis) 1. foldrplus xs = if null xs then 0 else ( headd xs) + foldrplus (tail xs) P ist die Menge aller endlichen Listen über Integer. Noethersche Ordnung (N, ≤). Als δ wähle die Funktion länge (Länge einer Liste). Zu zeigen: i) tail xs ist ein zulässiger Parameter: ok. ii) länge (tail xs) < länge xs: ok. ©Arnd Poetzsch-Heffter TU Kaiserslautern 538 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiele: (Terminierungsbeweis) (2) 2. einfuegen ([] ,z,ix) = einfuegen (xs ,z ,0) = einfuegen (x:xs ,z,ix) = [z] z:xs einfuegen (xs ,z,ix -1) P ist die Menge aller Tripel aus (a,[a],Int). Noethersche Ordnung (N, ≤). Als δ wähle die Funktion längefst, die länge auf die erste Komponente anwendet. Zu zeigen: i) (xs,z,ix-1) ist ein zulässiger Parameter : ok. ii) längefst(xs,z,ix-1) < längefst(x:xs,z,ix) : ok. ©Arnd Poetzsch-Heffter TU Kaiserslautern 539 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiele: (Terminierungsbeweis) (3) Bemerkung: Hätte man stattdessen für δ die Selektion auf die dritte Komponente gewählt, hätte man Terminierung nur für eine kleinere Menge zulässiger Parameter zeigen können, nämlich z.B. für Parametertripel (xl,el,ix) mit ix ≥ 0. ©Arnd Poetzsch-Heffter TU Kaiserslautern 540 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiele: (Terminierungsbeweis) (4) 3. foo(m,n) = if m<n then m `div` n else foo (m-n,n) P ist die Menge aller Paare (m, n) aus (Integer,Integer) mit (m < n und n , 0) oder n > 0. Noethersche Ordnung (N, ≤). δ : Z × Z → N mit ( δ(m, n) = ©Arnd Poetzsch-Heffter 0 , falls m < n m − n + 1 , falls m ≥ n TU Kaiserslautern 541 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Beispiele: (Terminierungsbeweis) (5) Zu zeigen: i) (m − n, n) ist ein zulässiger Parameter: ok. ii) Unter der Voraussetzung m ≥ n und n > 0: 1. Fall: m − n ≥ n: δ(m − n, n) = m − n − n + 1 < m − n + 1 = δ(m, n) 2. Fall: m − n < n: δ(m − n, n) = 0 < m − n + 1 = δ(m, n) ©Arnd Poetzsch-Heffter TU Kaiserslautern 542 3. Funktionales Programmieren 3.4 Semantik, Testen und Verifikation Bemerkung: • Terminierungsbeweise sind bei der Entwicklung von Qualitätssoftware sehr wichtig, und zwar unabhängig vom verwendeten Modellierungs- bzw. Programmierparadigma. • Es sollte zur Routine der Softwareentwicklung, gehören, den zulässigen Parameterbereich festzulegen und dafür Terminierung zu zeigen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 543