kompletter Foliensatz - HTWK Fakultät Informatik, Mathematik und

Transcription

kompletter Foliensatz - HTWK Fakultät Informatik, Mathematik und
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]]