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 )