Deklarative Programmierung Prof. Dr. Sibylle Schwarz HTWK Leipzig, Fakultät IMN Gustav-Freytag-Str. 42a, 04277 Leipzig Zimmer Z 411 (Zuse-Bau) http://www.imn.htwk-leipzig.de/~schwarz [email protected] Sommersemester 2015 Motivation . . . there are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult. Tony Hoare, 1980 ACM Turing Award Lecture Programmierparadigmen Abstraktionsstufen (zeitliche Entwicklung): I Programm = Maschinencode I menschenlesbare symbolische Darstellung (Assembler) I Beschreibung von Programmablauf- und Datenstrukturen (imperative und objektorientierte Sprachen) I Beschreibung der Aufgabenstellung (deklarative Sprachen, z.B. funktional, logisch, Constraints) Unterschied besteht darin, wie detailliert das Programm das Lösungsverfahren beschreiben muss. Formen der deklarativen Programmierung Grundidee: Jedes Programm ist ein mathematisches Objekt mit einer bekannten wohldefinierten Semantik funktionale Programmierung (z.B. Haskell, ML, Lisp): Programm: Menge von Funktions-Definitionen (Gleichungen zwischen Termen) Ausführung: Pattern matching, Reduktion (Termersetzung) logische Programmierung (Prolog): Programm: Menge logischer Formeln (Horn-Klauseln) Ausführung: Unifikation, SLD-Resolution funktional-logische Programmierung (z.B. Mercury, Curry): Kombination funktionaler und logischer Konzepte Constraint-Programmierung: Programm: Menge von Constraints (z.B. Gleichungen, Ungleichungen, logische Formeln) Ausführung: Constraint-Löser (abhängig vom Constraint-Bereich) Beispiele: Constraints: Menge linearer Gleichungen Constraint-Löser: Gauß-Algorithmus Constraints: aussagenlogische CNF Constraint-Löser: SAT-Solver Beispiele I I I funktionale Programmierung: foldr (+) 0 [1,2,3] foldr f z l = case l of [] -> z ; (x:xs) -> f x (foldr f z xs) logische Programmierung: append(A,B,[1,2,3]). append([],YS,YS). append([X|XS],YS,[X|ZS]):-append(XS,YS,ZS). Constraint-Programmierung (set-logic QF_LIA) (set-option :produce-models true) (declare-fun a () Int) (declare-fun b () Int) (assert (and (>= a 5) (<= b 30) (= (+ a b) 20))) (check-sat) (get-value (a b)) Deklarative vs. imperative Programmierung deklarativ (beschreibend) Programm: Repräsentation einer Aufgabe Programmelemente: Ausdrücke (Terme), Formeln, Gleichungen Programmierung: Modellierung der Aufgabe Ausführung: Lösung des beschriebenen Problems durch Standardverfahren z.B. logisches Schließen, Umformung von Ausdrücken imperativ zustandsorientiert (von-Neumann-Typ) Programm: Repräsentation eines Algorithmus Programmelemente: Ausdrücke und Anweisungen Programmierung: Modellierung eines Verfahrens zur Lösung einer Aufgabe Ausführung des Lösungsverfahrens durch schrittweise Zustandsänderungen (Speicherbelegung) Definition deklarativ: jedes (Teil-)Programm/Ausdruck hat einen Wert . . . und keine weitere (versteckte) Wirkung. Werte können sein: I „klassische“ Daten (Zahlen, Listen, Bäume. . . ) I Funktionen (Sinus, . . . ) I Aktionen (Datei schreiben, . . . ) Softwaretechnische Vorteile der deklarativen Programmierung: Beweisbarkeit : Rechnen mit Programmen wie in der Mathematik mit Termen Sicherheit : es gibt keine Nebenwirkungen und Wirkungen sieht man bereits am Typ Wiederverwendbarkeit : durch Entwurfsmuster (= Funktionen höherer Ordnung) Effizienz : durch Programmtransformationen im Compiler Parallelisierbarkeit : durch Nebenwirkungsfreiheit Beispiel Spezifikation/Test import Test.SmallCheck append :: [t] -> [t] -> [t] append x y = case x of [] -> y h : t -> h : append t y associative f = \ x y z -> f x (f y z) == f (f x y) z test1 = smallCheck (associative (append::[Int]->[Int]->[Int])) Übung: Kommutativität (formulieren und testen) Beispiel Verifikation app :: [t] -> [t] -> [t] app x y = case x of [] -> y h : t -> h : app t y zu beweisen: app x (app y z) == app (app x y) z Beweismethode: Induktion nach x. I Induktionsanfang: x == [] . . . I Induktionsschritt: x == h : t . . . Deklarative Programmierung in der Lehre funktionale Programmierung: diese Vorlesung logische Programmierung: in LV Künstliche Intelligenz Constraint -Programmierung: als Master-Wahlfach Beziehungen zu weiteren LV: Voraussetzungen I Bäume, Terme (Alg.+DS, TGI) I Logik (TGI, Digitaltechnik, Softwaretechnik) Anwendungen: I Softwarepraktikum I weitere Sprachkonzepte in LV Prinzipien v. Programmiersprachen I LV Programmverifikation (vorw. f. imperative Programme) Gliederung der Vorlesung I I I I I I Terme, Termersetzungssysteme algebraische Datentypen, Pattern Matching, Rekursive Datenypen, Rekursionsschemata Funktionen (polymorph, höherer Ordnung), Lambda-Kalkül Typklassen zur Steuerung der Polymorphie Bedarfsauswertung, unendl. Datenstrukturen Organisation der Lehrveranstaltung I I jede Woche eine Vorlesung Hausaufgaben: I I I schriftliche Übungen, autotool jede Woche eine Übung / Praktikum I I I Beispiele, Besprechung der schriftlichen Aufgaben, autotool Prüfungsvorleistung: regelmäßiges (d.h. innerhalb der jeweiligen Deadline) und erfolgreiches (ingesamt ≥ 50% der Pflichtaufgaben) Bearbeiten von Übungsaufgaben. Prüfung: Klausur (ohne Hilfsmittel) Literatur Skript voriges Semester: http://www.imn.htwk-leipzig.de/~waldmann/edu/ss14/ fop/folien/main Folien aktuelles Semester: http: //www.imn.htwk-leipzig.de/~schwarz/lehre/ss15/dp Bücher: I Graham Hutton: Programming in Haskell, Cambridge 2007 I Klassiker (englisch): http://haskell.org/haskellwiki/Books I deutsch: I Peter Pepper und Petra Hofstedt: Funktionale Programmierung. Sprachdesign und Programmiertechnik Springer 2006 I Manuel Chakravarty und Gabriele Keller: Einführung in die Programmierung mit Haskell Pearson 2004 online: http://www.haskell.org/ Informationen, Download, Dokumentation, Tutorials, . . . Werkzeug und Stil Die Grenzen meiner Sprache bedeuten die Grenzen meiner Welt. Ludwig Wittgenstein speziell in der Informatik: We are all shaped by the tools we use, in particular: the formalisms we use shape our thinking habits, for better or for worse, and that means that we have to be very careful in the choice of what we learn and teach, for unlearning is not really possible. (Many years ago, if I could use a new assistant, one prerequisite would be No prior exposure to FORTRAN", and at high schools in Siberia, the teaching of BASIC was not allowed.) Edsger W. Dijkstra aus E. W. Dijkstra Archive http://www.cs.utexas.edu/~EWD/ Konzepte und Sprachen Funktionale Programmierung ist ein Konzept. Realisierungen: I in prozeduralen Sprachen: I I I I in OO-Sprachen: Befehlsobjekte Multi-Paradigmen-Sprachen: I I Unterprogramme als Argumente (in Pascal) Funktionszeiger (in C) Lambda-Ausdrücke in C#, Scala, Clojure funktionale Programmiersprachen (LISP, ML, Haskell) Die Erkenntnisse sind sprachunabhängig. I A good programmer can write LISP in any language. I Learn Haskell and become a better Java programmer. Geschichte ab ca. 1930 ab ca. 1950 ab ca. 1960 ab ca. 1970 ab 1987 Alonzo Church John McCarthy Peter Landin John Backus Robin Milner David Turner λ-Kalkül LISP ISWIM FP ML Miranda Haskell Warum Haskell? I deklarativ, Nähe zum (mathematischen) Modell I keine Nebenwirkungen (klare Semantik) I Funktionen sind Daten (Funktionen höherer Ordnung) I starkes Typsystem I Typklassen I lazy evaluation (ermöglicht Rechnen mit unendlichen Datenstrukturen) I kompakte Darstellung (kurze Programme) I Modulsystem Entwicklung von Haskell-Programmen Haskell-Interpreter: ghci, Hugs Haskell-Compiler: ghc Entwicklungsumgebungen: I http://leksah.org/ I http://eclipsefp.sourceforge.net/ I http://www.haskell.org/visualhaskell/ alles kostenlos und open source Real Programmers (http://xkcd.com/378/) Wiederholung: Terme Signatur (funktional) Σ (ΣF ) ist Menge von Funktionssymbolen mit Stelligkeiten Term t = f (t1 , . . . , tk ) in Signatur Σ ist I Funktionssymbol der Stelligkeit k : (f , k ) ∈ Σ der Stelligkeit k mit Argumenten t1 , . . . , tk , die selbst Terme sind. Term(Σ, X) = Menge aller Terme über Signatur Σ mit Individuenvariablen aus X Graphentheorie: ein Term ist ein gerichteter, geordneter, markierter Baum Datenstrukturen: I Funktionssymbol = Konstruktor, I Term = Baum Beispiele: Signatur, Terme I Signatur: Σ1 = {Z /0, S/1, f /2} Elemente aus Term(Σ1 ): Z (), S(S(Z ())), f (S(S(Z ())), Z ()) I Signatur: Σ2 = {E/0, A/1, B/1} Elemente aus Term(Σ2 ): . . . Haskell-Programme Programm: Menge von Funktions-Definitionen Gleichungen zwischen Termen Ausdruck: Term Ausführung: Auswertung des Ausdruckes (Bestimmung seines Wertes) Pattern matching, Reduktion, (Termersetzung) Semantik: Funktion von Eingabe (Ausdruck) auf Ausgabe (Wert) I keine Variablen, also keine Programmzustände (kein Aufruf-Kontext) I Wert jeder Funktion(sanwendung) hängt ausschließlich von den Werten der Argumente ab Syntax Ausdrücke : Terme z.B. 2 + x * 7 oder double 2 Funktionsdefinition : Gleichung zwischen zwei Ausdrücken z.B. inc x = x + 1 Programm : I I Folge (Liste) von Funktionsdefinitionen Ausdruck Ausdrücke Ausdruck = Term (Baumstruktur) Jeder Ausdruck hat I einen Typ und I einen Wert Berechnung des Wertes durch schrittweise Reduktion (Termersetzung) Beispiele Ausdruck 7 hat I den Typ Int I den Wert 7 Ausdruck 3 * 7 + 2 hat I den Typ Int I den Wert . . . Reduktion : (rekursive) Berechnung des Wertes Funktionsdeklarationen double :: Int -> Int double x = x + x (Typdeklaration) (Funktionsdefinition) Ausdruck double 3 hat I den Typ Int I den Wert 6 Ausdruck double (double 3) hat I den Typ Int I den Wert . . . Ausdruck double hat I den Typ Int -> Int I den Wert x 7→ x + x (mathematische Notation) λx.(x + x) (λ-Kalkül) Was bisher geschah I deklarative Programmierung I I I funktionale Programmierung in Haskell: I I I I funktional: Programm: Menge von Termgleichungen, Term Auswertung: Pattern matching, Termumformungen logisch: Programm: Menge von Regeln (Horn-Formeln), Formel Auswertung: Unifikation, Resolution nebenwirkungsfrei lazy evaluation (ermöglicht unendliche Datentypen) kompakte Darstellung Praktikum: Termersetzung, ghci, Prelude Bezeichnungen für Teilterme Position : Folge von natürlichen Zahlen (bezeichnet einen Pfad von der Wurzel zu einem Knoten) Beispiel: für Signatur Σ = {(g, 2), (f , 1), (c, 0)} und Term t = f (g(f (f (c)), c)) ∈ TermΣ, ∅ ist [0, 1] eine Position in t, aber [1], [0, 1, 0], [0, 0, 1] nicht X Pos(t) Menge aller Positionen des Terms t ∈ Term(Σ, ) (rekursive) Definition: für t = f (t1 , . . . , tk ) gilt Pos(t) = {[]} ∪ {[i − 1] ++{p | i ∈ {1, . . . , k } ∧ p ∈ Pos(ti )}. dabei bezeichnen: I [] die leere Folge, I [i] die Folge der Länge 1 mit Element i, I ++ den Verkettungsoperator für Folgen Operationen mit (Teil)Termen t[p] : Teilterm von t an Position p Beispiele: I f (g(f (f (c)), c))[0, 0] = f (f (c)) I f (g(f (f (c)), c))[0, 1] = . . . (induktive) Definition (über die Länge von p): IA p = [] : t[] = t IS p = i ++p0 : f (t1 , . . . , tn )[p] = ti [p0 ] t[p := s] : wie t, aber mit Term s statt t[p] an Position p Beispiele: I f (g(f (f (c)), c))[[0, 0] := c] = f (g(c, c)) I f (g(f (f (c)), c))[[0, 1] := f (c)] = . . . (induktive) Definition (über die Länge von p): . . . Operationen mit Variablen in Termen I X X Menge Term(Σ, ) aller Terme über Signatur Σ mit Variablen aus Beispiel: Σ = {Z /0, S/1, f /2}, = {y }, f (Z (), y ) ∈ Term(Σ, ). X X X → Term(Σ, X) I Substitution σ: partielle Abbildung Beispiel: σ1 = {(y , S(Z ()))} I eine Substitution auf einen Term anwenden: tσ: Intuition: wie t, aber statt v ∈ immer σ(v ) Beispiel: f (Z (), y )σ1 = f (Z (), S(Z ())) Definition durch Induktion über t X Termersetzungssysteme Daten : Terme (ohne Variablen) Regel : Paar (l, r ) von Termen mit Variablen Programm R: Menge von Regeln Bsp: R = {(f (Z (), y ), y ), (f (S(x), y ), S(f (x, y )))} Relation →R : Menge aller Paare (t, t 0 ) mit I es existiert (l, r ) ∈ R I es existiert Position p in t I es existiert Substitution σ : (var(l) ∪ var(r )) → Term(Σ) I so dass t[p] = lσ und t 0 = t[p := r σ]. Termersetzungssysteme als Programme I →R beschreibt einen Schritt der Rechnung von R, I transitive Hülle →∗R beschreibt Folge von Schritten. I Resultat einer Rechnung ist Term in R-Normalform (ohne →R -Nachfolger) Dieses Berechnungsmodell ist im allgemeinen nichtdeterministisch R1 = {C(x, y ) → x, C(x, y ) → y } (ein Term kann mehrere →R -Nachfolger haben, ein Term kann mehrere Normalformen erreichen) nicht terminierend R2 = {p(x, y ) → p(y , x)} (es gibt eine unendliche Folge von →R -Schritten, es kann Terme ohne Normalform geben) Konstruktor-Systeme Für TRS R über Signatur Σ: Symbol s ∈ Σ heißt definiert , wenn ∃(l, r ) ∈ R : l[] = s(. . .) Konstruktor , sonst Das TRS R heißt Konstruktor-TRS, falls die definierten Symbole links nur in den Wurzeln vorkommen (rechts egal) Übung: diese Eigenschaft formal spezifizieren Beispiele: I R1 = {a(b(x)) → b(a(x))} über Σ1 = {a/1, b/1}, I R2 = {f (f (x, y ), z) → f (x, f (y , z))} über Σ2 = {f /2}: definierte Symbole? Konstruktoren? Konstruktor-System? Funktionale Programme sind ähnlich zu Konstruktor-TRS. Selbsttest-Übungsaufgaben zur Klausur-Vorbereitung (statt Praktikum diese Woche) zu I Signaturen I Termen I Substitutionen I Termersetzungsysstemen I Normalformen unter http://www.imn.htwk-leipzig.de/~waldmann/edu/ ss14/fop/folien/main/node28.html Funktionale Programme . . . sind spezielle Term-Ersetzungssysteme. Beispiel: Signatur: S einstellig, Z nullstellig, f zweistellig. Ersetzungssystem {f (Z , y ) → y , f (S(x), y ) → S(f (x, y ))}. Startterm f (S(S(Z )), S(Z )). entsprechendes funktionales Programm: data N = Z | S N f :: N -> N -> N f x y = case x of { Z -> y ; S x’ -> S (f x’ y) } Aufruf: f (S (S Z)) (S Z) Auswertung = Folge von Ersetzungsschritten →∗R Resultat = Normalform (hat keine →R -Nachfolger) data und case typisches Vorgehen beim Programmieren einer Funktion f :: T -> ... Für jeden Konstruktor des Datentyps data T = C1 ... | C2 ... schreibe einen Zweig in der Fallunterscheidung f x = case x of C1 ... -> ... C2 ... -> ... Peano-Zahlen data N = Z | S N deriving Show plus :: N -> N -> N plus x y = case x of Z -> y S x’ -> S (plus x’ y) Beispiel (Tafel): Multiplikation Was bisher geschah I Wiederholung Signatur, Term I Termersetzungssysteme (TRS) I Konstruktoren, definierte Symbole I Konstruktor-Systeme I funktionale Programmierung Programm: Menge von Termgleichungen (TRS) Ausdruck (dessen Wert zu bestimmen ist): Term Auswertung: Pattern matching, Termumformungen Haskell: I I I I nebenwirkungsfrei kompakte Darstellung Praktikum: ghci, Prelude, Typen, Hoogle Vordefinierte Haskell-Datentypen einfache Datentypen, z.B. Int ganze Zahlen (feste Länge) Integer ganze Zahlen (beliebige Länge) Bool Wahrheitswerte (False, True) Char ASCII-Symbole Float, Double zusammengesetzt (Typkonstruktoren): I Tupel (a, b), (a, b, c), (a1, a2, ...) z.B. (1, True, ’B’) :: (Int, Bool, Char) I Listen (polymorph) [a], z.B. [3,5,2] :: [Int], [[’I’, ’N’],[’B’]] :: [[Char]] I String = [Char], z.B. "INB" = [’I’,’N’,’B’] Definition von Funktionen Programmstrukturen: I Verzweigung (Fallunterscheidung) I Rekursion Beispiel: sumto :: Int -> Int sumto n = if n < 0 then 0 else n + sumto (n-1) Funktionsdeklarationen (Wiederholung) add :: Int -> Int -> Int (Typdeklaration) add x y = x + y (Funktionsdefinition) Ausdruck add 3 5 hat I den Typ Int I den Wert 8 Ausdruck add (add 3 5) 1 hat I den Typ Int I den Wert . . . Ausdruck add hat I den Typ Int -> Int -> Int I den Wert (x, y ) 7→ x + y (mathematische Notation) λx.λy .(x + y ) (λ-Kalkül) Ausdruck add 3 hat I den Typ Int -> Int I den Wert y 7→ 3 + y (mathematische Notation) λy .(3 + y ) (λ-Kalkül) (partielle Anwendung von add) Typinferenz Typinferenzregel: f :: A → B e :: A f e :: B Man bemerke die Analogie zur logischen Inferenzregel Modus Ponens: Beispiel: Typ von add 3, add 3 5 A→B B A Beispiele Typinferenz True :: Bool False :: Bool neg :: Bool -> Bool neg True = False neg False = True Typ von neg True, neg (neg True) len :: [a] -> Int gerade :: Int -> Bool Typ von [1,2,3], len [1,2,3], gerade ( len [1,2,3] ) Currying Idee: Jede Funktion mit mehreren Argumenten lässt sich als geschachtelte Funktionen mit je einem Argument auffassen (und aufschreiben) Beispiel: Die folgenden Zeilen definieren dieselbe Funktion vom Typ g :: Int -> Int -> Bool I g m n = m < n I g m = \ n -> m < n (g m) = λn.(m < n) I g = \ m n -> m < n g = λm.λn.(m < n) mit Argument-Tupel (Achtung anderer Typ): g’ :: (Int, Int) -> Bool g’ (m, n) = m < n in mathematischer Notation: zweistellig: C (A×B) ist isomorph zu (C B )A (A1 ×···×An−1 ) (n − 1)-stellig: An A1 A ist isomorph zu · · · An n−1 · · · Konstruktion zusammengesetzter Datentypen Operationen: I (kartesisches) Produkt I Vereinigung (Fallunterscheidung) z.B. Aufzählungstypen I Rekursion, z.B. Listen, Bäume, Peano-Zahlen I Potenz, Funktionen Algebraische Datentypen data Foo = Foo { bar :: Int, baz :: String } deriving Show Bezeichnungen (benannte Notation): I data Foo ist Typname I Foo { .. } ist Konstruktor I bar, baz sind Komponenten x :: Foo x = Foo { bar = 3, baz = "hal" } Bezeichnungen (positionelle Notation) data Foo = Foo Int String y = Foo 3 "bar" Mathematisch: Produkt Foo = Int × String Datentyp mit mehreren Konstruktoren Beispiel (selbst definiert): data T = A { foo :: Int } | B { bar :: String } deriving Show Beispiel (in Prelude vordefiniert) data Bool = False | True data Ordering = LT | EQ | GT Mathematisch: (disjunkte) Vereinigung Bool = { False } ∪ { True } Fallunterscheidung, Pattern Matching data T = A { foo :: Int } | B { bar :: String } Fallunterscheidung: f :: T -> Int f x = case x of A {} -> foo x B {} -> length $ bar x Pattern Matching (Bezeichner n,l werden lokal gebunden): f :: T -> Int f x = case x of A { foo = n } -> n B { bar = l } -> length l Rekursive Datentypen Wiederholung Peano-Zahlen: data Nat = Z | S Nat Menge aller Peano-Zahlen: Nat = {Z} ∪ {Sn | n ∈ Nat} Addition: add :: Nat -> Nat -> Nat add Z y = y add ( S x ) y = S ( add x y ) oder add :: Nat -> Nat -> Nat add x y = case x of Z -> y S x’ -> S ( add x’ y ) Definition weiterer Operationen: Multiplikation, Potenz Wiederholung ADT Nat Sorten: N (natürliche Zahlen) Signatur: Z S add mult ... :: :: :: :: N N -> N N -> N -> N N -> N -> N Axiome: ∀x ∀y ∀u: add Z x add x y add x ( add y u ) mult Z x mult ( S Z ) x mult x y mult x ( mult y u ) ... = = = = = = = x = add x Z add y x add ( add x y ) u Z = mult x Z x = mult x ( S Z ) mult y x mult ( mult x y ) u Nachweis durch strukturelle Induktion (Tafel) Wiederholung Strukturelle Induktion Induktive Definition strukturierter Daten (rekursive Datentypen): IA: Basisfälle IS: rekursive Fälle, Vorschrift zur Konstruktion zusammengesetzter Daten Induktive Definition von Funktionen über strukturierten Daten: IA: Definition des Funktionswertes für Basisfälle IS: Berechnung des Funktionswertes der zusammengesetzten Daten aus den Funktionswerten der Teile Prinzip der strukturellen Induktion zum Nachweis einer Aussage A über strukturierte Daten: IA: Nachweis, dass A für alle Basisfälle gilt I Hypothese (Voraussetzung): A gilt für Teildaten IS: I Behauptung: A gilt für aus Teildaten zusammengesetzte Daten I Induktionsbeweis: Nachweis, dass Behauptung aus Hypothese folgt. Was bisher geschah I Deklarative vs. imperative Programmierung I Funktionale Programmierung: Programm: Menge von Gleichungen von Termen (Konstruktor-System) Ausdruck hat Typ und Wert (zu berechnen) Ausführung: Pattern matching, Termersetzung Haskell: I Algebraische Datentypen und Pattern Matching I Rekursive Datentypen (Peano-Zahlen) I Rekursive Funktionen I strukturelle Induktion Wiederholung Haskell-Typen I vordefinierte Typen, z. B. Bool, Char, Int, Float, String, ... I Typvariablen, z.B. a, b ,... Konstruktion zusammengestzter Typen: I I I I I selbstdefiniert constr typ1 ... typn Listen-Konstruktor [ typ ] Tupel-Konstruktor ( typ1, ..., typn ) Funktions-Konstruktor typ1 -> typ2 Algebraische Datentypen – Wiederholung Operationen: I Produkt A × B Beispiel: data Punkt = Punkt { x :: Float, y :: Float} data Kreis = Kreis { mp :: Punkt, radius :: Float } I (disjunkte) Vereinigung A ∪ B Beispiel Wahrheitswerte (vordefiniert) data Bool = True | False data Shape = Circle { mp :: Punkt, radius :: Float } | Rect { ol, ur :: Punkt} umfang :: Shape -> Float umfang s = case s of Circle {} -> 2 * pi * ( radius s ) Rect ol ur -> ... I Potenz AB = {f : B → A} z.B. gerade_laenge :: String -> Bool Algebraische Datentypen – Beispiel data HR = N | O | S | W data Turn = Links | Rechts | Um dreh :: Turn -> HR -> HR dreh Rechts x = case x of N -> O O -> S S -> W W -> N dreh Links x = ... drehs :: [ Move ] -> HR -> HR drehs ( m : ms ) x = dreh m ( drehs ms x ) Algebraische Datentypen – Beispiele (Produkt) data Pair a b = Pair a b data Either a b = Left a | Right b (Vereinigung) data Maybe a = Nothing | Just a (Vereinigung) Binärbäume (rekursiv): data Bin a = Leaf | Branch (Bin a) a (Bin a) Spezialfall Listen (Unärbäume): data List a = Nil | Cons a (List a) Bäume (mit beliebigen Knotengraden): data Tree a = Node a (List (Tree a)) Typsynonyme (Um-)Benennung vorhandener Typen (meist als Kurzform) Beispiel: type type type type String = [ Char ] Name = String Telefonnummer = Int Telefonbuch = [ ( Name , Telefonnummer ) ] nummern :: Name -> Telefonbuch -> [ Telefonnummer ] nummern name [] = [] nummern name ( ( n , t ) : rest ) ... allgemeiner: Wörterbücher type Woerterbuch a b = [ ( a, b ) ] rekursive Typen sind nicht als Typsynonym definierbar Typsynonyme – Beispiel Zwei-Personen-Brettspiel (auf rechteckigem Spielfeld) I Spieler ziehen abwechselnd I Jeder Spieler hat Spielsteine seiner Farbe auf mehreren Positionen des Spielfeldes Spielfeld: type Feld = ( Int, Int ) type Belegt = [ Feld ] type Spieler = Bool Spielzustand: type Zustand = ( Belegt, Belegt, Spieler ) Spiel: type Spiel = [ Zustand ] Polymorphie nicht polymorphe Typen: tatsächlicher Argumenttyp muss mit dem deklarierten Argumenttyp übereinstimmen: f :: A → B e :: A (f e) :: B polymorphe Typen: Typ von f :: A -> B und Typ von e :: A’ können Typvariablen enthalten. A und A’ müssen unfizierbar (eine gemeinsame Instanz besitzen) aber nicht notwendig gleich sein. σ = mgu(A, A0 ) allgemeinster Unifikator Typ von f wird dadurch spezialisiert auf σ(A) → σ(B) Typ von e wird dadurch spezialisiert auf σ(A0 ) allgemeinster Typ von ( f e ) ist dann σ(B) Wiederholung Substitutionen Substitution: partielle Funktion θ : X → Term(Σ, X ) Notation als Aufzählung [x 7→ t1 , y 7→ t2 , . . .] Anwendung einer Substitution: I s[x 7→ t] ist der Term, welcher aus dem Term s durch Ersetzung jedes Vorkommens der Variable x durch t entsteht I ϕ[x 7→ t] ist die Formel, die aus der Formel ϕ durch Ersetzung jedes freien Vorkommens der Variable x durch t entsteht Beispiele: I g(x, f (a))[x 7→ b] = g(b, f (a)) I h(y , x, f (g(y , a)))[x 7→ g(a, z), y 7→ a] = h(a, g(a, z), f (g(a, a))) I g(x, f (a))[x 7→ b, y 7→ a] = g(b, f (a)) I g(b, f (y ))[x 7→ b, y 7→ a] = g(b, f (a)) I für θ = [x 7→ b], σ = [y 7→ f (a)] (auch θ(x) = b, σ(y ) = f (a) ) gilt (h((b, f (y )), k (x)))θσ = σ(θ(h((b, f (y )), k (x))) = σ(h((b, f (y )), k (b)) = h((b, f (f (a))), k (b)) Unifikator Substitution θ heißt genau dann Unifikator der Terme t1 und t2 (θ unifiziert t1 und t2 ), wenn θ(t1 ) = θ(t2 ) gilt. Beispiele: 1. θ = [x 7→ b, y 7→ a] unifiziert t1 = g(x, f (a)) und t2 = g(b, f (y )) 2. [x 7→ g(g(y )), z 7→ g(y )] unifiziert f (x, g(y )) und f (g(z), z) (und f (g(z), g(y )). 3. [x 7→ g(g(a)), y 7→ a, z 7→ g(a)] unifiziert f (x, g(y )) und f (g(z), z). 4. [x 7→ g(g(y )), z 7→ g(y ), v 7→ f (a)] unifiziert f (x, g(y )) und f (g(z), z). Terme t1 , t2 heißen genau dann unifizierbar, wenn ein Unifikator für t1 und t2 existiert. Beispiele: 1. g(x, f (a)) und g(b, f (y )) sind unifizierbar, f (g(a, x)) und f (g(f (x), a)) nicht. 2. h(a, f (x), g(a, y )) und h(x, f (y ), z) sind unifizierbar, h(f (a), x) und h(x, a) nicht. Was bisher geschah I Deklarative vs. imperative Programmierung I Funktionale Programmierung: Programm: Menge von Gleichungen von Termen (Konstruktor-System) Ausdruck hat Typ und Wert (zu berechnen) Ausführung: Pattern matching, Termersetzung Haskell: I Algebraische Datentypen und Pattern Matching I Rekursive Datentypen (Peano-Zahlen) I Rekursive Funktionen I strukturelle Induktion Typ-Inferenz in Haskell Inferenzregel: f :: A → B e :: A (f e) :: B für polymorphe Typen: f :: A → B e :: A0 (f e) ::? Unifikator σ der Typausdrücke (Terme) A und A0 (Substitution mit σ(A) = σ(A0 )) f :: σ(A) → σ(B) e :: σ(A0 ) (f e) :: σ(B) Wiederholung Unifikator Substitution θ heißt genau dann Unifikator der Terme t1 und t2 (θ unifiziert t1 und t2 ), wenn θ(t1 ) = θ(t2 ) gilt. Beispiele: 1. θ = [x 7→ b, y 7→ a] unifiziert t1 = g(x, f (a)) und t2 = g(b, f (y )) 2. [x 7→ g(g(y )), z 7→ g(y )] unifiziert f (x, g(y )) und f (g(z), z) (und f (g(z), g(y )). 3. [x 7→ g(g(a)), y 7→ a, z 7→ g(a)] unifiziert f (x, g(y )) und f (g(z), z). 4. [x 7→ g(g(y )), z 7→ g(y ), v 7→ f (a)] unifiziert f (x, g(y )) und f (g(z), z). Terme t1 , t2 heißen genau dann unifizierbar, wenn ein Unifikator für t1 und t2 existiert. Beispiele: 1. g(x, f (a)) und g(b, f (y )) sind unifizierbar, f (g(a, x)) und f (g(f (x), a)) nicht. 2. h(a, f (x), g(a, y )) und h(x, f (y ), z) sind unifizierbar, h(f (a), x) und h(x, a) nicht. (Keine) Ordnung auf Unifikatoren Für zwei Unifikatoren σ, θ der Terme s, t gilt: Relation R auf Substitutionen: (σ, θ) ∈ R gdw. ∃ρ : σ ◦ ρ = θ (Man bemerke die Analogie zur Teilerrelation) Beispiele: I ([x 7→ y ], [x 7→ a, y 7→ a]) ∈ R I ([x 7→ y ], [y 7→ x]) ∈ R I ([y 7→ x], [x 7→ y ]) ∈ R Diese Relation R ist reflexiv und transitiv, aber nicht antisymmetrisch. Ordung auf Unifikatoren σ heißt genau dann allgemeiner als θ, wenn eine Substitution ρ (die nicht nur Umbenennung ist) existiert, so dass σ ◦ ρ = θ Diese Relation ist eine Halbordnung Beispiele: Unifikatoren für f (x, g(y )), f (g(z), z) 1. Unifikator [x 7→ g(g(y )), z 7→ g(y )] ist allgemeiner als [x 7→ g(g(a)), z 7→ g(a)] ρ = [y 7→ a] 2. Unifikator [x 7→ g(g(y )), z 7→ g(y )] ist allgemeiner als [x 7→ g(g(y )), z 7→ g(y ), v 7→ g(b)] ρ = [v 7→ g(b)] Allgemeinster Unifikator Zu unifizierbaren Termen s, t existiert (bis auf Umbenennung der Variablen) genau ein Unifikator θ mit der folgenden Eigenschaft: Für jeden Unifikator σ für s, t ist θ allgemeiner als σ. Dieser heißt allgemeinster Unifikator θ = mgu(s, t) von s und t. (analog ggT) Beispiele: I mgu(f (x, a), f (g(b), y )) = [x 7→ g(b), y 7→ a] I mgu(f (x, g(y )), f (g(z), z)) = [x 7→ g(g(y )), z 7→ g(y )] Unifizierbarkeit I Jeder Term t ist mit t unifizierbar. allgemeinster Unifikator mgu(t, t) = [] I Jeder Term t ist mit jeder Variable x ∈ , die nicht in t vorkommt, unifizierbar. allgemeinster Unifikator mgu(t, t) = [x 7→ t] I f (t1 , . . . , tn ) und g(s1 , . . . , sm ) sind nicht unifizierbar, falls f 6= g oder n 6= m I θ ist Unifikator für f (t1 , . . . , tn ), f (s1 , . . . , sn ) gdw. ∀i ∈ {1, . . . , n} : θ unifiziert ti und si X Unifikation – Aufgabe Eingabe: Terme s, t ∈ Term(Σ, X) Ausgabe: ein allgemeinster Unifikator (mgu) σ : → Term(Σ, ) mit sσ = tσ. X X Satz: Jedes Unifikationsproblem ist I entweder gar nicht I oder bis auf Umbenennung eindeutig lösbar. Unifikation – Algorithmus Berechnung von σ = mgu(s, t) für Terme s, t ∈ Term(Σ, durch Fallunterscheidung: X) X I s∈ : falls s 6∈ var(t), dann σ = [s 7→ t], sonst nicht unifizierbar I t∈ I s = f (s1 , . . . , sm ) und t = g(t1 , . . . , tn ): falls f 6= g oder m 6= n, dann nicht unifizierbar sonst σ = mgu(s1 , t1 ) ◦ · · · ◦ mgu(sm , tm ) X: symmetrisch Dabei gilt für jede Substitution θ: θ◦„nicht unifizierbar“ = „nicht unifizierbar“◦θ = „nicht unifizierbar“ Unifikationsalgorithmus – Beispiele I mgu(f (x, h(y ), y ), f (g(z), z, a)) = [x 7→ g(h(a)), z 7→ h(a), y 7→ a] I mgu (k (f (x), g(y , h(a, z))), k (f (g(a, b)), g(g(u, v ), w))) = [x 7→ g(a, b), y 7→ g(u, v ), w 7→ h(a, z)] I mgu(k (f (a), g(x)), k (y , y )) existiert nicht I mgu(f (x, g(a, z)), f (f (y ), f (x)) existiert nicht I mgu(f (x, x), f (y , g(y )) existiert nicht I mgu(f (x, g(y )), f (y , x) existiert nicht Unifikation von Haskell-Typen – Beispiele I last :: [a] -> a Typ von [ 3, 5 .. 10 ] ist [Int] angewendete Instanz der Funktion last :: [Int] -> Int , der Typ von last [ 3, 5 .. 10 ] ist also Int I take :: Int -> [a] -> [a] Typ von take 1 ? Typ von take 1 [ "foo", "bar" ] ? Was bisher geschah I Deklarative vs. imperative Programmierung I Funktionale Programmierung: Programm: Menge von Gleichungen von Termen (Konstruktor-System) Ausdruck hat Typ und Wert (zu berechnen) Ausführung: Pattern matching, Termersetzung Funktionale Programmierung in Haskell I rekursive Funktionen I algebraische Datentypen und Pattern Matching I rekursive Datentypen (Peano-Zahlen) I strukturelle Induktion I Typen, Typ-Konstruktoren, Typ-Synonyme I Polymorphie I Typ-Inferenz, Unifikation Datentyp Liste (polymorph) data List a = Nil | Cons { head :: a, tail :: List a} oder kürzer (vordefiniert) data [a] = [] | a : [a] Pattern Matching: f :: [a] -> ... f xs = case xs of [] -> ... (x : xss) -> ... Beispiel: append :: [a] -> [a] -> [a] append xs ys = case xs of [] -> ys (x : xss) -> x : (append xss ys) Strukturelle Induktion über Listen zum Nachweis von Eigenschaften wie z.B. I append xs [] = xs I append ist assoziativ, d.h append xs (append ys zs) = append (append xs ys) zs Länge der Eingabeliste len :: [a] -> len xs = case [] (x : xss) Int xs of -> 0 -> 1 + len xss Strukturelle Induktion zum Nachweis von len ( append xs ys ) = len xs + len ys Mehr Beispiele Summe aller Elemente der Eingabeliste sum :: [Int] -> Int sum xs = case xs of [] -> ... (x : xss) -> ... jedes Element der Eingabeliste verdoppeln doubles doubles [] ( y :: [Int] -> [Int] xs = case xs of -> [] : ys ) -> ... : (doubles ys) Strukturelle Induktion zum Nachweis von sum ( doubles xs ) = 2 * ( sum xs ) Sortierte Listen (aufsteigend geordnet) sortiert :: [Int] -> Bool sortiert xs = case xs of [] -> True [ _ ] -> True (x : y : ys) -> x <= y && sortiert (y : ys) sortiertes Einfügen: insert :: Int -> [Int] -> [Int] insert y xs = case xs of [] -> ... ( x : xs ) -> if ... then ... else ... Strukturelle Induktion zum Nachweis von: Aus sortiert xs folgt sortiert ( insert x xs ) List Comprehensions – Motivation Menge der Quadrate aller geraden Zahlen zwischen 0 und 20: {i 2 | i ∈ {0, . . . , 20} ∧ i ≡ 0 (mod 2)} Liste der Quadrate aller geraden Zahlen zwischen 0 und 20: i 2 i∈[0,...,20], i≡0 (mod 2) Definition der Menge / Liste enthält: Generator i ∈ [0, . . . , 20] Funktion 2 : N→N Bedingung i ≡ 0 (mod 2) als List Comprehension in Haskell: [ i ^ 2 | i <- [0 .. 20], rem i 2 == 0] List Comprehensions I I I mit einem Generator [ f x | x <- ..] z.B. [ 3 * x | x <- [1 .. 5] ] mit mehreren Generatoren [ f x1 .. xn |x1 <- .., .. , xn <- .. ] z.B. [ ( x , y ) | x <- [1 .. 3], y <- [0,1] ] [ (x, x * y, x + z) | x <- [1 .. 5] , y <- [0 .. 2] , z <- [3 ..] ] mit Bedingungen: [ f x1 .. xn | x1 <- .., .. , xn <- .. , r1 xi xj , .. , rk xi xj ] z.B. [ ( x , y ) | x <- [1 .. 5], y <- [ 0 .. 4 ] , x + y > 5, rem x 2 == 0] Beispiele [ ( x, y ) | x <- [ 1 .. 3 ], y <- [ 4 , 5 ] ] [ ( x, y ) | y <- [ 1 .. 3 ], x <- [ 4 , 5 ] ] [ x * y | x <- [ 1 .. 3 ], y <- [ 2 .. 4 ] ] [ x * y | x <- [1 .. 3], y <- [2 .. 4], x < y ] [ ’a’ | _ <- [1 .. 4] ] [ [1 .. n] | n <- [0 .. 5] ] hat welchen Typ? [ x | xs <- xss , x <- xs ] ] xss hat welchen (allgemeinsten) Typ? Mehr Beispiele teiler :: Int -> [ Int ] teiler x = [ y | y <- [ 1 .. x ], rem x y == 0 ] prim :: Int -> Bool prim x = ( teiler x ) == [ 1, x ] primzahlen :: [ Int ] primzahlen = [ x | x <- [ 2 .. ], prim x ] ( später auch anders ) Was bisher geschah I Deklarative vs. imperative Programmierung I Funktionale Programmierung: Programm: Menge von Gleichungen von Termen (Konstruktor-System) Ausdruck hat Typ und Wert (zu berechnen) Ausführung: Pattern matching, Termersetzung Funktionale Programmierung in Haskell I rekursive Funktionen I algebraische Datentypen und Pattern Matching rekursive Datentypen I I I Peano-Zahlen, Listen I strukturelle Induktion I Typen, Polymorphie, Typ-Inferenz Datentyp Binärbaum (polymorph) data Bintree a = Leaf | Branch { left key right {} :: Bintree a, :: a, :: Bintree a } Beispiel: t :: Bintree Int t = Branch { left = Branch { left = Leaf {}, key = 5, right = Leaf {} }, key = 3, right = Branch { left = Leaf {}, key = 2, right = Branch { left = Leaf {}, key = 4, right = Leaf {} }}} Pattern Matching data Bintree a = Leaf | Branch { left key right {} :: Bintree a, :: a, :: Bintree a } f :: Bintree a -> .. f t = case t of Leaf {} -> .. Branch {} -> .. oder tiefer: f :: Bintree a -> .. f t = case t of Leaf {} -> .. Branch { left = l, key = k, right = r } -> .. Rekursion über binäre Bäume – Beispiele Anzahl der inneren Knoten count :: Bintree a -> Int count t = case t of Leaf {} -> 0 Branch {} -> count (left t) + 1 + count (right t) Anzahl der Blätter: leaves :: Bintree a -> Int leaves t = case t of Leaf {} -> ... Branch {} -> ... Summe der Schlüssel (Int): bt_sum :: Bintree Int -> Int bt_sum t = case t of Leaf {} -> ... Branch {} -> ... Mehr Beispiele jeden Schlüssel verdoppeln doubles :: Bintree Int -> Bintree Int doubles t = case t of Leaf {} -> Leaf {} Branch {} -> ... inorder :: Bintree a -> [a] inorder t = case t of Leaf {} -> [] Branch {} -> ... vollständiger binärer Baum der Höhe h: full :: Int -> Bintree Int full h = if h > 0 then Branch { left = full (h-1), key = h, right = full (h-1) } else Leaf {} Strukturelle Induktion über Binärbäume z.z. Jeder Binärbaum t mit Schlüsseln vom Typ a hat die Eigenschaft P IA (t = Leaf): z.z.: Leaf hat die Eigenschaft P IS IV: Binärbäume l und r erfüllen P IB: ∀ k :: a hat der Binärbaum Branch { left = l, key = k, right = r } die Eigenschaft P zum Nachweis von Eigenschaften wie z.B. I I ∀ ( t :: Bintree Int ) : bt_sum (doubles t) = 2 * bt_sum t ∀ ( t :: Bintree Int ) : bt_sum t = list_sum ( inorder t ) Was bisher geschah I Deklarative vs. imperative Programmierung I Funktionale Programmierung: Programm: Menge von Gleichungen von Termen (Konstruktor-System) Ausdruck hat Typ und Wert (zu berechnen) Ausführung: Pattern matching, Termersetzung Funktionale Programmierung in Haskell I rekursive Funktionen I algebraische Datentypen und Pattern Matching rekursive Datentypen I I I I Peano-Zahlen, Listen, Binärbäume I strukturelle Induktion I Typen, Polymorphie, Typ-Inferenz Wiederholung Datentyp Binärbaum (polymorph) data Bintree a = Leaf | Branch { left key right {} :: Bintree a, :: a, :: Bintree a } Beispiel: t :: Bintree Int t = Branch { left = Branch { left = Leaf {}, key = 1, right = Leaf {} }, key = 3, right = Branch { left = Leaf {}, key = 4, right = Branch { left = Leaf {}, key = 6, right = Leaf {} }}} Binäre Suchbäume Suchbaum-Eigenschaft: Ein binärer Baum t :: Bintree Int ist genau dann ein Suchbaum, wenn seine Knoten in Inorder-Durchquerung (aufsteigend) geordnet sind. search_tree t = sortiert (inorder t) mit sortiert sortiert sortiert sortiert :: [ Int ] -> [] = [ x ] = ( x : y : xs ) = Bool True True ... Einfügen eines Schlüssels in einen binären Suchbaum: insert :: Int -> Bintree Int -> Bintree Int insert x t = case t of Leaf {} -> Branch { left = Leaf {}, key = x, right = Leaf {} } Branch {} -> ... Sortieren durch Einfügen in binäre Suchbäume Einfügen mehrerer Schlüssel in binären Suchbaum: inserts :: [Int] -> Bintree Int -> Bintree Int inserts xs t = case xs of [] -> t ( x : xss ) -> ... Sortieren durch Einfügen in binären Suchbaum: sort :: [Int] -> [Int] sort xs = inorder ( inserts xs Leaf ) Strukturelle Induktion über Bäume zum Nachweis von Eigenschaften wie z.B. I bt_sum (insert x t) = x + bt_sum t I Für jeden Suchbaum t ist inorder t sortiert. I Einfügen, Löschen eines Knotens erhalten die Suchbaum-Eigenschaft. Eingeschänkte Polymorphie reverse [1,2,3,4] = [4,3,2,1] reverse "foobar" = "raboof" reverse :: [a] -> [a] reverse ist polymorph Sortieren von Listen sort [5,1,4,3] = [1,3,4,5] sort "foobar" = "abfoor" sort :: [a] -> [a] -- ?? sort [sin,cos,log] = ?? sort ist eingeschränkt polymorph Eingeschränkte Polymorphie in Haskell durch Typklassen Beispiel Sortieren/Vergleichen Einfügen (in monotone Liste) insert :: Int -> [Int] -> [Int] insert x ys = case ys of [] -> [x] y : ys’ -> if x < y then .. else .. Sortieren durch Einfügen: sort :: [Int] -> [Int] sort xs = case xs of [] -> [] x : xs’ -> insert x (sort xs’) Einfügen/Sortieren für beliebige Typen: mit Vergleichsfunktion lt :: a -> a -> Bool als zusätzlichem Argument insert :: ( a -> a -> Bool ) -> a -> [a] -> [a] insert lt x ys = ... if lt x y then ... Sortieren/Vergleichen Sortieren enthält Vergleiche < Für alle Typen a, die für die es eine Vergleichs-Funktion compare gibt, hat sort den Typ [a] -> [a]. sort :: Ord a => [a] -> [a] Ord ist eine Typklasse, definiert durch class Ord a where compare :: a -> a -> Ordering data Ordering = LT | EQ | GT Instanzen Typen können Instanzen von Typklassen sein. (analog in OO: Klassen implementieren Interfaces) Für vordefinierte Typen sind auch die meisten sinnvollen Instanzen vordefiniert instance Ord Int ; instance Ord Char ; ... weitere Instanzen kann man selbst deklarieren: data Student = Student { vorname :: String , nachname :: String , matrikel :: Int } instance Ord Student where compare s t = compare (matrikel s) (matrikel t) Typen und Typklassen In Haskell sind unabhängig: 1. Deklaration einer Typklasse (= Deklaration von abstrakten Methoden) class C where { m :: ... } 2. Deklaration eines Typs (= Sammlung von Konstruktoren und konkreten Methoden) data T = ... 3. Instanz-Deklaration (= Implementierung der abstrakten Methoden) instance C T where { m = ... } In Java sind 2 und 3 nur gemeinsam möglich class T implements C { ... } Typen mit Gleichheit class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool Beispiele: I (’a’ == ’b’) = False I (True /= False) = True I ("ab" /= "ac") = True I ([1,2] == [1,2,3]) = False I (\ x -> 2 * x) == (\ x -> x + x) = ? Typen mit totaler Ordnung Instanzen der Typklasse Eq mit data Ordering = LT | EQ | GT class Eq a => Ord a where compare :: a -> a -> Ordering (<) :: a -> a -> Bool (<=) :: a -> a -> Bool (>) :: a -> a -> Bool (>=) :: a -> a -> Bool min :: a -> a -> a max :: a -> a -> a Beispiele: I (’a’ < ’b’) = True I (False < True) = True I ("ab" < "ac") = True I ([1,2] > [1,2,3]) = False (lexikographisch) Klassen-Hierarchien Typklassen können in Beziehung stehen. Ord ist „abgeleitet“ von Eq: class Eq a where (==) :: a -> a -> Bool class Eq a => Ord a where (<) :: a -> a -> Bool Ord ist Typklasse mit Typconstraint (Eq) also muss man erst die Eq-Instanz deklarieren, dann die Ord-Instanz. Instanzen data Bool = False | True instance Eq Bool where False == False = True True == True = True _ == _ = False zu definieren: instance Ord Bool where False < True = True _ < _ = False abgeleitet: x x x <= y > y >= y = ( x < y ) || ( x == y ) = y < x = y <= x Typen mit Operation zum (zeilenweisen) Anzeigen class Show a where show :: a -> String Beispiele: I show 123 = "123" I show True = "True" I show [1,2] = "[1,2]" I show (1,’a’,True) = "show (1,’a’,True)" Instanzen Bool, Char, Int, Integer, Float, Listen und Tupel von Instanzen Typklasse Show Interpreter ghci gibt bei Eingabe exp (normalerweise) show exp aus. Man sollte (u. a. deswegen) für jeden selbst deklarierten Datentyp eine Show-Instanz schreiben. . . . oder schreiben lassen: deriving Show Typen mit Operation zum Lesen class Read a where read :: String -> a Beispiele: I ( read "3" :: Int ) = 3 I ( read "3" :: Float ) = 3.0 I ( read "False" :: Bool ) = False I ( read "’a’" :: Char ) = ’a’ I ( read "[1,2,3]" :: [Int] ) = [1,2,3] Instanzen Bool, Char, Int, Integer, Float, Listen und Tupel von Instanzen Numerische Typen class (Eq a, Show a) => Num a where (+) :: a -> a -> a (-) :: a -> a -> a (*) :: a -> a -> a negate :: a -> a abs :: a -> a signum :: a -> a Beispiele: I signum (-3) = -1 I signum (-3.3) = -1.0 Instanzen Int, Integer, Float Numerische Typen mit Division Ganzzahl-Division: class Num a => Integral a where div :: a -> a -> a mod :: a -> a -> a Instanzen Int, Integer Beispiel: 3 ‘div‘ 2 = 1 Division: class Num a => Fractional a where (/) :: a -> a -> a recip :: a -> a -> a Instanzen: Float, Double Beispiel: 3 / 2 = 0.6 Generische Instanzen class Eq a where (==) :: a -> a -> Bool Vergleichen von Listen (elementweise) wenn a in Eq, dann [ a ] in Eq: instance Eq [] (x : xs) _ a => Eq [a] where == [] = True == (y : ys) = (x == y) && ( xs == ys ) == _ = False instance Ord a => Ord [a] where compare [] [] = EQ compare [] (_:_) = LT compare (_:_) [] = GT compare (x:xs) (y:ys) = case compare x y of EQ -> compare xs ys other -> other Abgeleitete Instanzen Deklaration eigener Typen als Instanzen von Standardklassen durch automatische Erzeugung der benötigten Methoden: Beispiele: data Bool = False | True deriving (Eq, Ord, Show, Read) data Shape = Circle Float | Rect Float Float deriving (Eq, Ord, Show, Read) z.B. (Circle 3 < Rect 1 2) == True data Maybe a = Nothing | Just a deriving (Eq, Ord, Show, Read) z.B. (Just ’a’ == Just ’b’) == False Was bisher geschah I Deklarative vs. imperative Programmierung I Deklarative Programmierung Funktionale Programmierung in Haskell: I Algebraische Datentypen I Pattern Matching I (eingeschränkte) Polymorphie, Typklassen I Rekursive Datentypen: Peano-Zahlen, Listen, binäre Bäume I Rekursive Funktionen I strukturelle Induktion Funktionen als Daten bisher: f :: Int -> Int f x = 2 * x + 5 äquivalent: Lambda-Ausdruck f = \ x -> 2 * x + 5 Lambda-Kalkül: Alonzo Church 1936, Henk Barendregt 1984, ... Funktionsanwendung: ( \ x -> B ) A = B [ x := A ] ist nur erlaubt, falls keine in A freie Variable durch ein λ in B gebunden wird. Der Lambda-Kalkül . . . als weiteres Berechnungsmodell, (vgl. Termersetzungssysteme, Turingmaschine, Random-Access-Maschine) Syntax (induktive Definition): Die Menge der Lambda-Terme Λ( ) mit Variablen aus X X ist IA: jede Variable ist ein Term: v ∈ X ⇒ v ∈ Λ(X) IS: Applikation , Funktionsanwendung: Für alle F ∈ Λ( ), A ∈ Λ( ) gilt (FA) ∈ Λ( ) Abstraktion , Funktionsdefinition: Für alle v ∈ , B ∈ Λ( ) gilt (λv .B) ∈ Λ( ) X X X X X Semantik: eine Relation →β auf Λ( ) (vgl. →R für Termersetzungssystem R) X X Freie und gebundene Variablen(vorkommen) X X I Das Vorkommen von v ∈ an Position p in Term t ∈ Λ( ) heißt frei, wenn „darüber kein λv . . . . steht“ I Definition (durch strukturelle Induktion): fvar(t) = Menge der in t frei vorkommenden Variablen I Eine Variable x heißt in A gebunden, falls A einen Teilausdruck λx.B enthält. I bvar(t) = Menge der in t gebundenen Variablen Beispiele: I fvar(x(λx.λy .x)) = {x}, I bvar(x(λx.λy .x)) = {x, y } Semantik des Lambda-Kalküls X Relation →β auf Λ( ) (ein Reduktionsschritt) Es gilt t →β t 0 , falls I I I ∃p ∈ Pos(t), so daß t[p] = (λx.B)A mit bvar(B) ∩ fvar(A) = ∅ t 0 = t[p := B[x := A]] dabei bezeichnet B[x := A] ein Kopie von B, bei der jedes freie Vorkommen von x durch A ersetzt ist Ein (Teil-)Ausdruck der Form (λx.B)A heißt Redex. (Dort kann weitergerechnet werden.) Ein Term ohne Redex heißt Normalform. (Normalformen sind Resultate von Rechnungen.) Relation →α : gebundene Umbenennung Lambda-Terme: verkürzte Notation I Applikation als links-assoziativ auffassen, Klammern weglassen: (. . . ((FA1 )A2 ) . . . An ) ∼ FA1 A2 . . . An Beispiel: ((xz)(yz)) ∼ xz(yz) I geschachtelte Abstraktionen unter ein Lambda schreiben: λx1 .(λx2 . . . . (λxn .B) . . . ) ∼ λx1 x2 . . . xn .B Beispiel: λx.λy .λz.B ∼ λxyz.B Funktionen höherer Ordnung Funktionen als Argument von Funktionen Beispiel: twice :: (a -> a) -> a -> a twice f x = f (f x) Anwendung: I double hat den Typ Int -> Int I twice double hat den Typ Int -> Int I twice double 3 hat den Typ Int und den Wert ? I \x -> 2 * x + 1 hat den Typ Int -> Int twice (\x -> 2 * x + 1) hat den Typ Int -> Int I I twice (\x -> 2 * x + 1) 3 hat den Typ Int und den Wert ? I succ 0, twice succ 0, twice twice succ 0 I twice (^2) 3, twice twice (^2) 3 I Typ von twice twice ? Typ von twice twice twice ? Funktionen höherer Ordnung – Beispiele I punktweise Summe zweier Funktionen: fsum :: (a -> Int) -> (a -> Int) -> (a -> Int) fsum f g x = (f x) + (g x) fsum f g = \x -> (f x) + (g x) Beispiele: I I I fsum (*2) (+1) 4, fsum len head [ 2 .. 5 ] Komposition von Funktionen: (.) :: (a -> b) -> (b -> c) -> (a -> c) (f . g) x = f (g x) (f . g) = \ x -> f (g x) Beispiele: I I ( ( \ x -> x * 2 ) . len ) "foo" suchbaum = sortiert . inorder Was bisher geschah I Deklarative vs. imperative Programmierung Funktionale Programmierung in Haskell: I Algebraische Datentypen I Pattern Matching I Polymorphie I Typklassen I Rekursive Datentypen: Peano-Zahlen, Listen, Bäume I Rekursive Funktionen I strukturelle Induktion I Funktionen höherer Ordnung (mit Funktionen als Argumenten) I λ-Kalkül, β-Reduktion Wiederholung: rekursive Datentypen I I I Peano-Zahlen data Nat = Z | S Nat Listen data List a = Nil {} | Cons { head :: a, tail :: List a} oder kürzer data [a] = [] | a : [a] Binärbäume data Tree a = Leaf {} | Branch { left :: Tree a, key :: a, right :: Tree a} oder kürzer data Tree a = Leaf | Branch ( Tree a ) a ( Tree a ) Wiederholung: Funktionen auf rekursiven Datentypen Entwurf rekursiver Funktionen auf rekursiven Datentypen: 1. Typdefinition 2. Angabe aller Basis- und rekursiven Fälle 3. Definition der Ergebnisse der Basisfälle 4. Definition der Ergebnisse der rekursiven Fälle 5. evtl. Typ verallgemeinern Beispiel: Summe aller Schlüssel eines Baumes data Tree a = Leaf | Branch (Tree a) a (Tree a) 1. Typdefinition: tsum :: Tree Int -> Int 2. Angabe aller Basis- und rekursiven Fälle: tsum t = case t of Leaf -> ... Branch l k r -> ... 3. Definition der Ergebnisse der Basisfälle: Leaf -> 0 4. Definition der Ergebnisse der rekursiven Fälle: Branch l k r -> (tsum l) + k + (tsum r) Wiederholung: Funktionen auf Listen und Bäumen Operationen auf Listen: I Verdoppeln jedes Listenelements I Angabe gerade / ungerade für jedes Listenelement I Länge der Liste I Summe aller Listenelemente Operationen auf Bäumen: I Verdoppeln jedes Schlüssels I Angabe gerade / ungerade für jeden Schlüssel I Anzahl aller Schlüssel I Summe aller Schlüssel I Inorder-Durchquerung Wiederholung: Funktionen auf Listen Beispiel: Verdoppeln jedes Elementes in einer Liste double :: Int -> Int double x = x + x doubles :: [Int] -> [Int] doubles xs = case xs of [] -> [] (y:ys) -> (double y) : (doubles ys) oder mit anonymer Funktion (λ-Notation): doubles :: doubles xs [] -> (y:ys) [Int] -> [Int] = case xs of [] -> ((\ x -> x + x) y) : (doubles ys) evens :: [Int] -> [Bool] evens xs = case xs of [] -> [] (y:ys) -> ((\x->(mod x 2 == 0)) y) : (evens ys) Rekursionsmuster für Listen gemeinsame Eigenschaft: Ergebnis ist die Liste der Funktionswerte jedes Elementes der Eingabeliste I Parameter: I I auf jedes Element anzuwendende Funktion h :: a -> b Liste vom Typ [a] I Ergebnis: Liste vom Typ [b] I Berechnung (Pattern Matching): f xs = case xs of [] -> [] (x : xss) -> ( h x ) : ( f xss ) Rekursionsmuster map Beschreibung des Rekursionsschemas f x = case x of [] -> [] (x : xss) -> ( h x ) : ( f xss ) durch eine Funktion höherer Ordnung mit der Funktion h :: a -> b als Argument map :: ( a -> b ) -> [a] -> [b] Anwendung: f = map h ermöglicht kurze Funktionsdefinition, z.B. doubles :: [ Int ] -> [ Int ] doubles = map double oder mit anonymer Funktion: doubles = map (\z -> z*2) oder noch kürzer: doubles = map ( *2 ) filter Beispiel: nur gerade Zahlen der Eingabeliste ev :: Int -> Bool ev = \x -> ( mod x 2 == 0 ) evens :: [Int] -> [Int] evens xs = case xs of [] -> [] ( x : xss ) -> if ev x then x : ( evens xss ) else ( evens xss ) Funktion höherer Ordnung: filter :: ( a -> Bool ) -> [a] -> [a] filter p xs = case xs of [] -> [] ( x : xss ) -> if ( p x ) then x : ( filter p xss ) else filter p xss filter ev :: Int -> Bool ev = \x -> ( mod x 2 == 0 ) filter :: (a -> Bool) -> [a] -> [a] filter p xs = case xs of [] -> [] ( x : xss ) -> if ( p x ) then x : ( filter p xss ) else filter p xss ermöglicht kurze Funktionsdefinitionen, z.B.: evens = filter ev oder mit anonymer Funktion evens = filter ( \x -> ( mod x 2 == 0 ) ) filter ( < 100 ) ( map ( ^2 ) [ 0, 2 .. 10 ] ) Mehr rekursive Funktionen auf Listen data [a] = [] | a : [a] Länge einer Liste: len :: [a] -> Int len xs = case xs of [] -> 0 ( _ : xss ) -> 1 + (len xss) Summe aller Listenelemente: sum :: [Int] -> Int sum xs = case xs of [] -> 0 ( x : xss ) -> x + (sum xss) Mehr Rekursionsmuster für Listen gemeinsame Eigenschaft: I Parameter: I I I Wert nil :: b für leere Eingabeliste Funktion cons :: a -> b -> b zur Berechnung eines Wertes aus dem bisher berechneten Wert und einem Listenelement Liste vom Typ [a] I Ergebnis vom Typ b I Berechnung (Pattern Matching): f xs = case xs of [] -> nil (x : xss) -> cons x ( f xss ) Rekursionschema fold Funktion höherer Ordnung (mit Funktionen als Argumenten) fold :: b -> (a -> b -> b) -> [a] -> b fold nil cons xs = case xs of [] -> nil x : xss -> cons x ( fold nil cons xss ) ermöglicht kurze Funktionsdefinition, z.B. len = fold 0 (\ x y -> 1 + x) sum = fold 0 (\ x y -> x + y) and = fold True (&&) oder kurz: sum = fold 0 (+) Funktionen höherer Ordnung für Listen in Haskell vordefinierte Funktionen höherer Ordnung I zur Verarbeitung von Listen: map :: (a -> b) -> [a] -> [b] foldr :: (a -> b -> b) -> b -> filter :: (a -> Bool) -> [a] -> takeWhile :: (a -> Bool) -> [a] partition :: (a -> Bool) -> [a] I [a] -> b [a] -> [a] -> ([a],[a]) zum Vergleichen, Ordnen: nubBy :: (a -> a -> Bool) -> [a] -> [a] data Ordering = LT | EQ | GT minimumBy :: (a -> a -> Ordering) -> [a] -> a Rekursionsschemata über Bäume data Tree a = Leaf | Branch { left :: Tree a, key :: a, right :: Tree a } doubles :: Tree Int -> [Int] doubles t = case t of Leaf -> Leaf Branch l k r -> Branch (doubles l) (k*2) (doubles r) preorder :: Tree a -> [a] preorder t = case t of Leaf -> [] Branch l k r -> [ k ] ++ ( preorder l ) ++ ( preorder r ) sum :: Tree Int -> Int sum t = case t of Leaf -> 0 Branch l k r -> ( sum l ) + k + ( sum r ) Rekursionsschema map über Bäume f :: Tree a -> b f t = case t of Leaf -> leaf Branch l k r -> branch (f l) (g k) (f r) Beispiel: f = doubles g = double Rekursionsschema: tmap :: (a -> b ) -> ( Tree a ) -> ( Tree b ) tmap f t = case t of Leaf -> Leaf Branch l k r -> Branch (tmap f l) (f k) (tmap f r) doubles = tmap ( 2* ) Rekursionsschema fold über Bäume f :: Tree a -> b f t = case t of Leaf -> leaf Branch l k r -> branch (f l) k (f r) Beispiel: f = preorder leaf = [] branch l’ k r’ = [k] ++ l’ ++ r’ Rekursionsschema: tfold :: b -> (b -> a -> b -> b) -> (Tree a) -> b tfold leaf branch t = case t of Leaf -> leaf Branch l k r -> branch (tfold leaf branch l) k (tfold leaf branch r) Beispiele: fold über Bäume tfold :: b -> (b -> tfold leaf branch t Leaf -> Branch l k r -> a -> b -> b) -> (Tree a) -> b = case t of leaf branch (tfold leaf branch l) k (tfold leaf branch r) preorder = tfold [] ( \ l’ k r’ -> [k] ++ l’ ++ r’ ) sum = tfold 0 ( \ l’ k r’ -> l’ + k + r’ ) analog: Anzahl der Blätter, inneren Knoten, Tiefe Rekursionsmuster (Merksätze) Rekursionsmuster anwenden = jeden Konstruktor durch eine passende Funktion ersetzen I Anzahl der Muster-Argumente = Anzahl der Konstruktoren (plus eins für das Datenargument) I Stelligkeit eines Muster-Argumentes = Stelligkeit des entsprechenden Konstruktors I Rekursion im Typ → Rekursion im Muster I zu jedem rekursiven Datentyp gibt es genau ein passendes Rekursionsmuster Was bisher geschah Funktionale Programmierung in Haskell: I Algebraische Datentypen I Pattern Matching I Polymorphie I Typklassen I Rekursive Datentypen: Peano-Zahlen, Listen, Bäume I Rekursive Funktionen I strukturelle Induktion I Rekursionsschemata für Peano-Zahlen, Listen, Bäume I Funktionen höherer Ordnung (mit Funktionen als Argumenten) I λ-Kalkül, β-Reduktion I fold auf rekursiven Datentypen (Peano-Zahlen, Listen, Bäume) I map auf Listen und Bäumen, filter auf Listen Nützliche Funktionen take take take take :: Int -> [ a ] -> [ a ] 0 _ = [] _ [] = [] n ( x : xs ) = x : ( take ( n - 1 ) xs ) take 3 [ 1 .. 10 ] takeWhile :: ( a -> Bool ) -> [ a ] -> [ a ] takeWhile p xs = case xs of [] -> [] x : xss -> if p x then x : ( take While p xss ) else [] takeWhile ( \ x -> mod x 5 < 4) [ 1 .. 10 ] dropWhile :: ( a -> Bool ) -> [ a ] -> [ a ] dropWhile p xs = case xs of [] -> [] x : xss -> if p x then ( dropWhile p xss ) else xss Nützliche Funktionen zip :: [ a ] -> [ b ] -> [ ( a , b ) ] zip ( x : xs ) ( y : ys ) = ( x, y ) : zip ( xs ) ( ys ) zip _ _ = [] zip "foo" [1 .. 5] zipWith :: ( a -> b -> c ) -> [ a ] -> [ b ] -> [ c ] zipWith f xs ys = map ( \ ( x, y ) -> f x y ) ( zip xs ys ) zipWith (+) [ 1 .. 10 ] [ 2, 4 .. 10 ] zipWith (\x y -> ( foldr (\ _ y -> 1 + y) 0 x) + y) [ "foo", "b", "ar" ] [ 1 .. 10 ] Wiederholung – Auswertung von Ausdrücken Reduktion: Termersetzung durch Funktionsanwendung Redex: reduzierbarer Teilterm Normalform: nicht-reduzierbarer Ausdruck (Ausdruck ohne Redex) Auswertung: schrittweise Reduktion, bis Normalform erreicht square :: Int -> Int square x = x * x 2 Möglichkeiten, den Wert von square (3 + 1) zu berechnen Es wird bei beiden Möglichkeiten derselbe Wert berechnet. (Haskell ist nebenwirkungsfrei.) Auswertungsreihenfolge mult :: Int -> Int -> Int mult = \x y -> x * y Redexe von mult ( 1 + 2 ) ( 2 + 3 ) data N = Z | S N nichtnull :: N -> Bool nichtnull n = case n of Z -> False S _ -> True Redexe von nichtnull ( S undefined ) Auswertungs-Strategien innermost Reduktion von Redexen, die keinen Redex enthalten (Parameterübergabe by value) outermost Reduktion von Redexen, die in keinem Redex enthalten sind (Parameterübergabe by name) (jeweils so weit links wie möglich zuerst) square :: Int -> Int square x = x * x square (3 + 1) Teilterme in λ-Ausdrücken werden nicht reduziert. (\ x -> 1 + 2) 1 Termination inf :: Int inf = 1 + inf fst :: ( a , b ) -> a fst ( x, y ) = x Auswertung von fst (3, inf) terminiert unter outermost-Strategie, aber nicht unter innermost-Strategie Satz Für jeden Ausdruck, für den die Auswertung unter irgendeiner Strategie terminiert, terminert auch die Auswertung unter outermost-Strategie. Unendliche Datenstrukturen nats_from :: Int -> [ Int ] nats_from n = n : ( nats_from ( n + 1 ) ) nats_from 3 outermost-Auswertung von head ( tail ( tail ( nats_from 3 ) ) = = = = = = = = head ( tail ( tail ( 3 : ( nats_from ( 3 + 1 ))))) head ( tail ( nats_from (3 + 1))) head ( tail ( (3 + 1) : nats_from (( 3 + 1 ) + 1 )) head ( nats_from ( ( 3 + 1 ) + 1 ) ) head (((3 + 1) + 1) : nats_from (((3 + 1) + 1) + 1)) ( 3 + 1 ) + 1 4 + 1 5 Lazyness I jeder Wert wird erst bei Bedarf ausgewertet. I Listen sind Streams, der Tail wird erst bei Bedarf ausgewertet. I Wann die Auswertung stattfindet, lässt sich nicht beobachten. Die Auswertung hat keine Nebenwirkungen. Strictness zu jedem Typ T betrachte T⊥ = {⊥} ∪ T dabei ist ⊥ ein „Nicht-Resultat vom Typ T “ I Exception undefined :: T I oder Nicht-Termination let { x = x } in x Definition: Funktion f heißt strikt, wenn f (⊥) = ⊥. Funktion f mit n Argumenten heißt strikt in i, falls (xi = ⊥) ⇒ f (x1 , . . . , xn ) = ⊥ in Haskell: I I Konstruktoren (Cons,. . . ) sind nicht strikt, Destruktoren (head, tail,. . . ) sind strikt. Strictness – Beispiele I length :: [a] -> Int ist strikt: length undefined I ==> exception (:) :: a->[a]->[a] ist nicht strikt im 1. Argument: length (undefined : [2,3]) ==> 3 d.h. (undefined : [2,3]) ist nicht ⊥ I (&&) ist strikt im 1. Argument, nicht strikt im 2. Argument undefined && True False && undefined ==> (exception) ==> False Lazy Evaluation – Realisierung Begriffe: nicht strikt : nicht zu früh auswerten lazy : höchstens einmal auswerten bei jedem Konstruktor- und Funktionsaufruf: I kehrt sofort zurück I Resultat ist thunk I thunk wird erst bei Bedarf ausgewertet I Bedarf entsteht durch Pattern Matching I nach Auswertung: thunk durch Resultat überschreiben Lazy Evaluation (Bedarfsauswertung) = Outermost-Reduktionsstrategie mit Sharing Unendliche Datenstrukturen inf :: Int inf = 1 + inf fst(3, inf) einsen :: [Int] einsen = 1 : einsen head einsen take 3 einsen walzer :: [Int] walzer = 1 : 2 : 3 : walzer nats :: [Int] nats = 0 : map (+1) nats takeWhile (<= 5) nats Liste aller Quadratzahlen? Primzahlen? Motivation: Datenströme Folge von Daten: I erzeugen (producer) I transformieren I verarbeiten (consumer) aus softwaretechnischen Gründen: diese drei Aspekte im Programmtext trennen, aus Effizienzgründen: in der Ausführung verschränken (bedarfsgesteuerte Transformation/Erzeugung) Rekursive Stream-Definitionen nats = 0 : map (+1) nats fibonacci = 0 : 1 : zipWith (+) fibonacci ( tail fibonacci ) take 10 fibonacci take 1 $ dropWhile (< 200) fibonacci Welchen Wert hat bin ? bin = False : True : concat ( map ( \ x -> [ x, not x ] ) ( tail bin ) ) Thue-Morse-Folge t = 0110100110010110 . . . mit vielen interessanten Eigenschaften, z.B. I t := limn→∞ τ n (0) für τ : 0 7→ 01, 1 7→ 10 I t ist kubikfrei I Abstandsfolge v := 210201210120 . . . ist auch Fixpunkt eines Morphismus, quadratfrei Primzahlen Sieb des Eratosthenes nats_from :: Int -> [ Int ] nats_from n = n : nats_from ( n + 1 ) primzahlen :: [ Int ] primzahlen = sieb $ nats_from 2 sieb :: [ Int ] -> [ Int ] sieb (x : xs) = ... take 100 primzahlen takeWhile (< 100) primzahlen Was bisher geschah Funktionale Programmierung in Haskell: I I I I I I I I I I I I I Algebraische Datentypen Pattern Matching Polymorphie Typklassen Rekursive Datentypen: Peano-Zahlen, Listen, Bäume Rekursive Funktionen strukturelle Induktion Rekursionsschemata für Peano-Zahlen, Listen, Bäume Funktionen höherer Ordnung (mit Funktionen als Argumenten) λ-Kalkül, β-Reduktion fold auf rekursiven Datentypen (Peano-Zahlen, Listen, Bäume) map auf Listen und Bäumen, filter auf Listen Bedarfsauswertung (lazy evaluation): leftmost outermost reduction + sharing Sortieren sortiert :: Ord a => [a] -> Bool sortiert xs = foldr (&&) True $ zipWith (<=) xs $ tail xs sort :: Ord a => [a] -> [a] z.B. durch I Einfügen in (anfangs leeren) binären Suchbaum I Inorder-Ausgabe Klassische Sortier-Verfahren I Sortieren durch Einfügen insert :: Ord a => a -> [ a ] -> [ a ] insert x [] = [x] insert x ( y : ys ) | x <= y = x : y : ys | x > y = y : (insert x ys) isort :: Ord a => [a] -> [a] isort [] = [] isort (x:xs) = insert x $ isort xs I Quicksort qsort :: Ord a => [a] -> [a] qsort [] = [] qsort (x:xs) = qsort [ y | y <- xs, y <= x] ++ [x] ++ qsort [ y | y <- xs, y > x] Mergesort merge :: Ord a => [a] -> [a] -> [a] merge xs [] = xs merge [] ys = ys merge (x : xs) (y : ys) | x <= y = x : merge xs ( y : ys ) | otherwise = y : merge ( x : xs ) ys msort :: Ord a => [a] -> [a] msort [] = [] msort [ x ] = [ x ] msort xs = merge ( msort l ) ( msort r ) where ( l , r ) = splitAt halb xs halb = div (length xs) 2 Ver- und Entschlüsseln Verschiebe-Chiffre I symmetrisches Verschlüsselungs-Verfahren: derselbe Schlüssel zum Ver- und Entschlüsseln I Substitutionschiffre: Ersetzung jedes Klartextsymboles durch ein Chiffretextsymbol I monoalphabetisch: Klartextsymbol überall durch dasselbe Chiffretextsymbol ersetzt Klartextmenge: M = {a, b, . . . , z}∗ Chiffretextmenge: C = {a, b, . . . , z}∗ Schlüsselmenge: K = {0, . . . , 25} Verschlüsselung: jeden Buchstaben durch Buchstaben k Positionen später im Alphabet ersetzen Entschlüsselung: jeden Buchstaben durch Buchstaben k Positionen früher im Alphabet ersetzen klassisches Beispiel: Caesar-Chiffre k = 3 Verschlüsseln für jeden (Klein-)Buchstaben im Klartext: I Buchstabe durch Zahl ∈ {0, . . . , 25} ersetzen b2int :: Char -> Int b2int b = ord b - ord ’a’ I Zahl durch entsprechenden Buchstaben ersetzen int2b :: Int -> Char int2b n = chr (ord ’a’ + n) I Buchstabe mit Schlüssel k verschlüsseln: enc :: Int -> Char -> Char enc k b | isLower b = int2b ( mod (k + b2int b) 26) | otherwise = b Klartext verschlüsseln: encode :: Int -> String -> String encode k = map ( enc k ) Chiffretext entschlüsseln: . . . Angriffe auf Verschiebechiffren Ciphertext-Only-Angriffe auf Verschiebechiffren gegeben: verschlüsselter Text I hinreichend lang, I natürlichsprachig (deutsch), I mit Verschiebechiffre verschlüsselt gesucht: Klartext ( und evtl. Schlüssel ) Ideen für Angriffe: I Brute Force: Ausprobieren aller 26 Schlüssel I typische Häufigkeiten von Buchstaben, Buchstabengruppen Funktionen auf Listen / Strings Anzahl der Vorkommen eines Elementes in einer Liste: countEl :: Eq a => a -> [ a ] -> Int countEl b = ( foldr (\x y -> y + 1) 0 ) . filter ( == b ) z.B. countEl ’o’ "foo" = 2 Anzahl der Kleinbuchstaben in einer Zeichenkette: lowers :. String -> Int lowers = ( foldr (\x y -> y + 1) 0 ) . filter ( isLower ) z.B. lowers "Foo !" = 2 Funktionen auf Listen / Strings alle Positionen eines Elementes in einer Liste: positions :: Eq a => a -> [ a ] -> [ Int ] positions x xs = ( map fst ) $ filter (\ ( _ , y) -> y == x ) $ zip [ 0 .. ] xs z.B. positions ’o’ "foo" = [1,2] Rotieren von Listen rotate :: Int -> [ a ] -> [ a ] rotate n xs = drop n xs ++ take n xs Buchstaben-Häufigkeiten Häufigkeiten (in deutschen Texten): haeufigkeitstabelle :: [ Float ] haeufigkeitstabelle = [6.51, 1.89, 3.06, 5.08, 17.4, 1.66, 3.01, 4.76, 7.55, 0.27, 1.21, 3.44, 2.53, 9.78, 2.51, 0.79, 0.02, 7.00, 7.27, 6.15, 4.35, 0.67, 1.89, 0.03, 0.04, 1.13] zip [’a’ .. ’z’] häufigkeitstabelle proz :: Int -> Int -> Float proz m n = (fromIntegral m / fromIntegral n) * 100 Prozentuale Häufigkeit im (verschlüsselten) Text: häufigkeiten :: String -> [ Float ] häufigkeiten t = [ proz ( countEl x t ) n | x <- [ ’a’ .. ’z’ ] ] where n = lowers t Statistik Test auf (annähernd) gleiche Verteilung durch Chi-Quadrat-Test für Buchstabenhäufigkeiten R{0,...,25} I erwartet: e ∈ ≥0 (häufigkeitstabelle) I im Text t aufgetreten: a ∈ (häufigkeiten t) ∀e, a ∈ R{0,...,25} : ≥0 R{0,...,25} ≥0 χ2 (a, e) = n−1 X (ai − ei )2 ei i=0 chiquad :: [ Float ] -> [ Float ] -> Float chiquad a e = foldr (\x y -> x + y) 0 $ zipWith (\ x y -> (x - y)^2 / y) a e chiquad (häufigkeiten "ipiqirx") häufigkeitstabelle Knacken der Verschiebechiffre Chi-Test für alle möglichen Schlüssel k ∈ {0, . . . , 25} für Chiffretext c: chitab = [ chiquad ( rotate k ( häufigkeiten c ) ) häufigkeitstabelle | k <- [ 0 .. 25 ] ] Index (Verschiebung) des kleinsten χ2 -Wertes: k = head ( positions (minimum chitab ) chitab ) where chitab = [ chiquad (rotate n (häufigkeiten c)) häufigkeitstabelle | n <- [ 0 .. 25 ] ] ist (wahrscheinlich) der Schlüssel crack :: String -> crack c = decode k where k = head ( chitab = [ String c positions (minimum chitab ) chitab ) chiquad (rotate n (häufigkeiten c)) häufigkeitstabelle | n <- [0 .. 25]]