Deklarative Programmierung - IMN/HTWK

Werbung
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 )
Herunterladen