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