Modellieren und Implementieren in Haskell

Werbung
Modellieren und Implementieren in Haskell
Folien zu folgenden Lehrveranstaltungen:
Funktionale Programmierung
Funktionales und regelbasiertes Programmieren
Peter Padawitz
TU Dortmund
Tuesday 27th November, 2012
14:43
1
Inhalt
Nur auf Überschriften, nicht auf Seitenzahlen klicken!
1 Vorbemerkungen
8
2 Funktionen
10
3 Listen
18
1 Listenteilung
2 Listenintervall
3 Funktionslifting auf Listen
4 Listenmischung
5 Relationsdarstellung von Funktionen
6 Listenfaltung
7 Strings sind Listen von Zeichen
8 Listenlogik
9 Listenkomprehension
10 Unendliche Listen erzeugende Funktionen
11 Länge eines Linienzuges
2
22
23
24
23
25
26
30
32
33
34
36
12 Minimierung eines Linienzuges
36
13 Pascalsches Dreieck
38
4 Datentypen
41
1 Schema einer Datentypdefinition
41
2 Arithmetische Ausdrücke
44
3 Boolesche Ausdrücke
46
4 Symbolische Differentiation
49
5 Arithmetische Ausdrücke interpretieren
48
6 Hilbertkurven
50
7 Farbkreise
54
5 Termbäume
57
6 Typklassen
61
1 Mengenoperationen auf Listen
62
2 Unterklassen
67
3 Damenproblem
64
4 Quicksort
68
5 Mergesort
68
3
6 Binäre Bäume
70
7 Einlesen
71
8 Ausgeben
75
7 Graphen, Modallogik und Matrizen
77
1 CPOs und Fixpunkte
77
2 Ein Datentyp für Mengen
79
3 Graphen
80
4 Modallogik in funktionaler Form
84
5 Reflexiver und transitiver Abschluss
93
6 Matrizenrechnung
94
8 Weitere Beispiele
105
1 Graphen malen
105
2 Arithmetische Ausdrücke ausgeben
110
3 Arithmetische Ausdrücke reduzieren
111
4 Arithmetische Ausdrücke compilieren
115
9 Monaden
119
1 Von Typen zu Kinds
119
4
2 Monadenklasse und do-Notation
122
3 Die Identitätsmonade
125
4 Die Maybe-Monade
127
5 Monaden-Kombinatoren
130
6 Die Listenmonade
132
7 Direkte Suche in Listen
135
8 Tiefen- und Breitensuche in Bäumen
136
9 Eine Termmonade
138
10 Transitionsmonaden
141
1 Monaden totaler Transitionsfunktionen
142
2 Die IO-Monade
142
3 IOstore
148
4 Monaden partieller Transitionsfunktionen
150
11 Monadische Parser
152
1 Monadische Scanner
152
2 Binäre Bäume parsieren
155
3 Arithmetische Ausdrücke parsieren
155
5
4 Arithmetische Ausdrücke compilieren II
157
5 Testumgebung für den Expr-Compiler
161
12 Felder
165
1 Ix, die Typklasse für Indexmengen
165
2 Matrizenrechnung auf Basis von Feldern
167
3 Dynamische Programmierung
168
4 Alignments
169
13 Konstruktoren und Destruktoren*
178
14 Semantik funktionaler Programme*
182
1 Das relationale Berechnungsmodell
183
2 Das funktionale Berechnungsmodell
189
3 Induktiv definierte Funktionen
197
4 Termination und Konfluenz
202
5 Partiell-rekursive Funktionen
206
6 Schema 3 einer Funktionsdefinition
211
7 Die lazy-evaluation-Strategie
214
8 Auswertung durch Graphreduktion
220
6
15 Unendliche Objekte*
228
16 Verifikation*
232
17 Haskell-Lehrbücher
242
18 Index
243
7
Vorbemerkungen
Die gesternten Kapitel werden in der Bachelor-LV Funktionale Programmierung nicht
behandelt! Vielmehr sind ihre Inhalte Gegenstand der Wahlveranstaltung Funktionales
und regelbasiertes Programmieren (Master und Diplom) sowie zum Teil auch der Wahlveranstaltungen Einführung in den logisch-algebraischen Systementwurf (Bachelor und
Diplom) und Logisch-algebraischer Systementwurf (Master und Diplom).
Interne Links (einschließlich der Seitenzahlen im Index) sind an ihrer braunen Färbung,
externe Links (u.a. zu Wikipedia) an ihrer magenta-Färbung erkennbar. Jede Kapitelüberschrift ist mit dem Inhaltsverzeichnis verlinkt. Namen von Haskell-Modulen sind mit
den jeweiligen Programmdateien verknüpft. Diese Folien dienen dem Vor- (!) und Nacharbeiten von Vorlesungen, können und sollen aber regelmäßige Vorlesungsbesuche nicht
ersetzen!
Suchmaschine für Haskell-Begriffe, -Bibliotheken, -Standardfunktionen, usw.: Hoogle
Der Einstieg in die funktionale Programmierung ist für jede(n) mit anderen Problemen
verbunden:
8
C- oder Java-Programmierer sollten ihnen geläufige Begriffe wie Variable, Zuweisung
oder Prozedur erstmal komplett vergessen und sich von Beginn an auf das Einüben der
i.w. algebraischen Begriffe, die funktionalen Daten- und Programmstrukturen zugrundeliegen, konzentrieren. Erfahrungsgemäß bereiten diese mathematisch geschulten und
von Java, etc. weniger verdorbenen HörerInnen weniger Schwierigkeiten. Ihr Einsatz in
programmiersprachlichen Lösungen algorithmischer Probleme aus ganz unterschiedlichen
Anwendungsbereichen ist aber auch für diese Hörerschaft vorwiegend Neuland.
Diese Folien bilden daher i.w. eine Sammlung prototypischer Programmbeispiele,
auf die, falls sie eingehend studiert und verstanden worden sind, zurückgegriffen werden
kann, wenn später ein dem jeweiligen Beispiel ähnliches Problem funktionalsprachlich
gelöst werden soll. Natürlich werden wichtige Haskell-Konstrukte auch allgemein definiert.
Vollständige formale Definitionen, z.B. in Form von Grammatiken, finden sich hier jedoch
nicht. Dazu wie auch zur allgemeinen Motivation für einzelne Sprachkonstrukte sei auf die
zunehmende Zahl an Lehrbüchern, Tutorials und Sprachreports verwiesen (siehe HaskellLehrbücher und die Webseite Funktionale Programmierung).
Alle Hilfsfunktionen und -datentypen, die in den Beispielen vorkommen, werden auch hier
– manchmal in vorangehenden Abschnitten – eingeführt. Wenn das zum Verständnis nicht
ausreicht und auftretende Fragen nicht in angemessener Zeit durch Zugriff auf andere o.g.
Quellen geklärt werden können, dann stellt die Fragen in der Übung oder auch in der
Vorlesung!
9
Funktionen
Seien A und B Mengen. Neben der Bildung
• des (kartesischen) Produktes A × B (Haskell-Notation: (A,B); s.u.) und
• der Summe (disjunkten Vereinigung) A + B
(Haskell-Notation: data Sum a b = A a | B b; siehe Datentypen)
liefert in Haskell auch die Bildung des
• Funktionenraums A → B (Haskell-Notation: A -> B)
einen Typkonstruktor. Alle Typen von Haskell sind aus Standardtypen, – stets mit
einem Kleinbuchstaben beginnenden (!) – Typvariablen und diesen drei Typkonstruktoren
aufgebaut.
Elemente von A → B werden entweder benannt und durch rekursive Gleichungen definiert (s.u.) oder unbenannt als λ-Abstraktion λp.e (Haskell-Notation: \p -> e) dargestellt. p ist ein Muster für die möglichen Argumente (Parameter) von λp.e, z.B.
p = (x, y). Jede Variable eines Muster darf dort nur einmal vorkommen. e ist der “Rumpf”
von λp.e, also ein Ausdruck, der die Funktionswerte wiedergibt und i.d.R. Variablen von
p enthält.
10
Ein Ausdruck der Form f (e) heißt Funktionsapplikation (auch: Funktionsanwendung
oder -aufruf). Ist f eine λ-Abstraktion, dann nennt man f (e) eine λ-Applikation. Die
λ-Applikation (λp.e)(e0) is auswertbar, wenn der Ausdruck e0 das Muster p matcht. Beim
Matching werden die Variablen von p durch Teilausdrücke von e0 ersetzt.
Die Ersetzung findet in e statt, wobei eine Instanz von e entsteht, die das Ergebnis der
Applikation bildet, z.B.:
(λ(x, y).(x ∗ y + 5 + x))(7, 8);(7 ∗ 8 + 5 + 7);68
Typinferenzregeln
definieren die Typzugehörigkeit eines funktionalen Ausdrucks induktiv über seinem Aufbau.
Allgemeines Schema für Objektkonstruktor c und Typkonstruktor C
e1 :: t1, . . . , en :: tn
c(e1, . . . , en) :: C(t1, . . . , tn)
Hat für alle 1 ≤ i ≤ n der Ausdruck ei den Typ ti, dann hat der Ausdruck c(e1, . . . , en)
den Typ C(t1, . . . , tn).
11
Regeln für Produkte und Funktionstypen
e1 :: t1, . . . , en :: tn
(e1, . . . , en) :: (t1, . . . , tn)
p :: t, e :: t0
λp.e :: t → t0
e :: t → t0, e0 :: t
e(e0) :: t0
Ein geschachelter λ-Ausdruck λp1.λp2. . . . .λpn.e kann in Haskell durch \p1 p2 . . . pn → e
abgekürzt werden.
λp1.λp2. . . . .λpn.e hat einen Typ der Form A1 → (A2 → . . . (An → B) . . . ), wobei man
auf die Klammerung verzichten kann, da Haskell Funktionstypen automatisch rechtsassoziativ klammert.
Anstelle der Angabe eines λ-Ausdrucks kann eine Funktion benannt und dann mit f mit
Hilfe von Gleichungen definiert werden:
f = \p -> e
ist äquivalent zu
f p = e (applikative Definition)
Funktionen, die andere Funktionen als Argumente oder Werte haben, heißen Funktionen
höherer Ordnung. Der Typkonstruktor → ist rechtsassoziativ. Also ist die Deklaration
(+) :: Int -> (Int -> Int)
äquivalent zu
(+) :: Int -> Int -> Int
Die Applikation einer Funktion ist linksassoziativ. Also ist
((+) 5) 6
äquivalent zu
(+) 5 6
12
(+) ist zwar als Präfixfunktion deklariert, kann aber auch infix verwendet werden. Dann
entfallen die runden Klammern um den Infixoperator:
5 + 6
äquivalent zu
(+) 5 6
Dasgleiche gilt für jede Funktion f eines Typs der Form A → B → C. Besteht f aus
Sonderzeichen, dann wird f bei der Präfixverwendung in runde Klammern gesetzt, bei
der Infixverwendung nicht. Beginnt f mit einem Kleinbuchstaben und enthält f keine
Sonderzeichen, dann wird f bei der Infixverwendung in Akzentzeichen gesetzt, bei der
Präfixverwendung nicht:
mod :: Int -> Int -> Int
mod 11 5
ist äquivalent zu
11 `mod` 5
Die Infixnotation wird auch verwendet, um die in f enthaltenen Sektionen (Teilfunktionen) des Typs A → C bzw. B → C zu benennen. Z.B. bezeichnen die Ausdrücke (+5)
und (11‘mod‘) Funktionen des Typs Int → Int. Hier gehören die Klammern zum Namen
der Funktion!
(+5)
ist äquivalent zu
(+)5
ist äquivalent zu
13
\x -> x+5
Der Applikationsoperator
($) :: (a -> b) -> a -> b
f $ a = f a
führt die Anwendung einer gegebenen Funktion auf ein gegebenes Argument durch. Sie
ist rechtsassoziativ und hat unter allen Operationen die niedrigste Priorität. Daher kann
durch Benutzung von $ manch schließende Klammer vermieden werden:
f1 $ f2 $ ... $ fn a
;
f1 (f2 (...(fn a)...)))
Demgegenüber ist der Kompositionsoperator
(.) :: (b -> c) -> (a -> b) -> a -> c
(g . f) a = g (f a)
zwar auch rechtsassoziativ, hat aber – nach den Präfixoperationen – die höchste Priorität.
(f1 . f2 . ... . fn) a
;
f1 (f2 (...(fn a)...)))
U.a. benutzt man den Kompositionsoperator, um in einer applikativen Definition Argumentvariablen einzusparen:
f a b = g (h a) b ist äquivalent zu f a = g (h a) ist äquivalent zu f = g . h
14
f a = g . h a ist äquivalent zu f a = (g.) (h a) ist äquivalent zu f = (g.) . h
Weitere nützliche Funktionserzeuger, -modifizierer und -kombinierer
const :: a -> b -> a
const a b = a
konstante Funktion
update :: Eq a => (a -> b) -> a -> b -> a -> b
update f a b a' = if a == a' then b else f a'
Funktionsupdate
update-Aufrufe, ihre Typen und einige äquivalente Schreibweisen
update f
update f a
:: a -> b -> a -> b
:: b -> a -> b
update f a b
:: a -> b
update f a b a' :: b
15
update(f)
update(f)(a)
(update f) a
update(f)(a)(b)
((update f) a) b
update(f)(a)(b)(a')
(((update f) a) b) a'
flip :: (a -> b -> c) -> b -> a -> c
Vertauschung der Argumente
flip f b a = f a b
flip mod 11 ist äquivalent zu (`mod` 11) (s.o.)
curry :: ((a,b) -> c) -> a -> b -> c
curry f a b = f (a,b)
Kaskadierung (Currying)
uncurry :: (a -> b -> c) -> (a,b) -> c
uncurry f (a,b) = f a b
Dekaskadierung
(***) :: (a -> b) -> (a -> c) -> a -> (b,c)
(f *** g) a = (f a,g a)
Funktionsprodukt
(&&&), (|||) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
(f &&& g) a = f a && g a
Lifting von &&
(f ||| g) a = f a || g a
Lifting von ||
16
Monomorphe und polymorphe Typen
Innerhalb von Typen (die immer Mengen bezeichnen!) bezeichnen Wörter, die mit einem
Kleinbuchstaben beginnen, Typvariablen, während Typnamen wie Bool und Int immer
mit einem Großbuchstaben beginnen. Ein Typ mit Typvariablen wie z.B.
a -> Int -> b
heißt polymorph. Enthält er keine Typvariablen, dann ist er monomorph. Eine Funktion mit poly/monomorphem Typ heißt poly/monomorph. Ein Typ t heißt Instanz eines
Typs u, wenn t durch Ersetzung der Typvariablen von u aus u entsteht.
Typvariablen stehen für Mengen, Individuenvariablen stehen für Elemente einer Menge. Sie müssen ebenfalls mit einem Kleinbuchstaben beginnen.
Eine besondere Individuenvariable ist der Unterstrich _ (auch Wildcard genannt). Er
darf nur auf der linken Seite einer Funktionsdefinition vorkommen, was zur Folge hat, dass
er für einen Teil eines Argumentmusters der gerade definierten Funktion steht, der ihre
Werte an Stellen, die das Muster matchen, nicht beeinflusst (siehe Beispiele im nächsten
Abschnitt).
17
Listen
Sei A eine Menge. Die Menge A∗ der Wörter über A besteht aus Ausdrücken der Form
a1 . . . an mit a1, . . . , an ∈ A. A∗ ist ein rekursiv definierter Summentyp (s.o.). Mehr dazu
im Kapitel Datentypen.
Wörter werden in Haskell als Listen bezeichnet. Das Wort a1 . . . an wird durch [a1, . . . , an]
dargestellt.
[A] bezeichnet den Typ der Listen, deren Elemente den Typ A haben.
Eine n-elementige Liste kann extensional oder als funktionaler Ausdruck dargestellt werden:
[a1, . . . , an] ist äquivalent zu a1 : (a2 : (. . . (an : []) . . . ))
Die Konstante [] (leere Liste) vom Typ [a] und die Funktion (:) (Anfügen eines Elements
ans linke Ende einer Liste) vom Typ a → [a] → [a] heißen Konstruktoren, weil sie nicht
wie andere Funktionen Werte berechnen, sondern dazu dienen, die Elemente eines Typs
zu aufzubauen. Auch werden sie benötigt, um die Zugehörigkeit eines Elementes eines
Summentyps zu einem bestimmten Summanden wiederzugeben (siehe Datentypen).
Die Klammern in a1 : (a2 : (. . . (an : []) . . . )) können weggelassen werden, weil der Typ
von (:) keine andere Klammerung zulässt.
18
Die durch mehrere Gleichungen ausgedrückten Fallunterscheidungen bei den folgenden
Definitionen von Funktionen auf Listen ergeben sich aus verschiedenen Mustern der
Funktionsargumente:
Seien x, y, s Individuenvariablen. s ist ein Muster für alle Listen, [] das Muster für die leere
Liste, [x] ein Muster für alle einelementigen Listen, x : s ein Muster für alle nichtleeren
Listen, x : y : s ein Muster für alle mindestens zweielementigen Listen, usw.
length :: [a] -> Int
length (_:s) = length s+1
length _
= 0
length [3,2,8,4] ; 4
head :: [a] -> a
head (a:_) = a
head [3,2,8,4] ; 3
tail :: [a] -> [a]
tail (_:s) = s
tail [3,2,8,4] ; [2,8,4]
(++) :: [a] -> [a] -> [a]
(a:s)++s' = a:(s++s')
_++s
= s
[3,2,4]++[8,4,5] ; [3,2,4,8,4,5]
19
(!!) :: [a] -> Int -> a
(a:_)!!0
= a
(_:s)!!n | n > 0 = s!!(n-1)
[3,2,4]!!1 ; 2
init :: [a] -> [a]
init [_]
= []
init (a:s) = a:init s
init [3,2,8,4] ; [3,2,8]
last :: [a] -> a
last [a]
= a
last (_:s) = last s
last [3,2,8,4] ; 4
take
take
take
take
:: Int -> [a] -> [a]
take 3 [3,2,4,8,4,5] ; [3,2,4]
0 _
= []
n (a:s) | n > 0 = a:take (n-1) s
_ []
= []
drop
drop
drop
drop
:: Int -> [a] -> [a]
0 s
= s
n (_:s) | n > 0 = drop (n-1) s
_ []
= []
20
drop 4 [3,2,4,8,4,5] ; [4,5]
takeWhile :: (a -> Bool) -> [a] -> [a]
takeWhile f (a:s) = if f a then a:takeWhile f s else []
takeWhile f _
= []
takeWhile (<4) [3,2,8,4] ; [3,2]
dropWhile :: (a -> Bool) -> [a] -> [a]
dropWhile f s@(a:s') = if f a then dropWhile f s' else s
dropWhile f _
= []
dropWhile (<4) [3,2,8,4] ; [8,4]
updList :: [a] -> Int -> a -> [a]
updList [3,2,8,4] 2 9 ; [3,2,9,4]
updList s i a = take i s++a:drop (i+1) s
21
Beispiel Listenteilung
(beim n-ten Element bzw. beim ersten Element, das f nicht erfüllt)
splitAt
splitAt
splitAt
splitAt
:: Int -> [a] -> ([a],[a])
0 s
= ([],s)
_ []
= ([],[])
n (a:s) | n > 0 = (a:s1,s2)
where (s1,s2) = splitAt (n-1) s
span :: (a -> Bool) -> [a] -> ([a],[a])
span f s@(a:s') = if f a then (a:s1,s2) else ([],s)
where (s1,s2) = span f s'
span _ []
= ([],[])
22
Beispiel Listenintervall
(Teilliste vom i-ten bis zum j-ten Element)
sublist
sublist
sublist
sublist
sublist
:: [a] -> Int -> Int -> [a]
(a:_) 0 0
=
(a:s) 0 j | j > 0
=
(_:s) i j | i > 0 && j > 0 =
_ _ _
=
[a]
a:sublist s 0 $ j-1
sublist s (i-1) $ j-1
[]
Beispiel Listenmischung
Sind s1 und s2 zwei geordnete Listen mit eindeutigen Vorkommen ihrer jeweiligen Elemente, dann ist auch merge(s1, s2) eine solche Liste – was nicht heißt, dass merge nicht
auch auf anderen Listen definiert ist.
merge :: [Int] -> [Int] -> [Int]
merge s1@(x:s2) s3@(y:s4) | x < y = x:merge s2 s3
| x > y = y:merge s1 s4
| True = merge s1 s4
merge [] s = s
merge s _ = s
23
Funktionslifting auf Listen
map :: (a -> b) -> [a] -> [b]
map f (a:s) = f a:map f s
map _ _
= []
map (+1) [3,2,8,4]
; [4,3,9,5]
map ($ 7) [(+1),(+2),(*5)] ; [8,9,35]
map ($ a) [f1,f2,...,fn]
; [f1 a,f2 a,...,fn a]
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:s) (b:s') = f a b:zipWith f s s'
zipWith _ _ _
= []
zipWith (+) [3,2,8,4] [8,9,35] ; [11,11,43]
zip :: [a] -> [b] -> [(a,b)]
zip = zipWith (,)
zip [3,2,8,4] [8,9,35] ; [(3,8),(2,9),(8,35)]
24
Relationsdarstellung von Funktionen
Funktionen lassen sich auch als Liste ihrer (Argument,Wert)-Paare implementieren. Eine Funktionsanwendung wird als Listenzugriff mit Hilfe der Standardfunktion lookup
implementiert, ein Funktionsupdate als Listenupdate mit updRel:
lookup :: Eq a => [(a,b)] -> a -> Maybe b
lookup ((a,b):r) c = if a == c then Just b else lookup r c
lookup _ _
= Nothing
updRel :: Eq a => [(a,b)] -> a -> b -> [(a,b)]
updRel ((a,b):r) c d = if a == c then (a,d):r else (a,b):updRel r c d
updRel _ a b
= [(a,b)]
Der weiter unten näher behandelte Datentyp Maybe(b) mit der Typvariablen b enthält
außer den – in den Konstruktor Just eingebetteten – Elementen der a zugeordneten
Menge das Element Nothing. Eine Funktion f : a → Maybe(b) ist insofern partiell, als
ihr Wert Nothing an der Stelle a als Undefiniertheit von f an diser Stelle interpretiert
wird.
25
Beschränkte Iterationen (for-Schleifen) entsprechen meistens Listenfaltungen.
Faltung einer Liste von links her
f
f
f
foldl f a [b1,b2,b3,b4,b5]
f
f
a
b1
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f a (b:s) = foldl f (f a b) s
foldl _ a _
= a
b2
b3
b4
b5
a ist Zustand, b ist Eingabe
f ist Zustandsüberführung
foldl1 :: (a -> a -> a) -> [a] -> a
foldl1 f (a:s) = foldl f a s
sum
and
minimum
concat
=
=
=
=
foldl (+) 0
foldl (&&) True
foldl1 min
foldl (++) []
product
or
maximum
concatMap f
26
=
=
=
=
foldl (*) 1
foldl (||) False
foldl1 max
concat . map f
Beispiel Berechnung der Liste aller geordneten Partitionen einer Liste
partsL :: [a] -> [[[a]]]
partsL [a]
= [[[a]]]
partsL (a:s) = concatMap glue $ partsL s
where glue part@(s:rest) = [[a]:part,(a:s):rest]
Beispiel Berechnung der Menge aller Partitionen einer Menge
parts :: [a] -> [[[a]]]
parts [a]
= [[[a]]]
parts (a:s) = concatMap (glue []) $ parts s
where glue part (s:rest) = ((a:s):part++rest):
glue (s:part) rest
glue part _
= [[a]:part]
27
Parallele Faltung zweier Listen von links her
f
f
fold2 f a [b1,b2,b3,b4,b5] [c1,c2,c3,c4,c5]
f
f
f
a
b1
c1
b2
c2
b3
c3
b4
c4
b5
c5
fold2 :: (a -> b -> c -> a) -> a -> [b] -> [c] -> a
fold2 f a (b:s) (c:s') = fold2 f (f a b c) s s'
fold2 _ a _ _
= a
listsToFun :: Eq a => b -> [a] -> [b] -> a -> b
listsToFun = fold2 update . const
Beginnend mit const b, erzeugt listsToFun b schrittweise aus einer Argumentliste as
und einer Werteliste bs die entsprechende Funktion:
bs!!i falls i = max{k | as!!k = a, k < length(bs)},
listsToFun b as bs a =
b
sonst.
28
Faltung einer Liste s von rechts her = Auswertung der Konstruktordarstellung von
s in einer Algebra (Interpretation der Konstruktoren durch auswertbare Funktionen)
foldr f a [b1,b2,b3,b4,b5]
f
:
f
f
:
f
f
:
f
f
:
f
f
b1
b2
b3
b4
b5
:
a
b1
b2
b3
b4
b5
f
[]
a
foldr :: (b -> a -> a) -> a -> [b] -> a
foldr f a (b:s) = f b $ foldr f a s
foldr _ a _
= a
Beispiel Horner-Schema zur Berechnung von Polynomwerten
Der Wert von b0 + b1 ∗ x + b2 ∗ x2 + · · · + bn−1 ∗ xn−1 + bn ∗ xn ist das Ergebnis der folgenden
Faltung:
b0 + (b1 + (b2 + · · · + (bn−1 + bn ∗ x) ∗ x . . . ) ∗ x) ∗ x
horner :: [Float] -> Float -> Float
horner bs x = foldr f (last bs) (init bs) where f b a = b+a*x
29
Strings sind Listen von Zeichen
Strings werden als Listen von Zeichen betrachtet, d.h. die Typen String und [Char]
sind identisch:
"Hallo" == ['H','a','l','l','o'] = True
Alle Listenfunktionen sind daher auch für Strings verwendbar.
words :: String -> [String] und unwords :: [String] -> String zerlegen bzw.
konkatenieren Strings, wobei Leerzeichen, Zeilenumbrüche ('\n') und Tabulatoren ('\t')
als Trennsymbole fungieren.
unwords fügt Leerzeichen zwischen die zu konkatenierenden Strings.
lines :: String -> [String] und unlines :: [String] -> String zerlegen bzw.
konkatenieren Strings, wobei nur Zeilenumbrüche als Trennsymbole fungieren.
unlines fügt '\n' zwischen die zu konkatenierenden Strings.
30
Der Applikationsoperator $ als Parameter von Listenfunktionen
foldr ($) a [f1,f2,f3,f4]
;
f1 $ f2 $ f3 $ f4 a
foldl (flip ($)) a [f4,f3,f2,f1]
;
f1 $ f2 $ f3 $ f4 a
map f [a1,a2,a3,a4]
map ($a) [f1,f2,f3,f4]
;
;
[f a1,f a2,f a3,f a4]
[f1 a,f2 a,f3 a,f4 a]
zipWith ($) [f1,f2,f3,f4] [a1,a2,a3,a4]
; [f1 a1,f2 a2,f3 a3,f4 a4]
31
Listenlogik
any :: (a -> Bool) -> [a] -> Bool
any f = or . map f
any (>4) [3,2,8,4] ; True
all :: (a -> Bool) -> [a] -> Bool
all f = and . map f
all (>2) [3,2,8,4] ; False
elem :: Eq a => a -> [a] -> Bool
elem a = any (a ==)
elem 2 [3,2,8,4] ; True
notElem :: Eq a => a -> [a] -> Bool
notElem a = all (a /=)
notElem 9 [3,2,8,4] ; True
filter :: (a -> Bool) -> [a] -> [a]
filter f (a:s) = if f a then a:filter f s else filter f s
filter f _
= []
filter (<8) [3,2,8,4] ; [3,2,4]
32
Jeder Aufruf von map, zipWith, filter oder einer Komposition dieser Funktionen entspricht einer Listenkomprehension:
map f s
= [f a | a <- s]
zipWith f s s' = [f a b | (a,b) <- zip s s']
filter f s
= [a | a <- s, f a]
zip(s)(s’) ist nicht das kartesische Produkt von s und s’. Dieses entspricht der Komprehension [(a,b) | a <- s, b <- s’].
Allgemeines Schema von Listenkomprehensionen:
[e(x1, . . . , xn) | x1 ← s1, . . . , xn ← sn, be(x1, . . . , xn)] :: [a]
• x1, . . . , xn sind Variablen,
• s1, . . . , sn sind Listen,
• e(x1, . . . , xn) ist ein Ausdruck des Typs a,
• xi ← si heißt Generator und steht für xi ∈ si,
• be(x1, . . . , xn) heißt Guard und ist ein Boolescher Ausdruck.
Jede endlichstellige Relation lässt sich als Listenkomprehension implementieren, z.B. die
Menge aller Tripel (a, b, c) ∈ A1 × A2 × A3, die ein Prädikat p : A1 × A2 × A3 → Bool
erfüllen, durch [(a,b,c) | a <- a1, b <- a2, c <- a3, p(a,b,c)].
33
Unendliche Listen erzeugende Funktionen
Standardfunktionen:
repeat :: a -> [a]
repeat a = a:repeat a
repeat 5
replicate :: Int -> a -> [a]
replicate n a = take n $ repeat a
replicate 4 5
iterate :: (a -> a) -> a -> [a]
iterate f a = a:iterate f $ f a
iterate (+1) 5
take 4 $ iterate (+1) 5
;
;
5:5:5:5:...
;
;
[5,5,5,5]
5:6:7:8:...
[5,6,7,8]
Beispiele aus der Datei Lazy.hs:
blink :: [Int]
blink = 0:1:blink
blink
nats :: Int -> [Int]
nats n = n:map (+1) (nats n)
nats 3
34
;
;
0:1:0:1:...
3:4:5:6:...
fibs :: Int -> [Int]
fibs = 1:tailfibs where tailfibs = 1:zipWith (+) fibs tailfibs
take 11 fibs
;
[1,1,2,3,5,8,13,21,34,55,89]
primes :: [Int]
primes = sieve $ nats 2
sieve :: [Int] -> [Int]
sieve (p:s) = p:sieve [n | n <- s, n `mod` p /= 0]
take 11 prims
;
[2,3,5,7,11,13,17,19,23,29,31]
hamming :: [Int]
hamming = 1:map (*2) hamming `merge` map (*3) hamming `merge`
map (*5) hamming
take 30 hamming
;
[1,2,3,4,5,6,8,9,10,12,15,16,18,20,24,25,27,
30,32,36,40,45,48,50,54,60,64,72,75,80]
35
Beispiel Länge eines Linienzuges
type Point = (Float,Float)
type Path = [Point]
length :: Path -> Float
length ps = sum $ zipWith distance ps $ tail ps
distance :: Point -> Point -> Float
distance (x1,y1) (x2,y2) = sqrt $ (x2-x1)^2+(y2-y1)^2
Beispiel Minimierung eines Linienzuges
minimize :: Path -> Path
minimize (p:ps@(q:r:s)) | straight p q r = minimize $ p:r:s
| True
= p:minimize ps
minimize ps
= ps
36
straight p q r prüft, ob die Punkte p, q und r auf einer Geraden liegen:
straight :: Point -> Point -> Point -> Bool
straight (x1,y1) (x2,y2) (x3,y3) = x1 == x2 && x2 == x3 ||
x1 /= x2 && x2 /= x3 &&
(y2-y1)/(x2-x1) == (y3-y2)/(x3-x2)
Soll der Linienzug geglättet werden, dann ist es ratsam, ihn vorher zu minimieren, da
sonst unerwünschte Plateaus in der resultierenden Kurve verbleiben:
Zwei geglättete Linienzüge links vor und rechts nach der Minimierung
37
Beispiel Das Pascalsche Dreieck stellt eine Aufzählung der Binomialkoeffizienten dar.
Für alle n ∈ N, pascal(n)!!0 = pascal(n)!!n = 1
(1)
Für alle n, k > 0, pascal(n)!!k = pascal(n − 1)!!(k − 1) + pascal(n − 1)!!k (2)
pascal(n)!!k, der Wert an der Stelle (n, k) des Dreiecks, ist die Anzahl der k-elementigen
Teilmengen einer n-elementigen Menge.
pascal :: Int -> [Int]
pascal 0 = [1]
pascal n = zipWith (+) (s++[0]) (0:s) where s = pascal $ n-1
38
Die Binomialfunktion ist eine Lösung der Gleichungen (1) und (2):
binom :: Int -> Int -> Int
binom n k = product[k+1..n]`div`product[1..n-k]
n!
=
=
k!(n − k)!
n
k
Da Gleichungsysteme wie (1) und (2) eindeutige Lösungen haben (Beweis durch strukturelle Induktion), gilt für alle n ∈ N und k ≤ n:
pascal(n)!!k = binom(n)(k).
Mit der Binomialfunktion lässt sich u.a. die Anzahl der Partitionen einer n-elementigen
Menge berechnen:
partsno 0 = 1
partsno n = sum $ map f [0..n-1] where f i = binom (n-1) i*partsno i
partsnos = map partsno [1..10] ; [1,2,5,15,52,203,877,4140,21147,115975]
39
Die obige Darstellung des Pascalschen Dreiecks ist übrigens die mit Expander2 erzeugte
graphische Interpretation des Ausdrucks
shelf(1) $ box(1):map(rows)[1..10]
wobei
box(x) = turtle[rect(15,11),text x]
rows(n) = shelf(n+1) $ map(box) $ pascal(n)
und shelf(n)(s) die Liste s in Listen der Länge n aufteilt und diese zentriert stapelt.
40
Datentypen
Zunächst das allgemeine Schema einer Datentypdefinition:
data DT a1 ... am = Constructor_1 e11 ... e1n_1 | ... |
Constructor_k ek1 ... ekn_k
e11,...,ekn_k sind beliebige Typausdrücke, die aus Typkonstanten (z.B. Int), Typvariablen a1,...,am und Typkonstruktoren wie ->, Produktbildung oder Listenbildung
zusammengesetzt sind.
Kommt DT selbst in einem dieser Typausdrücke vor, dann spricht man von einem rekursiven Datentyp.
Für alle 1 ≤ i ≤ n sei ei eine monomorphe Instanz der Typvariablen ai. Dann ist jedes
Element von DT e1 ... em ein funktionaler Ausdruck der Form
Constructor_i ti1 ... tin_1,
wobei 1 ≤ i ≤ k und tij ein Element vom Typ eij ist. M.a.W., Constructor_i hat
den Typ
ei1 -> ... -> ein_i -> DT a1 ... am.
41
Ein Konstruktor ist zwar eine Funktion. Er verändern aber seine Argumente nicht, sondern fasst sie nur zusammen. Den Zugriff auf ein einzelnes Argument erreicht man durch
Einführung von Attributen, eines für jede Argumentposition des Konstruktors. In der
Definition von DT wird Constructor_i ei1 ... ein_i ersetzt durch
Constructor_i {attribute_i1 :: ei1, ..., attribute_in_i :: ein_i}.
Z.B. lassen sich die aus anderen Programmiersprachen bekannten Recordtypen und
Objekt-Klassen als Datentypen mit genau einem Konstruktor, aber mehreren Attributen
implementieren.
Wie ein Konstruktor, so ist auch ein Attribut eine Funktion. Als solche hat attribute_ij
den Typ
attribute_ij :: DT a1 ... am -> eij
Attribute sind also invers zu Konstruktoren. Man nennt sie deshalb auch Destruktoren.
attribute_ij (Constructor_i t1 ... tn_i)
42
hat den Wert
tj.
Die Objektdefinition
obj = Constructor_i t1 ... tn_i
ist äquivalent zu
obj = Constructor_i {attribute_i1 = t1, ..., attribute_in_i = tn_i}
Attribute dürfen nicht rekursiv definiert werden. Folglich deutet Haskell das Vorkommen
von attribute_ij auf der rechten Seite einer Definitionsgleichung als eine vom gleichnamigen Attribut verschiedene Funktion und sucht nach deren Definition. Diese Tatsache
kann man nutzen, um attribute_ij doch rekursiv zu definieren: An die obige Objektdefinition wird einfach die Zeile
where attribute_ij = tj
angefügt.
Derselbe Konstruktor darf nicht zu mehreren Datentypen gehören. Dasselbe Attribut darf
nicht zu mehreren Konstruktoren gehören.
Die Werte von Attributen eines Objektes können wie folgt verändert werden:
obj' = obj {attribute_ij = t, attribute_ik = t', ...}
43
Arithmetische Ausdrücke (Der Haskell-Code steht hier.)
data Expr = Con Int | Var String | Sum [Expr] | Prod [Expr] |
Expr :- Expr | Int :* Expr | Expr :^ Int
oder:
data Expr where Con
Var
Sum
Prod
(:-)
(:*)
(:^)
::
::
::
::
::
::
::
Int -> Expr
String -> Expr
[Expr] -> Expr
[Expr] -> Expr
Expr -> Expr -> Expr
Int -> Expr -> Expr
Expr -> Int -> Expr
Z.B. lautet der Ausdruck 5*11+6*12+x*y*z als Objekt vom Typ Expr folgendermaßen:
Sum [5 :* Con 11,6 :* Con 12,Prod [Var "x",Var "y",Var "z"]]
44
45
Beispiel Boolesche Ausdrücke
data BExpr = True_ | False_ | BVar String | Or [BExpr] | And [BExpr] |
Not BExpr | Expr :< Expr | Expr := Expr | Expr :<= Expr
oder
data BExpr where True_
False_
BVar
Or
And
Not
(:<)
(:=)
(:<=)
::
::
::
::
::
::
::
::
::
BExpr
BExpr
String -> BExpr
[BExpr] -> BExpr
[BExpr] -> BExpr
BExpr -> BExpr
Expr -> Expr -> BExpr
Expr -> Expr -> BExpr
Expr -> Expr -> BExpr
46
Beispiel Arithmetische, Boolesche und bedingte Ausdrücke, Paare und Listen
von Ausdrücken, ...
data Exp a where Con
Var
Sum
Prod
(:-)
(:*)
(:^)
True_
False_
Or
And
Not
(:<)
(:=)
(:<=)
If
Pair
List
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
::
Int -> Exp Int
String -> Exp a
[Exp Int] -> Exp Int
[Exp Int] -> Exp Int
Exp Int -> Exp Int -> Exp Int
Int -> Exp Int -> Exp Int
Exp Int -> Int -> Exp Int
Exp Bool
Exp Bool
[Exp Bool] -> Exp Bool
[Exp Bool] -> Exp Bool
Exp Bool -> Exp Bool
Exp Int -> Exp Int -> Exp Bool
Exp Int -> Exp Int -> Exp Bool
Exp Int -> Exp Int -> Exp Bool
Exp Bool -> Exp a -> Exp a -> Exp a
Exp a -> Exp b -> Exp (a,b)
[Exp a] -> Exp [a]
47
Funktionen mit Argumenten eines Datentyps werden in Abhängigkeit vom jeweiligen
Muster der Argumente definiert:
Arithmetische Ausdrücke interpretieren
type Store = String -> Int
evalE
evalE
evalE
evalE
evalE
evalE
evalE
evalE
(Belegung der Variablen)
:: Expr -> Store -> Int
(Con i) _
= i
(Var x) st
= st x
(Sum es) st = sum $ map (flip evalE st) es
(Prod es) st = product $ map (flip evalE st) es
(e :- e') st = evalE e st - evalE e' st
(i :* e) st = i * evalE e st
(e :^ i) st = evalE e st ^ i
Eine Testumgebung für evalE wird hier vorgestellt.
48
Symbolische Differentiation
diff
diff
diff
diff
diff
:: String -> Expr -> Expr
x (Con _)
= zero
x (Var y)
= if x == y then one else zero
x (Sum es) = Sum $ map (diff x) es
x (Prod es) = Sum $ map f [0..length es-1]
where f i = Prod $ updList es i $ diff x $ es!!i
diff x (e :- e') = diff x e :- diff x e'
diff x (i :* e) = i :* diff x e
diff x (e :^ i) = i :* Prod [diff x e,e:^(i-1)]
zero = Con 0
one = Con 1
49
Hilbertkurven
Hilbertkurven der Tiefen 1, 2, 3 und 5. Man erkennt die auf gleicher Rekursionstiefe
erzeugten Punkte daran, dass sie mit Linien gleicher Farbe verbunden sind.
50
Linienzüge wie die Hilbertkurven lassen sich nicht nur als Punktlisten (s.o.), sondern
auch als Listen von Aktionen repräsentieren, die auszuführen sind, um einen Linienzug
zu zeichnen. Ein Schritt von einem Punkt zum nächsten erfordert die Drehung um einen
Winkel α (Turn α) und die anschließende Vor- bzw. Rückwärtsbewegung um eine Distanz
d (Move d).
data Action = Turn Float | Move Float
Welche Aktionen auszuführen sind, hängt bei Hilbertkurven von einem Richtungsparameter ab, dessen Werte durch folgenden Datentyp repräsentiert sind:
data Direction = North | East | South | West
move :: Direction -> [Action]
move dir = case dir of North -> [Turn (-90),Move 1,Turn 90]
East -> [Move 1]
South -> [Turn 90,Move 1,Turn (-90)]
West -> [Move (-1)]
Die Aktionsliste für eine Hilbertkurve der Tiefe n wird aus den Aktionslisten von vier
Hilbertkurven der Tiefe n − 1 konstruiert:
51
hilbertActs :: Int -> [Action]
hilbertActs n = f n East
where f :: Int -> Direction -> [Action]
f 0 _
= []
f n dir = g sdir++move dir++g dir++move sdir++
g dir++move (flip dir)++g (flip sdir)
where g = f $ n-1; sdir = swap dir
flip,swap :: Direction -> Direction
flip dir = case dir of North -> South; East -> West; South -> North
West -> East
swap dir = case dir of North -> West; East -> South; South -> East
West -> North
Die Aufrufe move dir, move (swap dir) und move (flip dir) erzeugen die Aktionen
zum Zeichnen der Linie, welche die erste mit der zweiten, die zweite mit der dritten
bzw. die dritte mit der vierten Teilkurve verbindet. Die Rolle der Anfangspunkte der
von hilbert berechneten vier Teilkurven wird hier von move-Befehlen übernommen, die
von einer Teilkurve zur nächsten führen. Die Änderungen des Direction-Parameters dir
beim Aufruf von g = f (n − 1) bzw. move lassen sich recht gut durch den Vergleich der
Hilbertkurven der Tiefen 1, 2 und 3 (s.o.) nachvollziehen.
52
Mit Hilfe von foldl (s.o.) kann eine Aktionsliste vom Typ [Action] leicht in eine Punktliste vom Typ Path (s.o.) überführt werden:
hilbertPoints :: Int -> Path
hilbertPoints = executeActs . hilbertActs
executeActs :: [Action] -> Path
executeActs = fst . foldl f ([(0,0)],0)
where f (ps,a) (Move d) = (ps++[successor (last ps) d a],a)
f (ps,a) (Turn b) = (ps,a+b)
successor p d a = (x+d*cos deg,y+d*sin deg,a)
where deg = a*pi/180
53
Farbkreise
Zur Repräsentation von Farben wird häufig der folgende Datentyp verwendet:
data RGB = RGB Int Int
red
= RGB 255 0 0;
cyan = RGB 0 255 255;
black = RGB 0 0 0;
Int
magenta = RGB 255 0 255; blue
= RGB 0 255 0
green
= RGB 0 0 255;
yellow = RGB 255 255 0
white
= RGB 255 255 255
Zwischen den sechs Grundfarben Rot, Magenta, Blau, Cyan, Grün und Gelb liegen weitere
sog. reine oder Hue-Farben. Davon lassen sich mit dem Datentyp RGB also insgesamt
1530 darstellen und mit folgender Funktion aufzählen:
nextCol
nextCol
nextCol
nextCol
nextCol
nextCol
nextCol
:: RGB -> RGB
(RGB 255 0 n)
(RGB n 0 255)
(RGB 0 n 255)
(RGB 0 255 n)
(RGB n 255 0)
(RGB 255 n 0)
|
|
|
|
|
|
n
n
n
n
n
n
<
>
<
>
<
>
255
0
255
0
255
0
=
=
=
=
=
=
RGB
RGB
RGB
RGB
RGB
RGB
54
255 0 (n+1)
(n-1) 0 255
0 (n+1) 255
0 255 (n-1)
(n+1) 255 0
255 (n-1) 0
Rot bis Magenta
Magenta bis Blau
Blau bis Cyan
Cyan bis Grün
Grün bis Gelb
Gelb bis Rot
Lässt man den Konstruktor RGB weg, dann besteht der Definitionsbereich Def (nextcol)
von nextCol aus allen Tripeln (r, g, b) ∈ {0, . . . , 255}3 mit 0, 255 ∈ {r, g, b}. Diese Tripel
entsprechen gerade den o.g. Hue-Farben, während jedes Element von {0, . . . , 255}3 eine
aufgehellte bzw. abgedunkelte Variante einer Hue-Farbe repräsentiert.
Ausgehend von einer Startfarbe liefert die Iteration von nextCol einen Kreis von
|Def (nextcol)| = 1530 Hue-Farben. Damit können den Elementen jeder Liste mit n ≤
1530 Elementen n verschiedene – bzgl. des Farbkreises äquidistante – Hue-Farben zugeordnet werden:
addColor :: [a] -> [(a,RGB)]
addColor s = zip s $ map f [0..]
where f i = iterate nextCol red!!
round (float i*1530/float (length s))
Z.B. ordnet addColor den jeweiligen Elementen einer 11-elementigen Liste die folgenden
Farben zu:
55
Anwendung von addColor auf die Hilbertkurve der Tiefe 5
56
Termbäume
Wir definieren Bäume mit beliebigem endlichen Knotenausgrad und zwei Blatttypen
f und a. f ist auch der Typ der inneren Knoten und wird oft durch einen Typ von
Funktionsnamen instanziiert. a wird oft durch einen Typ (substituierbarer) Variablen
instanziiert. Der Haskell-Code steht hier.
data Term f a = F f [Term f a] | V a
root :: Term a a -> a
root (F a _) = a
root (V a)
= a
subterms :: Term a a -> [Term a a]
subterms (F _ ts) = ts
subterms t
= []
t :: Term Int Int
t = F 1 [F 2 [F 2 [V 3 ,V (-1)],V (-2)],F 4 [V (-3),V 5]]
subterms t
;
[F 2 [F 2 [V 3,V (-1)],V (-2)],F 4 [V (-3),V 5]]
57
size,height :: Term f a -> Int
size (F _ ts) = sum (map size ts)+1
size _
= 1
height (F _ ts) = foldl max 0 (map height ts)+1
height _
= 1
nodes :: Term f a -> [[Int]]
nodes (F _ ts) = []:concat (zipWith f ts [0..])
where f t i = map (i:) $ nodes t
nodes _
= [[]]
t :: Term Int Int
t = F 1 [F 2 [F 2 [V 3 ,V (-1)],V (-2)],F 4 [V (-3),V 5]]
positions t
;
[[],[0],[0,0],[0,0,0],[0,0,1],[0,1],[1],[1,0],[1,1]]
58
getSubterm t p liefert den Unterbaum von t, dessen Wurzel an Position p von t steht,
putSubterm t p u ersetzt ihn durch den Baum u:
getSubterm :: Term f a -> [Int] -> Term f a
getSubterm t [] = t
getSubterm (F _ ts) (i:is) | i < length ts
= getSubterm (ts!!i) is
getSubterm _ _ = error "getSubterm"
putSubterm :: Term f a a -> [Int] -> Term f a -> Term f a
putSubterm t [] u = u
putSubterm (F a ts) (i:is) u | i < length ts
= F a $ updList ts i $ putSubterm (ts!!i) is u
putSubterm _ _ _ = error "getSub"
59
mapTerm h t wendet h auf die Markierungen der F-Knoten von t an:
mapTerm :: (f -> g) -> Term f a -> Term g a
mapTerm h (F f ts) = F (h f) $ map (mapTerm h) ts
mapTerm _ (V a)
= V a
foldT h t wertet t aus gemäß einer durch h gegebenen Interpretation der Markierungen
der F-Knoten von t durch Funktionen des Typs [a] -> a:
foldT :: (f -> [a] -> a) -> Term f a -> a
foldT h (F f ts) = h f $ map (foldT h) ts
foldT _ (V a)
= a
h :: String -> [Int] -> Int
h "+" = sum
h "*" = product
t :: Term String Int
t = F "+" [F "*" $ map V [2..6], V 55]
foldT h t
;
775
60
Typklassen
stellen Bedingungen an die Instanzen einer Typvariablen. Die Bedingungen bestehen in
der Existenz bestimmter Funktionen, z.B.
class Eq a where (==), (/=) :: a -> a -> Bool
(/=) = (not .) . (==)
Eine Instanz einer Typklasse besteht aus den Instanzen ihrer Typvariablen sowie Definitionen der in ihr deklarierten Funktionen.
instance Eq (Int,Bool) where (x,b) == (y,c) = x == y && b == c
instance Eq a => Eq [a]
where s == s' = length s == length s' && and $ zipWith (==) s s'
Auch (/=) könnte hier definiert werden. Die Definition von (/=) in der Klasse Eq als
Negation von (==) ist nur ein Default!
Der Typ jeder Funktion einer Typklasse muss die - in der Regel eine - Typvariable der
Typklasse mindestens einmal enthalten. Sonst wäre die Funktion ja gar nicht von (der
jeweiligen Instanz) der Typvariable abhängig.
61
Beispiel Mengenoperationen auf Listen
insert :: Eq a => a -> [a] -> [a]
insert a s@(b:s') = if a == b then s else b:insert a s'
insert a _
= [a]
union :: Eq a => [a] -> [a] -> [a]
union = foldl $ flip insert
Mengenvereinigung
unionMap :: Eq b => (a -> [b]) -> [a] -> [b]
unionMap f = foldl union [] . map f
concatMap für Mengen
inter :: Eq a => [a] -> [a] -> [a]
inter = filter . flip elem
Mengendurchschnitt
remove :: Eq a => a -> [a] -> [a]
remove = filter . (/=)
diff :: Eq a => [a] -> [a] -> [a]
diff = foldl $ flip remove
62
Mengendifferenz
subset :: Eq a => [a] -> [a] -> Bool
s `subset` s' = all (`elem` s') s
Mengeninklusion
eqset :: Eq a => [a] -> [a] -> Bool
s `eqset` s' = s `subset` s' && s' `subset` s
Mengengleichheit
powerset :: Eq a => [a] -> [[a]]
Potenzmenge
powerset (a:s) = if a `elem` s then ps else ps ++ map (a:) ps
where ps = powerset s
powerset _
= [[]]
63
Beispiel Damenproblem
queens 5
;
64
Zwischenwerte einer relationalen Version von queens 4
65
queens :: Int ->
queens n = board
where board ::
board []
board xs
[[Int]]
[1..n]
[Int] -> [[Int]]
= [[]]
= [x:ys | x <- xs,
ys <- board $ remove xs,
and $ zipWith (safe x) ys [1..]]
safe :: Int -> Int -> Int -> Bool
safe x y i = x /= y+i && x /= y-i
Das Argument xs von board repräsentiert eine Menge möglicher Spaltenpositionen von
Damen, die board schrittweise den Zeilen eines Schachbretts zuordnet:
Sei n = length(xs). ys ∈ Nn wird als (n × n)-Schachbrett interpretiert, auf dem an der
Position (i, j) genau dann eine Dame steht, wenn j = ys!!i ist. board(xs) berechnet alle
Permutationen ys von xs derart, dass sich – bzgl. der Schachbrett-Interpretation von ys
– keine zwei Damen schlagen können.
66
Unterklassen
Typklassen können wie Objektklassen in OO-Sprachen andere Typklassen erben. Die
jeweiligen Oberklassen werden vor dem Erben vor dem Pfeil => aufgelistet.
class Eq a => Ord a where (<=), (<), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a
a < b
= a <= b && a /= b
a >= b = b <= a
a > b
= b < a
max x y = if x >= y then x else y
min x y = if x <= y then x else y
67
Beispiel Quicksort
quicksort :: Ord a => [a] -> [a]
quicksort (x:s) = quicksort (filter (<= x) s)++x:
quicksort (filter (> x) s)
quicksort s
= s
Quicksort ist ein typischer divide-and-conquer-Algorithmus mit mittlerer Laufzeit
O(n ∗ log2(n)), wobei n die Listenlänge ist. Wegen der 2 rekursiven Aufrufe in der Definition von quicksort ist log2(n) die (mittlere) Anzahl der Aufrufe von quicksort. Wegen
des einen rekursiven Aufrufs in der Definition der conquer-Operation ++ ist n die Anzahl
der Aufrufe von ++. Entsprechendes gilt für Mergesort mit der divide-Operation split
oder splitAt (siehe Listen) anstelle von f ilter und der conquer-Operation merge anstelle
von ++:
Beispiel Mergesort
mergesort :: Ord a => [a] -> [a]
mergesort (x:y:s) = merge (mergesort $ x:s1) $ mergesort $ y:s2
where (s1,s2) = split s
mergesort s
= s
68
Die rechte Seite der ersten Gleichung ist äquivalent zu folgender λ-Applikation:
(\(s1,s2) -> merge (mergesort $ x:s1) $ mergesort $ y:s2) $ split s
Allgemein:
t where p = u
ist äquivalent zu
(λp.t)(u)
split :: [a] -> ([a],[a])
split (x:y:s) = (x:s1,y:s2) where (s1,s2) = split s
split s
= (s,[])
merge :: Ord a => [a] -> [a] -> [a]
merge s1@(x:s2) s3@(y:s4) = if x <= y then x:merge s2 s3
else y:merge s1 s4
merge [] s
= s
merge s _
= s
mergesort2 :: Ord a => [a] -> [a]
mergesort2 s | n < 2 = s
| True = merge (mergesort2 s1) $ mergesort2 s2
where n = length s
(s1,s2) = splitAt (n `div` 2) s
69
Beispiel Binäre Bäume
data Bintree a = Empty | Fork (Bintree a) a (Bintree a)
leaf :: a -> Bintree a
leaf a = Fork Empty a Empty
subtrees :: Bintree a -> [Bintree a]
subtrees (Fork left _ right) = [left,right]
subtrees _
= []
baltree :: [a] -> Bintree a
baltree [] = Empty
baltree s = Fork (baltree s1) x (baltree s2)
where (s1,x:s2) = splitAt (length s`div`2) s
insertTree :: Ord a => a -> Bintree a ->
insertTree a t@(Fork t1 b t2) | a == b =
| a < b =
| True
=
insertTree a _ = leaf a
70
Bintree a
t
Fork (insertTree a t1) b t2
Fork t1 b $ insertTree a t2
instance Eq a => Ord (Bintree a) where
Empty <= _
= True
_ <= Empty
= False
Fork t1 a t2 <= Fork t3 b t4 = t1 <= t3 && a == b && t2 <= t4
Einlesen (Der Haskell-Code steht hier.)
Vor der Eingabe von Daten eines Typs T wird automatisch die T-Instanz der Funktion
read aufgerufen, die zur Typklasse Read a gehört.
type ReadS a = String -> [(a,String)]
Das Argument einer Funktion vom Typ ReadS a ist der jeweilige Eingabestring s. Ihr
Wert ist eine Liste von Paaren, bestehend aus dem als Objekt vom Typ a erkannten
Präfix von s und der jeweiligen Resteingabe (= Suffix von s).
class Read a where
read :: String -> a
read s = case [x | (x,t) <- reads s, ("","") <- lex t] of
[x] -> x
[] -> error "PreludeText.read: no parse"
71
_
-> error "PreludeText.read: ambiguous parse"
reads :: ReadS a
reads = readsPrec 0
readsPrec :: Int -> ReadS a
lex :: ReadS String ist eine Standardfunktion, die ein evtl. aus mehreren Zeichen bestehendes Symbol erkennt, vom Eingabestring abspaltet und das Paar (Symbol,Resteingabe)
ausgibt.
Der Generator
("","") <- lex t
in der obigen Definition von read s bewirkt, dass nur die Paare (x,t) von reads s
berücksichtigt werden, bei denen die Resteingabe t :: String aus Leerzeichen, Zeilenumbrüchen und Tabulatoren besteht (siehe Beispiele unten).
Steht deriving Read am Ende der Definition eines Datentyps T, dann werden T-Objekte
in genau der Darstellung erkannt, in der sie in Programmen vorkommen.
Will man das Eingabeformat von T-Objekten selbst bestimmen, dann muss die T-Instanz
von readsPrec definiert werden.
72
Beispiel Binäre Bäume
instance Read a => Read (Bintree a) where readsPrec _ = readTree
readTree :: Read a => ReadS (Bintree a)
readTree s = leaves++forks1++forks2
where leaves = [(leaf a,s1) | (a,s1) <- reads s]
forks1 = [(Fork left a Empty,s4) | (a,s1) <- reads s,
("(",s2) <- lex s1,
(left,s3) <- readTree s2,
(")",s4) <- lex s3]
forks2 = [(Fork left a right,s6) | (a,s1) <- reads s,
("(",s2) <- lex s1,
(left,s3) <- readTree s2,
(",",s4) <- lex s3,
(right,s5) <- readTree s4,
(")",s6) <- lex s5]
Die Instanzen von reads in der Definition von readTree haben den Typ
String -> ReadS a.
73
reads "5(7(3, 8),6( 2) ) " :: [(Bintree Int,String)]
; [(5(7(3,8),6(2))," "),(5,"(7(3, 8),6( 2) ) ")]
Diese Ausgabe setzt die u.g. Bintree-Instanz von Show voraus.
read "5(7(3, 8),6( 2) ) " :: Bintree Int
; 5(7(3,8),6(2))
reads "5(7(3,8),6(2))hh"
:: [(Bintree Int,String)]
; [(5(7(3,8),6(2)),"hh"),(5,"(7(3,8),6(2))hh")]
read "5(7(3,8),6(2))hh"
:: Bintree Int
; Exception: PreludeText.read:
74
no parse
Ausgeben (Der Haskell-Code steht hier.)
Vor der Ausgabe von Daten eines Typs T wird automatisch die T-Instanz der Funktion
show aufgerufen, die zur Typklasse Show a gehört.
ShowS = String -> String
Das Argument einer Funktion vom Typ ShowS ist der an die Ausgabe eines Objektes vom
Typ a anzufügende String. Ihr Wert ist die dadurch entstehende Gesamtausgabe.
class Show a where
show :: a -> String
show x = shows x ""
shows :: a -> ShowS
shows = showsPrec 0
showsPrec :: Int -> a -> ShowS
Steht deriving Show am Ende der Definition eines Datentyps T, dann werden T-Objekte
in genau der Darstellung ausgegeben, in der sie in Programmen vorkommen.
75
Will man das Ausgabeformat von T-Objekten selbst bestimmen, dann muss die T-Instanz
von show oder showsPrec definiert werden.
Beispiel Binäre Bäume
instance Show a => Show (Bintree a) where show = showTree0
bzw.
showsPrec _ = showTree
showTree0
showTree0
showTree0
showTree0
:: Show a => Bintree
(Fork Empty a Empty)
(Fork left a Empty)
(Fork left a right)
a
=
=
=
-> String
show a
show a++'(':showTree0 left++")"
show a++'(':showTree0 left++','
:showTree0 right++")"
oder effizienter, weil iterativ:
showTree :: Show a => Bintree a -> ShowS
showTree (Fork Empty a Empty) = shows a
showTree (Fork left a Empty) = shows a . ('(':) .
showTree left . (')':)
showTree (Fork left a right) = shows a . ('(':) .
showTree left . (',':) .
showTree right . (')':)
showTree _ = ""
76
Graphen, Modallogik und Matrizen
CPOs und Fixpunkte (Der Haskell-Code steht hier.)
Sei A eine Menge. Eine reflexive, transitive und antisymmetrische Relation auf A heißt
Halbordnung und A eine halbgeordnete Menge, kurz: poset (partially ordered set).
Eine (ω-)Kette bzw. Cokette von A ist eine abzählbare Teilmenge von A mit aiRai+1
bzw. ai+1Rai für alle i ∈ N.
Ein poset A mit Halbordnung ≤ heißt vollständig bzw. covollständig, kurz ein CPO
(complete partial order) bzw. co-CPO, falls A ein kleinstes bzw. größtes Element ⊥
(bottom) ⊥ (bottom) bzw. > (top) und Suprema tB bzw. Infima uB aller Ketten bzw.
Coketten B von A besitzt.
Eine Funktion f : A → B zwischen zwei CPOs A und B heißt stetig bzw. costetig,
falls für alle Ketten bzw. Coketten C von A
f (tC) = t{f (c) | c ∈ C} bzw. f (uC) = u{f (c) | c ∈ C}
gilt. Ist f stetig, dann ist f monoton, d.h. für alle a ∈ A gilt:
a ≤ b ⇒ f (a) ≤ f (b).
a ∈ A heißt Fixpunkt von f : A → A, falls f (a) = a gilt.
77
Fixpunktsatz von Kleene
Sei f : A → A stetig. Dann ist ti∈Nf i(⊥) der (bzgl. ≤) kleinste Fixpunkt von f .
Sei f : A → A costetig. Dann ist ui∈Nf i(>) der (bzgl. ≤) größte Fixpunkt von f .
o
Aus der Monotonie von f folgt f i(⊥) ≤ f i+1(⊥) bzw. f i(>) ≥ f i+1(>) für alle i ∈ N.
Demnach gibt es i ∈ N mit f i(⊥) = f i+1(⊥) bzw. f i(>) = f i+1(>), falls A endlich ist.
In diesem Fall ist also f i(⊥) bzw. f i(>) der kleinste bzw. größte Fixpunkt von f und wir
erhalten einen einfachen Algorithmus zu seiner Berechnung: ti∈Nf i(⊥) = lfp(f )(⊥) bzw.
ui∈Nf i(>) = gfp(f )(>), wobei
lfp,gfp :: Ord a => (a -> a) -> a -> a
lfp f a = if fa <= a then a else lfp f fa where fa = f a
gfp f a = if fa >= a then a else gfp f fa where fa = f a
78
Ein Datentyp für Mengen (siehe Mengenoperationen auf Listen)
newtype Set a = Set {list :: [a]}
newtype kann data ersetzen, wenn der
Datentyp genau einen Konstruktor hat
instance Eq a => Eq (Set a)
where s == s' = list s `eqset` list s'
instance Eq a => Ord (Set a)
where s <= s' = list s `subset` list s'
Set[1,2,3] <= Set[3,4,2,5,1,99]
Set[1,2,3] >= Set[3,4,2,5,1,99]
;
;
True
False
instance Show a => Show (Set a)
where show = ('{':) . (++"}") . init . tail . show . list
show $ Set[3,4,2,5,1,99]
;
{3,4,2,5,1,99}
mkSet :: Eq a => [a] -> Set a
mkSet = Set . union []
eliminiert Duplikate
79
Graphen
Adjazenzlistendarstellung
Unmarkierte Graphen:
type Graph a = ([a], a -> [a])
instance Show a => Show (Graph a) where
show (nodes,f) = concatMap g nodes
where g a = '\n':show a++" -> "++show (f a)
Kantenmarkierte Graphen: type LGraph a label = ([a],a -> [(label,a)])
Relationale Darstellung
Unmarkierte Graphen: type BinRel a = [(a,a)]
Kantenmarkierte Graphen: type TernRel a label = [(a,label,a)]
80
Adjazenzmatrixdarstellung (siehe Matrizenrechnung)
type AdjMat a b = ([a],Mat b)
Unmarkierte Graphen: AdjMat a Bool
Kantenmarkierte Graphen: AdjMat a label
Kantengewichtete Graphen: Semiring label => AdjMat a label
Erreichbare Knoten
Die Knotenmengen eines unmarkierten Graphen (nodes, f ) (Adjazenzlistendarstellung),
die einen bestimmten Knoten a enthalten, bilden den CPO hai ⊆ P(nodes) mit der
Mengeninklusion als Halbordnung und {a} als kleinstem Element.
Die Menge der von a aus erreichbaren Knoten von (nodes, f ) ist der kleinste Fixpunkt
der Funktion
Φ : hai → hai
S
M 7→ M ∪ {f (b) | b ∈ M }.
81
Da hai eine Teilmenge von P(nodes) ist, implementieren wir sie durch den oben eingeführten Typ Set a:
reachables :: Eq a => Graph a -> a -> Set a
reachables (nodes,f) a = lfp phi $ Set [a]
where phi (Set as) = Set $ union as $ unionMap f as
Hier wird f in mehreren Iterationen auf dieselben Knoten angewendet. Um das zu vermeiden, wählen wir einen anderen CPO, nämlich hai×P(nodes), mit komponentenweiser
Mengeninklusion als Halbordnung und ({a}, ∅) als kleinstem Element.
Die Menge der von a aus erreichbaren Knoten ist jetzt die erste Komponente des kleinsten
Fixpunkts der Funktion
Ψ : hai × P(nodes) → hai × P(nodes)
S
(M, used) 7→ (M ∪ {f (b) | b ∈ M \ used}, used ∪ M )
Wir implementieren sowohl hai als auch P(nodes) durch Set a:
reachables :: Eq a => Graph a -> a -> Set a
reachables (nodes,f) a = fst $ lfp psi $ (Set [a],Set [])
where psi (Set as,Set used) = (Set $ union as $ unionMap f
$ diff as used,
Set $ union used as)
82
Beispiel
graph1 :: Graph Int
graph1 = ([1..6], \a -> case a of 1 -> [2,3]; 2 -> []; 3 -> [1,4,6]
4 -> [1]; 5 -> [3,5]; 6 -> [2,4,5])
graph1 und seine Adjazenzlistendarstellung
reachables graph1 1
;
[1,2,3,4,6,5]
graph2 :: Graph Int
graph2 = ([1..], \a -> if a > 0 && a < 7 then [a+1] else [])
reachables graph2 1
;
[1,2,3,4,5,6,7]
83
Modallogik in funktionaler Form
Graphen repräsentieren binäre (oder, falls sie kantenmarkiert sind, ternäre) Relationen.
Demnach sind auch die in der LV Logik für Informatiker behandelten Kripke-Strukturen
Graphen: Zustände (“Welten”) entsprechen den Knoten, Zustandsübergänge den Kanten
des Graphen. Hinzu kommt eine Funktion, die jedem Zustand state eine Menge lokaler
atomarer Eigenschaften zuordnet. Dementsprechend liefern die Werte dieser Funktion
Knotenmarkierungen.
Modallogische Formeln beschreiben lokale, aber vor allem auch globale Eigenschaften von
Zuständen, das sind Eigenschaften, die von der gesamten Kripke-Struktur K abhängen.
Um eine modallogische Formel ϕ so wie einen anderen Ausdruck auswerten zu können,
weist man ihr folgende – vom üblichen Gültigkeitsbegriff abweichende, aber dazu äquivalente – Semantik zu: ϕ wird interpretiert als die Menge aller Zustände von K, die ϕ
erfüllen sollen.
Zu diesem Zweck definieren eine Kripke-Struktur K als Quadrupel
(State, Atom, trans, atoms),
bestehend aus einer Zustandsmenge State, einer Menge Atom atomarer Formeln,
einer Transitionsfunktion trans : State → P(State), die jedem Zustand von State
die Menge seiner möglichen Nachfolger zuordnet, und einer Funktion atoms : State →
P(Atom), die jeden Zustand auf die Menge seiner atomaren Eigenschaften abbildet.
84
Beispiel Mutual exclusion (Huth, Ryan, Logic in Computer Science, 2nd ed., Example
3.3.1)
Die Kanten und Knotenmarkierungen des Graphen definieren die Funktionen trans bzw.
atoms der Kripkestruktur
Mutex = ({s0, . . . , s7, s9}, {n1, n2, t1, t2, c1, c2}, trans, atoms).
Bedeutung der atomaren Formeln: Sei i = 1, 2. ni: Prozess i befindet sich ausserhalb des
kritischen Abschnitts und hat nicht um Einlass gebeten. ti: Prozess i bittet um Einlass
in den kritischen Abschnitt. ci: Prozess i befindet sich im kritischen Abschnitt.
o
85
Unter den zahlreichen Modallogiken wählen wir hier CTL (computation tree logic) und
den – alle Modallogiken umfassenden – µ-Kalkül (siehe auch Algebraic Model Checking).
Deren Formelmenge MF ist induktiv definiert:
Sei V eine Menge von Variablen.
{True, False} ∪ Atom ∪ V ⊆ MF ,
ϕ, ψ ∈ MF
⇒ ¬ϕ, ϕ ∧ ψ, ϕ ∨ ψ, EXϕ, AXϕ ∈ MF ,
x ∈ V ∧ ϕ ∈ MF
⇒ µx.ϕ, νx.ϕ ∈ MF .
(µ-Formeln)
Alle anderen CTL-Formeln sind spezielle µ-Formeln:
EF ϕ
AF ϕ
AGϕ
EGϕ
ϕEU ψ
ϕAU ψ
ϕ⇒ψ
=
=
=
=
=
=
=
µx.(ϕ ∨ EX x)
µx.(ϕ ∨ (EXTrue ∧ AX x))
νx.(ϕ ∧ AX x)
νx.(ϕ ∧ (AXFalse ∨ EX x))
µx.(ψ ∨ (ϕ ∧ EX x))
µx.(ψ ∨ (ϕ ∧ AX x))
¬ϕ ∨ ψ
86
exists finally
always finally
always generally
exists generally
exists ϕ until ψ
always ϕ until ψ
87
Wie bei arithmetischen oder Booleschen Ausdrücken hängt die Auswertung modaler Formeln von einer Variablenbelegung ab, das ist hier eine Funktion des Typs
Store = (V → P(State)).
Für eine gegebene Kripke-Struktur K = (State, Atom, trans, value) ist die Auswertungsfunktion
eval : MF → (Store → P(State))
daher wie folgt induktiv über der Struktur modallogischer Formeln definiert:
Sei atom ∈ Atom, x ∈ V , ϕ, ψ ∈ MF und b ∈ Store.
eval(True)(b)
eval(False)(b)
eval(atom)(b)
eval(x)(b)
eval(¬ϕ)(b)
eval(ϕ ∧ ψ)(b)
eval(ϕ ∨ ψ)(b)
eval(EXϕ)(b)
eval(AXϕ)(b)
eval(µx.ϕ)(b)
eval(νx.ϕ)(b)
=
=
=
=
=
=
=
=
=
=
=
State,
∅,
{s ∈ State | atom ∈ atoms(s)},
b(x),
State \ eval(ϕ)(b),
eval(ϕ)(b) ∩ eval(ψ)(b),
eval(ϕ)(b) ∪ eval(ψ)(b),
{state ∈ State | trans(state) ∩ eval(ϕ)(b) 6= ∅}, exists next state
{state ∈ State | trans(state) ⊆ eval(ϕ)(b)},
all next states
∪i∈NQi,
∩i∈NRi,
88
wobei für alle x, y ∈ V und Q ⊆ State,
b[Q/x](y) =
Q falls x = y,
b(y) sonst,
und für alle i ∈ N,
Q0 = ∅,
R0 = State,
Qi+1 = eval(ϕ)(b[Qi/x]), Ri+1 = eval(ϕ)(b[Ri/x]).
P(State) ist ein CPO mit der Mengeninklusion als Halbordnung, kleinstem Element ∅
und größtem Element State. Ist trans bildendlich, d.h. hat jeder Zustand höchstens
endlich viele direkte Nachfolger, und werden alle Vorkommen der gebundenen Variablen
einer µ-Formel in deren Rumpf von einer geraden Anzahl von Negationen präfixiert, dann
ist die Funktion
Φ : P(State) → P(State)
Q 7→ eval(ϕ)(b[Q/x])
stetig.
Also sind eval(µx.ϕ)(b) und eval(νx.ϕ)(b) nach dem Fixpunktsatz von Kleene der kleinste bzw. größte Fixpunkt von Φ. Ist State endlich, dann ist auch P(State) endlich, so
dass die Werte von µ-Formeln durch entsprechende Instanziierungen der o.g. Funktion
lfp bzw. gfp berechnet werden können:
eval(µx.ϕ)(b) = lfp(Φ)(∅),
eval(νx.ϕ)(b) = gfp(Φ)(State).
89
Beispiel Mutual exclusion
Jede der folgenden modalen Formeln ϕ gilt in Mutex (s.o), d.h.
eval(ϕ)(λx.∅) = State.
safety
Es befindet sich immer nur ein Prozess im kritischen Abschnitt.
¬(c1 ∧ c2)
liveness
Wenn ein Prozess um Einlass in den kritischen Abschnitt bittet,
wird er diesen auch irgendwann betreten.
ti ⇒ AF ci, i = 1, 2
non-blocking Ein Prozess kann stets um Einlass in den kritischen Abschnitt bitten.
ni ⇒ EX ti, i = 1, 2
no strict
Es kann vorkommen, dass ein Prozess nach Verlassen des kritischen
sequencing
Abschnitts diesen wieder betritt, bevor der andere Prozess dies tut.
3(ci ∧ (ciEU (¬ci ∧ (¬cj EU ci)))), i = 1, 2, j = 1, 2, i 6= j
o
90
Die Bisimilarität oder Verhaltensgleichheit ∼ von Zuständen einer Kripke-Struktur
ist ebenfalls ein größter Fixpunkt. Sie ist definiert als Durchschnitt aller binären Relationen ∼i, i ∈ N, wobei
∼0 = {(st, st0) ∈ State2 | atoms(st) = atoms(st0)},
∼i+1 = {(st, st0) ∈ State2 | trans(st) ∼i trans(st0)}.
Folglich ist ∼ nach dem Fixpunktsatz von Kleene der größte Fixpunkt von
Ψ : State2 → State × State)
R 7→ {(st, st0) ∈ State2 | (trans(st), trans(st0)) ∈ R}
Übrigens liefert der Quotient einer Kripke-Struktur nach ihrer Bisimilarität anoalog zur
Zustandsäquivalenz endlicher Automaten eine bzgl. der Zustandszahl minimale Struktur.
91
Aufgabe
Implementiere die hier beschriebene Modallogik in Haskell in drei Schritten:
• Gib einen Datentyp für MF an und Typen für Kripke-Strukturen und die Menge
Store.
• Programmiere die Auswertungsfunktion eval unter Verwendung der o.g. Funktionen
lfp und gfp. Verwenden Sie den Datentyp Set und die Mengenoperationen auf Listen.
• Teste deine Implementierung an einigen Kripke-Strukturen aus einschlägiger Literatur, z.B. Mutual exclusion (s.o.); Beispiele aus der LV Logik für Informatiker, Teil B5
(hier wird 2 für AX und 3 für EX verwendet); die Mikrowelle in Clarke, Grumberg,
Peled, Model Checking, Section 4.1.
92
Reflexiver und transitiver Abschluss
Der reflexive Abschluss eines Graphen erweitert ihn für jeden Knoten a um eine Kante
von a nach a:
rClosure :: Eq a => Graph a -> Graph a
rClosure (nodes,f) = (nodes,fold2 update f nodes $ map g nodes)
where g a = insert a $ f a
Der transitive Abschluss eines Graphen erweitert ihn für jeden Weg von a nach b
um eine Kante von a nach b. Der Floyd-Warshall-Algorithmus berechnet ihn, indem er iterativ für jeden Knoten b aus dem zuvor berechneten Graphen f einen neuen,
trans(f, b), berechnet, der für jedes Paar von Kanten von a nach b bzw. von b nach c
zusätzlich eine Kante von a nach c enthält:
tClosure :: Eq a => Graph a -> Graph a
tClosure (nodes,f) = (nodes,foldl trans f nodes)
where trans f b = fold2 update f nodes $ map g nodes
where g a = if b `elem` sucs
then union sucs $ f b else sucs
where sucs = f a
93
tClosure berechnet den transitiven Abschluss mit Aufwand O(|nodes|3):
Die äußere Faltung durchläuft die Liste nodes, die innere ebenfalls, und die Abfrage
b ∈ sucs kann im schlechtesten Fall (sucs = nodes) dazu führen, dass jeder einzelne
Update von f aus |nodes| Vergleichen mit b besteht.
Matrizenrechnung
Viele Graphalgorithmen lassen sich aus Matrixoperationen zusammensetzen, die generisch
definiert sind für Matrixeinträge unterschiedlichen Typs. Der Typ der Einträge muss ein
Semiring S sein, d.i. eine algebraische Struktur mit einer einer Addition, einer Multiplikation und neutralen Elementen bzgl. dieser Operationen. Diese werden zu Operationen
auf Matrizen über S geliftet, so dass die Matrizen selbst einen Semiring bilden.
Die Repräsentation von Graphen durch Matrizen macht aus aufwändigen rekursiven Algorithmen wie dem o.g. Floyd-Warshall-Algorithmus schnelle iterative Programme.
94
Für alle a, b, c ∈ R müssen die folgenden Gleichungen gelten:
a + (b + c) = (a + b) + c
a+b=b+a
0+a=a=a+0
a ∗ (b ∗ c) = (a ∗ b) ∗ c
1∗a=a=a∗1
0∗a=0=a∗0
a ∗ (b + c) = (a ∗ b) + (a ∗ c)
(a + b) ∗ c = (a ∗ c) + (b ∗ c)
Assoziativität von +
Kommutativität von +
Neutralität von 0 bzgl. +
Assoziativität von ∗
Neutralität von 1 bzgl. ∗
Annihilierung von A durch 0
Linksdistributivität von ∗ über +
Rechtsdistributivität von ∗ über +
Ein Ring A hat außerdem additive Inverse. Aus deren Existenz kann man die Annihilierung von A durch 0 ableiten. Ist auch die Multiplikation kommutativ und haben alle
a ∈ R \ {0} multiplikative Inverse, dann ist R ein Körper (engl. field).
In einem vollständigen Semiring sind auch unendliche Summen definiert. Die obigen
Gleichungen gelten entsprechend (siehe G. Karner, On Limits in Complete Semirings; B.
Mahr, A Bird’s Eye View to Path Problems).
Wir führen eine Typklasse für Semiringe ein:
class Ord r => Semiring r where add,mul :: r -> r -> r
zero,one :: Pos -> r
95
Matrizen definieren wir als Datentyp mit den Attributen dim (Dimension) und mat. Letzteres ist die Funktion, die für jede Zeile und Spalte den jeweiligen Eintrag der Matrix
speichert:
type Pos = (Int,Int)
data Mat r = Mat {dim :: Pos, mat :: Pos -> r}
Die Matrizen über einem Semiring R bilden selbst einen solchen. Gleichheit, Halbordnung, Addition, Multiplikation, Null und Eins werden wie folgt von R auf die Matrizen
fortgesetzt:
instance Semiring r => Eq (Mat r)
where (==) = compareM (==)
instance Semiring r => Ord (Mat r) where (<=) = compareM (<=)
compareM :: Semiring r => (r -> r -> Bool) -> Mat r -> Mat r -> Bool
compareM rel m n = all f $ range ((1,1),dim m)
where f p = mat m p `rel` mat n p
range ist eine Funktion der Typklasse Ix für Indexmengen (siehe Matrizenrechnung auf
Basis von Feldern). range(i, j) liefert das Intervall von i bis j.
96
instance Semiring r => Semiring (Mat r)
where add m n = Mat (dim m) $ \p -> mat m p `add` mat n p
mul m n = Mat (n1,n3) $ \p -> foldl1 add $ map (g p) [1..n2]
where (n1,n2) = dim m; (_,n3) = dim n
g (i,j) k = mat m (i,k) `mul` mat n (k,j)
zero d = Mat d $ const $ zero d
one d = Mat d $ \(i,j) -> if i == j then one d else zero d
Der transitive Abschluss M ∗ einer Matrix M
Für alle M ∈ Mat(r), M ∗ =def 1 + M + M 2 + M 3 + . . . . Mit der oben definierten Halbordnung ≤ und der Nullmatrix als kleinstem Element bilden alle quadratischen Matrizen
über demselben Semiring und gleicher Dimension einen CPO Mat. Da die Funktion
f : Mat → Mat
N 7→ 1 + M × N
stetig ist, hat f nach dem Fixpunktsatz von Kleene den kleinsten Fixpunkt ti∈Nf i(0).
Dieser stimmt mit M ∗ überein. Deshalb kann M ∗ mit lfp(f )(0) berechnet werden:
star :: Semiring r => Mat r -> Mat r
star m = lfp f $ zero d where d = dim m
f n = one d `add` (m `mul` n)
97
Semiring für Boolesche Matrizen und unmarkierte Graphen
instance Semiring Bool
where add = (||);
mul = (&&)
zero _ = False; one _ = True
Semiring für ganzzahlige Matrizen und kantengewichtete Graphen
instance Semiring Int
where add = min;
mul m n = maybeMax m n
zero _ = maxBound; one _ = 0
maybeMax m n = if maxBound `elem` [m,n] then maxBound else m+n
Semiring zur Berechnung kürzester Wege (s.u.)
type Path = (Int,[Int])
instance Semiring Path
where add (m,p) (n,q) = if m <= n then (m,p) else (n,q)
mul (m,p) (n,q) = (maybeMax m n,p++q)
zero _ = (maxBound,[]); one _ = (0,[])
98
Von Adjazenzmatrizen zu Adjazenzlisten und umgekehrt (siehe Graphen)
mat2fun :: Eq a => AdjMat a Bool -> Graph a
mat2fun (nodes,m) = (nodes,map g . f . nodeIndex nodes)
where f i = filter h [1..length nodes]
where h j = mat m (i,j)
g i = nodes!!(i-1)
fun2mat :: Eq a => Graph a -> AdjMat a Bool
fun2mat (nodes,f) = (nodes,Mat (n,n) g)
where n = length nodes
g (i,j) = nodes!!(j-1) `elem` f (nodes!!(i-1))
nodeIndex :: Eq a => [a] -> a -> Int
nodeIndex nodes a = get $ lookup a $ zip nodes [1..length nodes]
where get (Just i) = i
nodeIndex nodes a berechnet die den Knoten a repräsentierende Zeilenposition.
nodes!!(i-1) berechnet umgekehrt den durch die Zeilenposition i repräsentierten Knoten.
99
Iterative Berechnung des reflexiv-transitiven Abschlusses eines Graphen
closure :: Eq a => Graph a -> Graph a
closure graph = mat2fun (nodes,star m)
where (nodes,m) = fun2mat graph
closure graph1 ; 1
2
3
4
5
6
->
->
->
->
->
->
[1,2,3,4,5,6]
[2]
[1,2,3,4,5,6]
[1,2,3,4,5,6]
[1,2,3,4,5,6]
[1,2,3,4,5,6]
Iterative Berechnung kürzester Wege eines kantengewichteten Graphen
Die folgende Funktion berechnet den transitiven Abschluss der Adjazenzmatrix m eines
kantengewichteten Graphen mit Knotenmenge {1, . . . , fst(dim(m))}.
iniP aths berechnet aus m die Anfangsmatrix vom Typ M at(P ath). Sie enthält an der
Position (i, j) das Paar (m(i, j), if m(i, j) ∈ {0, maxBound} then [] else [j]).
100
paths :: Mat Int -> Mat Path
paths m = star $ Mat (dim m) iniPaths
where iniPaths :: Pos -> Path
iniPaths pos = (lg,if lg `elem` [0,maxBound] then []
else [snd pos])
where lg = mat m pos
Die Ergebnismatrix enthält an der Position (i, j) die Länge des kürzesten Weges von i
nach j und die Liste seiner Knoten außer dem ersten.
Beispiel
graph3 :: Mat Int
graph3 =
Mat (5,5) $ \(i,j) -> if i == j then 0
else case (i,j) of (1,5) -> 100; (1,2) -> 40
(2,5) -> 50; (2,3) -> 10
(3,4) -> 20; (4,5) -> 10
_ -> maxBound
(keine Kante)
101
paths graph3
;
1 - [2]:40 -> 2
1 - [2,3]:50 -> 3
1 - [2,3,4]:70 -> 4 der kürzeste Weg von 1 nach 4 führt
über 2,3,4 und hat die Länge 70
1 - [2,3,4,5]:80 -> 5
2 - [3]:10 -> 3
2 - [3,4]:30 -> 4
2 - [3,4,5]:40 -> 5
3 - [4]:20 -> 4
3 - [4,5]:30 -> 5
4 - [5]:10 -> 5
Die Ausgabe wurde mit folgender Show-Instanz für Matrizen vom Typ Mat Path erzeugt:
instance Show (Mat Path) where
show m = concatMap f $ range ((1,1),dim m)
where f (i,j) = if lg `elem` [0,maxBound] then ""
else '\n':show i++" - "++show p++
':':show lg++" -> "++show j
where (lg,p) = mat m (i,j)
102
Aufgabe
(1) Schreibe eine zu paths analoge Funktion
numbers :: Mat Bool -> Mat Int
einschließlich einer zu iniP aths analogen Funktion
iniNums :: Pos -> Int
mit folgender Bedeutung: Für alle Knoten i, j eines als Boolesche Matrix dargestellten
Graphen g liefert mat(numbers(g))(i, j) die Anzahl der Wege von i nach j in g. Es wird
vorausgesetzt, dass g kreisfrei ist und die Knoten von g positive natürliche Zahlen sind.
Die Lösung erfordert eine andere Int-Instanz der Typklasse Semiring als die o.g.: add
und mul müssen als ganzzahlige Addition bzw. Multiplikation definiert werden, zero(_)
und one als 0 bzw. 1.
(2) Schreiben Sie eine zur obigen M at(P ath)-Instanz von Show analoge M at(Int)Instanz von Show, so dass z.B. der Aufruf
numbers $ Mat(5,5) $ \(i,j) -> i < j
die folgende Ausgabe liefert:
103
1
1
1
1
1
2
2
2
2
3
3
3
4
4
5
-
1
1
2
4
8
1
1
2
4
1
1
2
1
1
1
->
->
->
->
->
->
->
->
->
->
->
->
->
->
->
1
2
3
4
5
2
3
4
5
3
4
5
4
5
5
es gibt 8 Wege von 1 nach 5
104
Weitere Beispiele
Graphen malen
Der Painter enthält einen Datentyp für Graphen, die als Listen von Wegen (= Linienzügen) dargestellt werden (siehe Farbkreise):
data Curves = C {file :: String, paths :: [Path], colors :: [RGB],
modes :: [Int], points :: [Point]}
type Path = [Point]
type Point = (Float,Float)
Für alle Graphen g ist file(g) die Datei, in der das Quadrupel
(paths(g), colors(g), modes(g), points(g))
abgelegt wird. paths(g) ist eine Zerlegung des Graphen in Wege.
colors(g), modes(g) und points(g) ordnen jedem Weg von g eine (Start-)Farbe, einen
fünfstelligen Zahlencode, der steuert, wie er gezeichnet und gefärbt wird, bzw. einen Rotationsmittelpunkt zu. Mit dem Aufruf drawC(g) wird g in die Datei file(g) eingetragen
und eine Schleife gestartet, in der zur – durch Leerzeichen getrennten – Eingabe reellwertiger horizontaler und vertikaler Skalierungsfaktoren aufgefordert wird.
105
Nach Drücken der return-Taste wird svg-Code für g erzeugt und in die Datei
PainterPix/file(g).svg geschrieben, so dass beim Öffnen dieser Datei mit einem Browser
dort das Bild von g erscheint. Verlassen wird die Schleife, wenn anstelle einer Parametereingabe die return-Taste gedrückt wird.
Der Painter stellt zahlreiche Operationen zur Erzeugung, Veränderung oder Kombination
von Graphen des Typs Curves zur Verfügung, u.a. (hier z.T. in vereinfachter Form
wiedergegeben):
combine :: [Curves] -> Curves
combine cs@(c:_) = c {paths = f paths, colors = f colors,
modes = f modes, points = f points}
where f = flip concatMap cs
zipCurves :: (Point -> Point -> Point) -> Curves -> Curves -> Curves
zipCurves f c d = c {paths = zipWith (zipWith f) (paths c) $ paths d,
points = zipWith f (points c) $ points d}
morphing :: Int -> [Curves] -> Curves
morphing n cs = combine $ zipWith f (init cs) $ tail cs
where f c d = combine $ map g [0..n]
where g i = zipCurves h c d
where h (xc,yc) (xd,yd) = (t xc xd,t yc yd)
t x x' = (1-step)*x+step*x'
step = float i/float n
106
zipCurves(f )(g)(g’) erzeugt einen neuen Graphen aus den Graphen g und g 0, indem die
Funktion f : P oint → P oint → P oint auf jedes Paar sich entsprechender Punkte von
g bzw. g 0 angewendet wird. combine(gs) vereinigt alle Graphen der Liste gs zu einem
einzigen Graphen, ohne ihre jeweiligen zu verschieben. morphing(n)(gs) fügt zwischen
je zwei benachbarte Graphen der Liste gs n von einem Morphing-Algorithmus erzeugte
äquidistante Zwischenstufen ein.
Beispiele
poly1,poly2,poly3,poly4 :: Curves
poly1 = poly 12111 10 [44]
poly2 = poly 12111 5 [4,44]
poly3 = turn 36 $ scale 0.5 poly2
poly4 = turn 72 poly2
poly5 = morphing 11 [poly2,poly3,poly4]
poly(mode)(n)(rs) erzeugt ein Polygon mit n ∗ |rs| Ecken. Für alle 1 ≤ i ≤ |rs| liegt
die (i ∗ n)-te Ecke auf einem Kreis mit Radius rs!!i um den Mittelpunkt des Polygons.
107
poly1
poly2
morphing(11)(poly1,poly2)
poly3
poly4
poly5
Einige Modes bewirken, dass anstelle der Linien eines Weges von den Endpunkten der
Linien und dem Wegmittelpunkt aufgespannte Dreiecke gezeichnet werden, wie es z.B.
bei der Polygon-Komponente des folgenden Graphen der Fall ist:
graph :: Curves
graph = overlay [g,flipV g,scale 0.25 $ poly 13123 11 rs]
where g = cant 12121 33
rs = [22,22,33,33,44,44,55,55,44,44,33,33]
108
cant(mode)(n) erzeugt eine Cantorsche Diagonalkurve der Dimension n, flipV(g) spiegelt
g an der Vertikalen durch den Mittelpunkt von g, scale(0.25)(g) verkleinert g auf ein
Viertel der ursprünglichen Größe, overlay(gs) legt alle Elemente der Graphenliste gs
übereinander.
drawC(graph) zeichnet schließlich folgendes Bild in die Datei cant.svg:
109
Die folgenden Programme stehen hier.
Arithmetische Ausdrücke ausgeben
instance Show Expr where showsPrec _ = showE
showE
showE
showE
showE
showE
showE
showE
showE
:: Expr -> ShowS
(Con i)
= (show i++)
(Var x)
= (x++)
(Sum es) = foldShow '+' es
(Prod es) = foldShow '*' es
(e :- e') = ('(':) . showE e . ('-':) . showE e' . (')':)
(n :* e) = ('(':) . (show n++) . ('*':) . showE e . (')':)
(e :^ n) = ('(':) . showE e . ('^':) . (show n++) . (')':)
foldShow :: Char -> [Expr] -> ShowS
foldShow op (e:es) = ('(':) . showE e . foldr trans id es . (')':)
where trans e h = (op:) . showE e . h
foldShow _ _
= id
showE (Prod[Con 3,Con 5,x,Con 11]) "" ; (3*5*x*11)
showE (Sum [11 :* (x :^ 3),5 :* (x :^ 2),16 :* x,Con 33]) ""
; ((11*(x^3))+(5*(x^2))+(16*x)+33)
110
Arithmetische Ausdrücke reduzieren
Die folgende Funktion reduce wendet die Gleichungen
0+e=e
(m ∗ e) + (n ∗ e) = (m + n) ∗ e
m ∗ (n ∗ e) = (m ∗ n) ∗ e
0∗e=0
em ∗ en = em+n
(em)n = em∗n
1 ∗ e = e e0 = 1
e1 = e
auf einen arithmetischen Ausdruck an.
Die Reduktion von Ausdrücken der Form Sum[e1, . . . , en] oder P rod[e1, . . . , en] erfordern
ein Zustandsmodell zur schrittweisen Verarbeitung von Skalarfaktoren bzw. Exponenten:
type Estate = (Int,[Expr],Expr -> Int)
updState :: Estate -> Expr -> Int -> Estate
updState (c,bases,f) e i = (c,insert e bases,update f e $ f e+i)
applyL :: ([Expr] -> Expr) -> [Expr] -> Expr
applyL _ [e] = e
applyL f es = f es
Die gesamte Reduktionsfunktion kann damit wie folgt implementiert werden:
111
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
:: Expr -> Expr
(e :- e')
= reduce $ Sum [e,(-1):*e']
(i :* Con j) = Con $ i*j
(0 :* e)
= zero
(1 :* e)
= reduce e
(i :* e)
= i :* reduce e
(Con i :^ j) = Con $ i^j
(e :^ 0)
= one
(e :^ 1)
= reduce e
(e :^ i)
= reduce e :^ i
(Sum es)
= case (c,map summand bases) of
(_,[]) -> Con c
(0,es) -> applyL Sum es
(_,es) -> applyL Sum $ Con c:es
where summand e = if i == 1 then e else i :* e where i = scal e
(c,bases,scal) = foldl trans (0,[],const 0) $ map reduce es
trans state@(c,bases,scal) e =
case e of Con 0 -> state
Con i -> (c+i,bases,scal)
i:*e -> updState state e i
_ -> updState state e 1
112
reduce (Prod es)
= case (c,map factor bases) of
(_,[]) -> Con c
(1,es) -> applyL Prod es
(_,es) -> c :* applyL Prod es
where factor e = if i == 1 then e else e :^ i where i = expo e
(c,bases,expo) = foldl trans (1,[],const 0) $ map reduce es
trans state@(c,bases,expo) e =
case e of Con 1 -> state
Con i -> (c*i,bases,expo)
e:^i -> updState state e i
_ -> updState state e 1
reduce e = e
reduce(Sum(es)) wendet zunächst reduce auf alle Ausdrücke der Liste es an. Dann wird
die Ergebnisliste res = map(reduce)(es), ausgehend vom Anfangszustand (0, [], const0)
mit der Zustandsüberführung trans zum Endzustand (c, bases, scal) gefaltet, der schließlich in eine reduzierte Summe der Elemente von res überführt wird.
Bei der Faltung werden gemäß der Gleichung 0 + e = e die Nullen aus res entfernt und
alle Konstanten von res sowie alle Skalarfaktoren von Summanden mit derselben Basis
gemäß der Gleichung (m ∗ e) + (n ∗ e) = (m + n) ∗ e summiert.
113
Im Endzustand (c, bases, scal) ist c die Summe aller Konstanten von res und bases die
Liste aller Summanden von res. Die Funktion scal : Expr → Int ordnet jedem Ausdruck
e die Summe der Skalarfaktoren der Summanden von res mit der Basis e zu. Nur im Fall
c 6= 0 wird Conc in den reduzierten Summenausdruck eingefügt.
Demnach minimiert reduce die Liste es von Skalarprodukten eines Summenausdrucks
Sum(es). Analog minimiert reduce die Liste es von Potenzen eines Produktausdrucks
P rod(es).
Der Ausdruck 11*x^3*x^4+5*x^2+6*x^2+16*x+33 und seine reduzierte Form als Expr-Objekte
Die Korrektheit von reduce zeigt man, indem man die Interpretation eines Ausdrucks mit
der seiner reduzierten Form in Beziehung setzt und für alle Ausdrücke e die Gleichung
evalE(reduce(e)) = evalE(e)
durch Induktion über den Aufbau von e beweist.
114
Arithmetische Ausdrücke compilieren
Die unten definierte Funktion compileE übersetzt Objekte des Datentyps Expr in Assemblerprogramme. executeE führt diese in einer Kellermaschine aus.
Die Zielkommandos sind durch folgenden Datentyp gegeben:
data StackCom = Push Int | Load String | Sub | Add Int | Mul Int | Pow
Die (virtuelle) Zielmaschine besteht aus einem Keller für ganze Zahlen und einem Speicher
(Menge von Variablenbelegungen) wie beim Interpreter arithmetischer Ausdrücke (s.o.).
Genaugenommen beschreibt ein Typ für diese beiden Objekte nicht diese selbst, sondern
die Menge ihrer möglichen Zustände:
type State = ([Int],Store)
Die Bedeutung der einzelnen Zielkommandos wird durch einen Interpreter auf State
definiert:
115
executeCom
executeCom
executeCom
executeCom
executeCom
executeCom
executeCom
:: StackCom -> State -> State
(Push a) (stack,store) = (a:stack,store)
(Load x) (stack,store) = (store x:stack,store)
(Add n) st
= executeOp sum n st
(Mul n) st
= executeOp product n st
Sub st
= executeOp (foldl1 (-)) 2 st
Pow st
= executeOp (foldl1 (^)) 2 st
Die Ausführung eines arithmetischen Kommandos besteht in der Anwendung der jeweiligen arithmetischen Operation auf die obersten n Kellereinträge, wobei n die Stelligkeit
der Operation ist:
executeOp :: ([Int] -> Int) -> Int -> State -> State
executeOp f n (stack,store) = (f (reverse as):bs,store)
where (as,bs) = splitAt n stack
Die Ausführung einer Kommandoliste besteht in der Hintereinanderausführung ihrer Elemente:
execute :: [StackCom] -> State -> State
execute = flip $ foldl $ flip executeCom
116
Die Übersetzung eines arithmetischen Ausdrucks von seiner Baumdarstellung in eine
Befehlsliste erfolgt wie die Definition aller Funktionen auf Expr-Objekten induktiv:
compileE
compileE
compileE
compileE
compileE
compileE
compileE
compileE
:: Expr -> [StackCom]
(Con i)
= [Push i]
(Var x)
= [Load x]
(Sum es) = concatMap compileE es++[Add $ length es]
(Prod es) = concatMap compileE es++[Mul $ length es]
(e :- e') = compileE e++compileE e'++[Sub]
(i :* e) = Push i:compileE e++[Mul 2]
(e :^ i) = compileE e++[Push i,Pow]
Wie die Korrektheit der Reduktion von Ausdrücken e, so ist auch die Korrektheit der
Übersetzung von e durch eine Gleichung gegeben, welche die Beziehung zur Interpretation
von e herstellt und durch Induktion über den Aufbau von e gezeigt werden kann:
executeE(compileE(e))(stack, store) = (evalE(e)(store) : stack, store).
Beginnt die Ausführung des Zielcodes von e im Zustand (stack, store), dann endet sie
im Zustand (a : stack, store), wobei a der Wert von e unter der Variablenbelegung store
ist.
117
Die Ausdrücke 5*11+6*12+x*y*z und 11*x^3+5*x^2+16*x+33 als Expr-Objekte
Z.B. übersetzt compileE die obigen Expr-Objekte in folgende Kommandosequenzen:
0:
1:
2:
3:
4:
5:
6:
7:
Push 5
Push 11
Mul 2
Push 6
Push 12
Mul 2
Load "x"
Load "y"
8: Load "z"
9: Mul 3
10: Add 3
0:
1:
2:
3:
4:
5:
6:
7:
Push 11
Load "x"
Push 3
Pow
Mul 2
Push 5
Load "x"
Push 2
8:
9:
10:
11:
12:
13:
14:
Eine Testumgebung für execute und compileE wird hier vorgestellt.
118
Pow
Mul 2
Push 16
Load "x"
Mul 2
Push 33
Add 4
Monaden
Von Typen zu Kinds
Während bisher nur Typvariablen erster Ordnung vorkamen, sind Monaden Instanzen
der Typklasse M onad, die eine Typvariable zweiter Ordnung enthält. Typvariablen erster
Ordnung werden durch Typen erster Ordnung instanziiert, das sind parameterlose Typen
wie Int, Expr und Curves (s.o.). Typvariablen zweiter Ordnung werden durch Typen
zweiter Ordnung instanziiert wie z.B. Bintree, Set, Graph und M at (s.o.).
Demnach ist ein Typ erster Ordnung (ein Name für) eine Menge und ein Typ zweiter
Ordnung (ein Name für) eine Funktion von einer Menge von Mengen in eine – i.d.R.
andere – Menge von Mengen: Bintree bildet jede Menge A auf die Menge der binären
Bäume ab, deren Knoteneinträge Elemente von A sind.
Wir hatten auch schon Typen dritter Ordnung: T erm (siehe Termbäume), LGraph
und AdjM at (siehe Graphen). Durch Festhalten des ersten (und zweiten) Parameters
erhält man daraus Typen zweiter Ordnung, z.B. T erm(a), und erster Ordnung, z.B.
T erm(a)(b).
Allgemein werden Typen nach ihren Kinds (englisch für Art, Sorte) klassifiziert: Typen
erster, zweiter oder dritter Ordnung haben den Kind ∗, ∗ → ∗ bzw. ∗ → (∗ → ∗).
119
Weitere Kinds ergeben sich aus anderen Kombinationen der Kind-Konstruktoren ∗ und
→. Z.B. ist (∗ → ∗) → ∗ der Kind eines Typs, der eine Funktion darstellt, die jedem
Typ des Kinds ∗ → ∗ (also jeder Funktion von einer Menge von Mengen in eine Menge
von Mengen) einen Type des Kinds ∗, also eine Menge von Mengen zuordnet.
Kinds erlauben es u.a., in Typklassen nicht nur Funktionen, sondern auch Typen zu
deklarieren, z.B.:
class TK a where type T a :: *
f :: [a] -> T a
instance TK Int where type T Int = Bool
f = null
Wie die Typen der Funktionen einer Typklasse, so müssen auch deren Typen die Typvariable der Typklasse mindestens einmal enthalten!
Alternativ kann anstelle der Typdeklaration die Typklasse um eine Typvariable t zu
erweitern. Die Abhängigkeit von t von a lässt sich dann als functional dependency
a -> t ausdrücken:
class TK a t | a -> t where f :: [a] -> t
instance TK Int Bool where f = null
a -> t verbietet unterschiedliche Instanzen von t bei gleicher Instanz von a.
120
Eine Typklasse mit einer Typvariablen zweiter Ordnung ist z.B. die Klasse Functor :
class Functor f where fmap :: (a -> b) -> f a -> f b
Wie ihr Name schon andeutet, verallgemeinert die Funktion f map die Funktion map :
(a → a) → ([a] → [a]) von Listen auf beliebige Datentypen. Umgekehrt bilden Listen
eine (Standard-)Instanz von F unctor:
instance Functor [ ] where fmap = map
Im Abschnitt Termbäume wurde mit der Funktion mapT erm implizit eine Instanz von
Functor für T erm(a)(a) definiert:
data Term1 a = F1 a [Term1 a] | V1 a
instance Functor Term1 where
fmap f (F1 a ts) = F1 (f a) $ map (fmap f) ts
fmap f (V1 a)
= V1 (f a)
121
Oft gibt es Anforderungen an die Instanzen einer Typklasse (z.B. bei Semiring; siehe
Matrizenrechnung). Bei Functor sind es folgende Gleichungen:
fmap id
fmap (f . g)
=
=
id
fmap f . fmap g
Monadenklasse und do-Notation
Nun zur Klasse M onad und ihrer Unterklasse M onadP lus.
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b sequentielle Komposition (pipelining, bind)
(>>)
:: m a -> m b -> m b
sequentielle Komposition ohne Wertübergabe
return :: a -> m a
Einbettung in monadischen Typ
fail
:: String -> m a
Wert im Fall eines Matchfehlers
m >> m' = m >>= const m'
Jedes Objekt vom Typ m(a) stellt eine Prozedur (Berechnung, Aktion o.ä.) dar, die mit
der Ausgabe eines Wertes vom Typ a endet.
122
class Monad m => MonadPlus m where
mzero :: m a
mplus :: m a -> m a -> m a
scheiternde Berechnung heißt zero in hugs
parallele Komposition heißt (++) in hugs
Anforderungen an die Instanzen von M onad bzw. M onadP lus:
(m >>= f) >>= g
m >>= return
return a >>= f
mzero >>= f
m >>= \a -> mzero
mzero `mplus` m
m `mplus` mzero
=
=
=
=
=
=
=
m >>= \a -> f a >>= g
m
f a
(1)
mzero (2)
mzero
m
m
Die Verwendung von Funktionen oder Instanzen von MonadPlus erfordert den Import
des Standardmoduls Monad.hs, also die Zeile
import Monad
am Anfang des Programms, das ihn verwendet.
123
Die do-Notation verwendet Zuweisung und Sequentialisierung als Funktionen folgender
Typen:
(<-) :: Monad m => a -> m a -> m ()
(;) :: Monad m => m a -> m b -> m b
Der monadische Ausdruck m>>=λa.m0 lautet in der do-Notation wie folgt:
do a <- m; m'
oder
do a <- m
m'
Wird die Ausgabe von m nicht benötigt, dann genügt m anstelle der Zuweisung a ← m.
Da der Sequentialisierungsoperator (;) assoziativ ist, schreibt man
do a <- m; b <- m'; m''
anstelle von
do a <- m; (do b <- m'; m'')
Die o.g. Anforderungen an Instanzen von M onad führen zu entsprechenden Gleichungen
in do-Notation. Z.B. ist do a ← m; return(a) semantisch äquivalent zu m.
Guards erlauben wie Exitbefehle imperativer Sprachen das Verlassen von Prozeduren:
guard :: MonadPlus m => Bool -> m ()
guard b = if b then return () else mzero
124
Gleichung (1) impliziert
Gleichung (2) impliziert
do guard True; m1; ...; mn
do guard False; m1; ...; mn
=
=
do m1; ...; mn
mzero
Die Identitätsmonade
dient nur dazu, eine gleichungsdefinierte Funktion f : a → b in eine prozedurale Form zu
bringen, indem man f in eine Funktion g : a → Id(b) mit run ◦ g = f überführt.
newtype Id a = Id {run :: a}
instance Monad Id where Id a >>= f = f a
return
= Id
Anschaulich gesprochen, führt die Anwendung des Attributs run : Id(a) → a auf eine Prozedur vom Typ Id(a) zu deren Ausführung und damit zu einem Wert vom Typ
a. Durch Verwendung der do-Notation in der Definition von g : a → Id(b) wird der
prozedurale Charakter von g(a) deutlich.
125
Beispiel
Die Funktion sumTree summiert die (ganzzahligen) Knoteneinträge eines binären Baums
auf:
sumTree :: Bintree Int -> Int
sumTree (Fork left a right) = a+sumTree left+sumTree right
sumTree _
= 0
Prozedurale Version:
sumTree = run . f where f :: Bintree Int -> Id Int
f (Fork left a right) = do b <- f left
c <- f right
return $ a+b+c
f _
= return 0
126
Die Maybe-Monade
data Maybe a = Just a | Nothing
instance Monad Maybe where Just a >>= f = f a
_ >>= _
= Nothing
return = Just
fail _ = mzero
erfüllt (1)
instance MonadPlus Maybe where mzero = Nothing
erfüllt (2)
Nothing `mplus` m = m
m `mplus` _
= m
127
Rechnen mit partiellen Funktionen
Eine partielle Funktion f : A → B ist an manchen Stellen undefiniert. “Undefiniert” wird
in Haskell durch den obigen Konstruktor Nothing wiedergegeben und f als Funktion vom
Typ f : A → Maybe(B). Sei g : B → Maybe(C). Dann ist
do b <- f a; g b
äquivalent zu:
f a >>= \b -> g b
Nach Definition von >>= in der Maybe-Monade ist
f a >>= \b -> g b
case f a of Just b -> g b
_ -> Nothing
äquivalent zu:
und damit eine Implementierung von g(f (a)).
Nach Definition von mzero bzw. mplus in der Maybe-Monade gilt z.B.:
case f a of Just b | p b -> g b
_ -> Nothing
ist äquivalent zu:
do b <- f a
guard $ p b
g b
case f a of Nothing -> g a
b -> b
ist äquivalent zu:
f a `mplus` g a
128
Beispiel
Die folgende Variante von filter wendet zwei Boolesche Funktionen f und g auf die
Elemente einer Liste s und ist genau dann definiert, wenn für jedes Listenelement x
f (x) oder g(x) gilt. Im definierten Fall liefert split2 das Listenpaar, bestehend aus
f ilter(f )(s) und f ilter(g)(s):
filter2 :: (a -> Bool) -> (a -> Bool) -> [a] -> Maybe ([a],[a])
filter2 f g (x:s) = do (s1,s2) <- filter2 f g s
if f x then Just (x:s1,s2)
else do guard $ g x; Just (s1,x:s2)
filter2 _ _ _
= Just ([],[])
129
Monaden-Kombinatoren
when :: Monad m => Bool -> m () -> m ()
when b m = if b then m else return ()
sat :: MonadPlus m => m a -> (a -> Bool) -> m a
sat m f = do a <- m; guard $ f a; return a
sat(m)(f ) (m satisfies f) prüft, ob die von m erzeugte Ausgabe die Bedingung f erfüllt.
Falls nicht, fährt sat(p)(f ) mit der stets scheiternden Prozedur mzero fort.
some, many :: MonadPlus m => m a -> m [a]
some m = do a <- m; as <- many m; return $ a:as
many m = some m `mplus` return []
some(m) und many(m) wiederholen die Prozedur m, bis sie scheitert. Die Ausgabe von
some(m) und many(m) ist die Liste der Ausgaben der einzelnen Iterationen von m.
some(m) scheitert, wenn bereits die erste Iteration von m scheitert. many(m) scheitert
in diesem Fall nicht, sondern liefert die leere Liste von Ausgaben.
130
msum :: MonadPlus m => [m a] -> m a
msum = foldr mplus mzero
heißt concat in hugs
msum setzt mplus von zwei Prozeduren auf Listen beliebig vieler Prozeduren fort.
sequence :: Monad m => [m a] -> m [a]
sequence (m:ms) = do a <- m; as <- sequence ms; return $ a:as
sequence _
= return []
heißt accumulate in hugs
sequence(ms) führt die Prozeduren der Liste ms hintereinander aus. Wie bei some(m)
und many(m) werden die dabei erzeugten Ausgaben aufgesammelt. Im Gegensatz zu
some(m) und many(m) ist die Ausführung von sequence(ms) erst beendet, wenn ms
leer ist und nicht schon dann, wenn eine Wiederholung von m scheitert.
sequence_ :: Monad m => [m a] -> m ()
sequence_ = foldr (>>) $ return ()
heißt sequence in hugs
sequence_(ms) arbeitet wie sequence(ms), vergisst aber die erzeugten Ausgaben.
131
Die folgenden Funktionen führen mit map bzw. zipWith transformierte Prozedurfolgen
aus:
mapM :: Monad m => (a -> m b) -> [a] -> m [b]
mapM f = sequence . map f
mapM_ :: Monad m => (a -> m b) -> [a] -> m ()
mapM_ f = sequence_ . map f
zipWithM :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m [c]
zipWithM f s = sequence . zipWith f s
zipWithM_ :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m ()
zipWithM_ f s = sequence_ . zipWith f s
Die Listenmonade
instance Monad [ ] where (>>=) = flip concatMap
return a = [a]
fail _ = mzero
132
erfüllt (1)
instance MonadPlus [ ] where mzero = []
mplus = (++)
erfüllt (2)
Rechnen mit nichtdeterministischen Funktionen
Eine nichtdeterministische Funktion ist eine Funktion in eine Potenzmenge. Deren Elemente werden in Haskell oft durch Listen dargestellt und f als Funktion vom Typ
f : A → [B]. Sei g : B → [C]. Dann ist
do b <- f a; g b
f a >>= \b -> g b
äquivalent zu:
Nach Definition von >>= in der Listenmonade ist
f a >>= \b -> g b
äquivalent zu:
concat [g b | b <- f a]
und damit eine Implementierung von g(f (a)).
Nach Definition von mzero bzw. mplus in der Listenmonade gilt:
do b <- f a; guard (p b); g b
ist äquivalent zu: concat [g b | b <- f a, p b]
do a <- s; let b = f a; guard (p b); [h b]
ist äquivalent zu: [h b | a <- s, let b = f a, p b]
133
Die Listeninstanz sequence : [[a]] → [[a]] von sequence : [m(a)] → m([a]) (s.o.) liefert das – als Liste von Listen dargestellte – kartesische Produkt ihrer Argumentlisten
as1, . . . , asn:
sequence([as1, . . . , asn]) = [[a1, . . . , an] | ai ∈ asi, 1 ≤ i ≤ n] = as1 × · · · × asn.
Beispiel
sequence $ replicate(3)[1..4]
;
sequence [[1..4],[1..4],[1..4]]
;
[[1,1,1],[1,1,2],[1,1,3],[1,1,4],[1,2,1],[1,2,2],[1,2,3],[1,2,4],
[1,3,1],[1,3,2],[1,3,3],[1,3,4],[1,4,1],[1,4,2],[1,4,3],[1,4,4],
[2,1,1],[2,1,2],[2,1,3],[2,1,4],[2,2,1],[2,2,2],[2,2,3],[2,2,4],
[2,3,1],[2,3,2],[2,3,3],[2,3,4],[2,4,1],[2,4,2],[2,4,3],[2,4,4],
[3,1,1],[3,1,2],[3,1,3],[3,1,4],[3,2,1],[3,2,2],[3,2,3],[3,2,4],
[3,3,1],[3,3,2],[3,3,3],[3,3,4],[3,4,1],[3,4,2],[3,4,3],[3,4,4],
[4,1,1],[4,1,2],[4,1,3],[4,1,4],[4,2,1],[4,2,2],[4,2,3],[4,2,4],
[4,3,1],[4,3,2],[4,3,3],[4,3,4],[4,4,1],[4,4,2],[4,4,3],[4,4,4]]
134
Beispiel Prozedurale Version von queens (siehe Damenproblem)
queens :: Int -> [[Int]]
queens n = board [1..n]
where board [] = [[]]
board xs = do x <- xs
ys <- board $ remove x xs
guard $ and $ zipWith (safe x) ys [1..]
[x:ys]
Direkte Suche in Listen
lookupM :: (Eq a,MonadPlus m) => a -> [(a,b)] -> m b
lookupM a ((a',b):s) = if a == a' then return b `mplus` lookupM a s
else lookupM a s
lookupM _ _
= mzero
Wird m durch Maybe oder [ ] instanziiert, dann liefert lookupM (a)(s) die zweite Komponente des ersten Paares bzw. aller Paare von s, deren erste Komponente mit a übereinstimmt.
135
Tiefen- und Breitensuche in Bäumen (Der Haskell-Code steht hier.)
searchDF, searchBF, checkRoot ::
MonadPlus m => (a -> Bool) -> Term a a -> m a
searchDF h t = msum $ checkRoot h t:map (searchDF h) (subterms t)
searchBF h t = visit [t]
where visit ts = do guard $ not $ null ts
msum $ map (checkRoot h) ts ++
[visit $ concatMap subterms ts]
checkRoot h t = do guard $ h a; return a where a = root t
Wird m durch Maybe instanziiert, dann liefern searchDF (h)(t) und searchBF (h)(t)
einen den ersten Knoteneintrag des Termbaums t, der die Bedingung h erfüllt. Wird m
durch [ ] instanziiert, dann liefern dieselbe Aufrufe alle Knoteneinträge von t, welche
die Bedingung h erfüllen.
t :: Term Int Int
t = F 1 [F 2 [F 2 [V 3 ,V (-1)],V (-2)],F 4 [V (-3),V 5]]
searchDF (< 0) t :: Maybe Int ; Just (-1)
searchDF (< 0) t :: [Int]
; [-1,-2,-3]
searchBF (< 0) t :: Maybe Int ; Just (-2)
searchBF (< 0) t :: [Int]
; [-2,-3,-1]
136
Für binäre Bäume sehen die Suchfunktionen fast genauso aus:
searchBDF, searchBBF, checkBRoot ::
MonadPlus m => (a -> Bool) -> Bintree a -> m a
searchBDF h t = msum $ checkBRoot h t:map (searchBDF h) (subtrees t)
searchBBF h t = visit [t]
where visit ts = do guard $ not $ null ts
msum $ map checkBRoot ts ++
[visit $ concatMap subtrees ts]
checkBRoot h (Fork _ a _) = do guard $ h a; return a
checkBRoot h _
= mzero
t :: Bintree Int
t = read "5(4(3,8(9)),6(2))"
searchBDF
searchBDF
searchBBF
searchBBF
(>
(>
(>
(>
5)
5)
5)
5)
t
t
t
t
::
::
::
::
Maybe Int
[Int]
Maybe Int
[Int]
;
;
;
;
Just 8
[8,9,6]
Just 6
[6,8,9]
137
Eine Termmonade (siehe Termbäume)
instance Monad (Term f)
where F f ts >>= h = F f $ map (>>= h) ts
V a >>= h
= h a
return = V
erfüllt (1)
t>>=h wendet die Substitution h : a → Term(f )(a) auf jede Variable V (x) von t an,
d.h. h ersetzt dort V (x) durch den Baum h(x).
t>>=h ist die h-Instanz von t.
instance Functor (Term f) where fmap f m = m >>= return . f
Da die Definition von f map nur Monad -Funktionen verwendet, kann auf diese Weise
offenbar jede Monade zum Funktor gemacht werden.
type Subst f a = a -> Term f a
138
t :: Term String String
t = F "+" [F "*" [c "5",
V "x"],
V "y",
c "11"]
where c = flip F []
sub :: Subst String String
sub x = case x of "x" -> F "/" [F "-" [c "6"],c "9",V "z"]
"y" -> F "-" [c "7",F "*" [c "8",c "0"]]
_ -> V x
where c = flip F []
t >>= sub ;
F "+" [F "*" [F "5" [],
F "/" [F "-" [F "6" []],F "9" [],V "z"]],
F "-" [F "7" [],F "*" [F "8" [],F "0" []]],
F "11" []]
139
Termunifikation
unify(t)(t0) bildet zwei Bäume t und t0 auf die allgemeinste Substitution sub ∈ Subst(f )(a)
ab, so dass die Instanzen t>>=sub und t0>>=sub von t bzw. t0 miteinander übereinstimmen
– falls t und t0 unifizierbar sind.
unify :: (Eq f,Eq a) => Term f a -> Term f a -> Maybe (Subst f a)
unify (V a) (V b)
= Just $ if a == b then V else update V a $ V b
unify (V a) t
= do guard $ a `in` t
Just $ update V a t
where a `in` F _ ts = all (a `in`) ts
a `in` V b
= b /= a
occurs check
unify t (V a)
= unify (V a) t
unify (F f ts) (F g us) = do guard $ f == g && length ts == length us
unifyall ts us
unifyall :: (Eq f,Eq a) => [Term f a] -> [Term f a] -> Maybe (Subst f a)
unifyall [] []
= Just V
identische Substitution
unifyall (t:ts) (u:us) = do sub1 <- unify t u
let sub2 = map (>>= sub1)
sub3 <- unifyall (sub2 ts) $ sub2 us
Just $ (>>= sub3) . sub1
140
Transitionsmonaden
total
functions
IO a
IOstore a
state is system state
state is Store
Trans state a
state -> (a,state)
PTrans state a
state -> Maybe (a,state)
state is String
Compiler a
partial
functions
141
Monaden totaler Transitionsfunktionen
newtype Trans state a = T {run :: state -> (a,state)}
instance Monad (Trans state)
where T trans >>= f = T $ \st -> let (a,st') = trans st
trans' = run (f a)
in trans' st'
return a = T $ \st -> (a,st)
Hier komponiert der bind-Operator >>= die Zustandstransformationen trans und trans0
sequentiell. Dabei erhält trans0 die von trans erzeugte Ausgabe a als Eingabe.
Die IO-Monade
kann man sich vorstellen als Instanz von des Datentyps Trans, wobei die Menge möglicher
Systemzustände die Typvariable state substituiert.
Verwenden lässt sie sich nur indirekt über Standardfunktionen wie readFile, writeFile,
putStr, getLine, usw., wie in den folgenden Beispielen.
142
Ein/Ausgabe von Funktionsargumenten bzw. -werten
test :: (Read a,Show b) => (a -> b) -> IO ()
test f = do str <- readFile "source"
writeFile "target" $ show $ f $ read str
readFile :: String -> IO String
readFile "source" liest den Inhalt der
Datei source und gibt ihn als String zurück.
read :: Read a => String -> a
read übersetzt einen String in ein Objekt
vom Typ a.
show :: Show b => b -> String
show übersetzt ein Objekt vom Typ b
in einen String.
writeFile :: String -> String -> IO ()
writeFile "target" schreibt einen String
in die Datei target.
143
Ein/Ausgabe-Schleifen
loop :: IO ()
loop = do putStrLn "Hello!"
putStrLn "Enter an integer x!"
str <- getLine
let x = read str
lokale Definition, gilt in allen darauffolgenden
Kommandos. Nicht mit einer lokalen Definition
let a = e in e' verwechseln, die nur in e' gilt!
if x < 5 then do putStr "x < 5"
else do putStrLn $ "x = "++show x
loop
then und else müssen bzgl. der Spalte ihres
ersten Zeichen hinter if stehen!
putStr :: String -> IO ()
putStr str schreibt str ins Konsolenfenster.
putStrLn :: String -> IO ()
putStrLn str schreibt str ins Konsolenfenster
und geht in die nächste Zeile.
getLine :: IO String
getLine liest den eingebenen String
und geht in die nächste Zeile.
144
Funktionstest mit Ein/Ausgabe über Dateien
Eine einfache Umgebung zum Testen von Haskell-Funktionen besteht aus drei Dateien: einer Programmdatei prog.hs, welche die unten definierten Funktionen enthält, einer Eingabedatei source und einer Ausgabedatei target. Nach dem Aufruf eines Haskell-Interpreters
(ghci oder hugs) wird die Programmdatei mit dem Kommando :load prog geladen.
test :: (Read a,Show b) => (a -> b) -> IO ()
test f = readFileAndDo "source" $ writeFile "target" . show . f . read
Für jede in prog.hs definierte Funktion f bewirkt der Aufruf test(f ) die Anwendung von
f auf den Inhalt von source und das Ablegen des Ergebnisses der Anwendung in target.
readFileAndDo :: String -> (String -> IO ()) -> IO ()
readFileAndDo file continue =
do str <- readFile file `catch` const (return "")
if null str then putStrLn $ file++" does not exist"
else continue str
readFileAndDo(file)(continue) liest den Inhalt der Datei file und übergibt ihn als String
str zur Weiterverarbeitung an die Funktion continue.
145
Beim Aufruf test(f ) ist continue durch die Funktionskomposition
writeFile "target" . show . f . read
gegeben: read übersetzt str in ein Objekt vom Typ a, aus dem f ein Objekt vom Typ b
berechnet, das show in einen String transformiert, der mit writeFile(”target”) in die Datei
target geschrieben wird.
catch hat den Typ IO a -> (IOError -> IO a) -> IO a.
catch(m)(f ) fängt einen bei der Ausführung von m auftretenden IO-Fehler err ab, indem
f auf err angewendet wird.
146
Noch ein IO-Beispiel
Mehrere Ausgabefunktionen des Painter rufen Varianten der folgenden Schleife auf, die
bei jeder Iteration ein graphisches Objekt a aus der Datei file liest, gemäß eingegebener
Faktoren skaliert, in SVG-Code übersetzt und diesen in die Datei file.svg schreibt:
readFileAndDraw :: Read a =>
String -> (Float -> Float -> a -> (String,Pos)) -> IO ()
readFileAndDraw file draw = readFileAndDo file $ scale . read
where scale a =
do putStrLn "Enter a horizontal and a vertical scaling factor!"
str <- getLine
let strs = words str
when (length strs == 2) $
do let [hor,ver] = map read strs
(code,size) = draw hor ver a
writeFile (file++".svg") $ svg code size
scale a
147
Speicher mit Update- und Zugriffsfunktion (Der Haskell-Code steht hier.)
type IOstore = Trans Store
updM :: String -> Int -> IOstore ()
updM x a = T $ \st -> ((),update st x a)
getM :: String -> IOstore Int
getM x = T $ \st -> (st x,st)
Beispiel Eine Aufgabe aus der Datenflussanalyse: Ein straight-line-Programm wird auf
die Liste der Definitions- und Verwendungsstellen seiner Variablen reduziert. Ausgehend
von einem Startzustand st filtert trace die Verwendungsstellen aus der Liste heraus und
ersetzt sie durch Paare, bestehend aus der jeweils benutzten Variable und ihrem an der
jeweiligen Verwendungsstelle gültigen Wert.
data DefUse = Def String Int | Use String
148
trace :: [DefUse] -> [(String,Int)]
trace s = f s $ const 0
where f :: [DefUse] -> Store -> [(String,Int)]
f (Def x a:s) st = f s $ update st x a
f (Use x:s) st
= (x,st x): f s st
f _ _
= []
trace [Def x 1,Use x,Def y 2,Use y,Def x 3,Use x,Use y]
; [(x,1),(y,2),(x,3),(y,2)]
trace macht alle Zustandsänderungen sichtbar.
Prozedurale Version von trace:
traceM :: [DefUse] -> [(String,Int)]
traceM s = fst $ run (f s) $ const 0
where f :: [DefUse] -> IOstore [(String,Int)]
f (Def x a:s) = do updM x a; f s
f (Use x:s)
= do a <- getM x; s <- f s; return $ (x,a):s
f _
= return []
149
traceM versteckt die Zustandsänderungen: Die Kapselung in IOStore macht Zustände
(vom Typ Store) implizit zu Werten einer globalen Variable, die – wie in imperativen
Programmen üblich – weder Parameter noch Wert der Funktionen, die sie benutzen, ist.
run(f (s)) ist die der “Prozedur” f (s) entsprechende Zustandstransformation.
Aufgabe Formuliere eine Variante von traceM , die auf Listen vom Typ [DefUseE]
anwendbar ist, wobei DefUseE wie folgt definiert ist:
data DefUseE = Def String Expr | Use String
(siehe Arithmetische Ausdrücke).
Monaden partieller Transitionsfunktionen
Diese entstehen aus den Transitionen von Trans(state), indem deren Wertetyp (a, state)
in die Maybe-Monade eingebettet wird. PTrans nimmt Maybe huckepack (piggyback):
newtype PTrans state a = PT {runP :: state -> Maybe (a,state)}
150
instance Monad (PTrans state)
where PT trans >>= f = PT $ \st -> do (a,st) <- trans st
runP (f a) st
return a = PT $ \st -> return (a,st)
fail _
= mzero
instance MonadPlus (PTrans state)
where mzero = PT $ const Nothing
PT trans `mplus` PT trans' =
PT $ \st -> trans st `mplus` trans' st
PTrans(state) liftet die Operationen >>=, mplus, return, fail und mzero von der
Maybe-Monade zum Funktionenraum state → Maybe(a, state) und zwar so, dass die
Gültigkeit der Gleichungen (1) und (2) von Maybe auf PTrans(state) übertragen wird.
Die Objekte von PTrans(state) implementieren deterministische Automaten: state ist
die Zustandsmenge, die Ausgabe- bzw. Übergangsfunktion ist durch fst ◦ runP bzw.
snd ◦ runP gegeben.
Zwei mit mplus verknüpfte Automaten m und m0 realisieren Backtracking: Erreicht m
einen Endzustand, von dem aus es keinen Zustandsübergang mehr gibt, dann wird der
Anfangzustand wiederhergestellt und m0 gestartet.
151
Monadische Compiler
Die folgenden Programme stehen hier.
Ein Compiler liest eine (auch Wort genannte) Zeichenfolge von links nach rechts, übersetzt das jeweils gelesene Teilwort in das Objekt eines Typs a und gibt daneben das
jeweilige Restwort aus. Betrachtet man den Compiler als Automaten, dann bilden die
Eingabewörter seine Zustände und die Objekte vom Typ a seine Ausgaben. Demzufolge
kann er als Objekt der Instanz
type Compiler = PTrans String
der Monade partieller Transitionsfunktionen implementiert und aus mit Hilfe der o.g.
Monaden-Kombinatoren aus elementaren Compilern zusammensetzt werden wie z.B. den
folgenden, die typische Scannerfunktionen realisieren.
Monadische Scanner
getChr :: Compiler Char
getChr = PT $ \str -> do c:str <- Just str; Just (c,str)
152
getChr liefert Nothing, wenn der Eingabestring str leer ist. Andernfalls gibt getChr das
erste Zeichen von str aus.
char(chr) und string(str) erwarten das Zeichen chr bzw. den String str:
char :: Char -> Compiler Char
char chr = sat getChr (== chr)
string :: String -> Compiler String
string = mapM char
token(p) erlaubt vor und hinter dem von p erkannten String Leerzeichen, Zeilenumbrüche
oder Tabulatoren:
Compiler a -> Compiler a
token p = do space; a <- p; space; return a
where space = many $ sat getChr (`elem` " \t\n")
Es folgen vier Scanner, die Elemente von Standardtypen erkennen und in entsprechende
Haskell-Typen übersetzen.
153
bool :: Compiler Bool
bool = msum [do token $ string "True"; return True,
do token $ string "False"; return False]
nat,int :: Compiler Int
nat = do ds <- some $ sat getChr (`elem` ['0'..'9']); return $ read ds
int = msum [nat,
do char '-'; n <- nat; return $ -n]
identifier :: Compiler String
identifier = do first <- sat getChr (`elem` ['a'..'z']++['A'..'Z'])
rest <- many $ sat getChr (`notElem` "();=!>+-*^ \t\n")
return $ first:rest
Das zweite Argument des Applikationsoperators $ endet beim ersten darauffolgenden
Semikolon, es sei denn, $ steht direkt vor do.
Die Kommas trennen die Elemente der Argumentliste von msum.
154
Binäre Bäume parsieren
tchar = token . char
bintree :: Compiler a -> Compiler (Bintree a)
bintree p = do a <- p
msum [do tchar '('
left <- bintree p
msum [do tchar ','
right <- bintree p
tchar ')'
return $ Fork left a right,
do tchar ')'
return $ Fork left a Empty],
return $ leaf a]
Z.B. übersetzt runP (bintree(int))(str) den String str, falls möglich, in einen Baum vom
Typ Bintree(Int).
Arithmetische Ausdrücke parsieren
runP (exprP )(str) übersetzt den String str, falls möglich, in ein Objekt des Typs Expr:
155
exprP,summand,factor :: Compiler Expr
exprP
= do e <- summand; moreSummands e
summand = do e <- factor; moreFactors e
factor = msum [do x <- token identifier; power $ Var x,
do i <- token int; scalar i,
do tchar '('; e <- exprP; tchar ')'; power e]
moreSummands,moreFactors,power :: Expr -> Compiler Expr
moreSummands e = msum [do tchar '-'; e' <- summand
moreSummands $ e :- e',
do es <- some $ do tchar '+'; summand
moreSummands $ Sum $ e:es,
return e]
moreFactors e = msum [do es <- some $ do tchar '*'; factor
moreFactors $ Prod $ e:es,
return e]
power e = msum [do tchar '^'; i <- token int; return $ a :^ i,
return i]
scalar :: Int -> Compiler Expr
scalar i = msum [do tchar '*'; a <- summand; return $ i :* a,
power $ Con i]
156
Die Unterscheidung zwischen Compilern für Ausdrücke, Summanden bzw. Faktoren dient
nicht nur der Berücksichtigung von Operatorprioritäten (+ und − vor ∗ und ^), sondern
auch der Vermeidung linksrekursiver Aufrufe des Compilers: Zwischen je zwei aufeinanderfolgenden Aufrufen muss mindestens ein Zeichen gelesen werden, damit der zweite
Aufruf ein kürzeres Argument hat als der erste und so die Termination des Compilers
gewährleistet ist.
Arithmetische Ausdrücke compilieren II
Ersetzt man im obigen Parser exprP und seinen Unterparsern den Datentyp Expr durch
eine Typvariable expr und die Konstruktoren von Expr durch Funktionsvariablen, dann
wird durch deren passende Instanziierung aus dem Parser ein – auf Eingabestrings operierender (!) – Interpreter bzw. ein Compiler in eine Assemblersprache wie die im Abschnitt
Arithmetische Ausdrücke compilieren vorgestellte. expr und die Funktionsvariablen werden zu einer Signatur zusammengefasst. Die Klasse ihrer Interpretationen (= Modelle
= Algebren) wird als Datentyp mit Attributen implementiert:
data ExprAlg expr = ExprAlg {con :: Int -> expr, var :: String -> expr,
sum_, prod :: [expr] -> expr,
sub :: expr -> expr -> expr,
scal :: Int -> expr -> expr,
157
expo :: expr -> Int -> expr}
Die drei Algebren, die zum Parser, Interpreter bzw. Compiler arithmetischer Ausdrücke
sind durch die folgenden Objekte vom Typ ExprAlg gegeben:
parsAlg :: ExprAlg Expr
parsAlg = ExprAlg Con Var Sum Prod (:-) (:/) (:*) (:^)
evalAlg :: ExprAlg (Store -> Int)
evalAlg = ExprAlg {con = \i -> const i,
var = \x st -> st x,
sum_ = \es st -> sum $ map ($st) es,
prod = \es st -> product $ map ($st) es,
sub = \e e' st -> e st - e' st,
scal = \i e st -> i * e st,
expo = \e i st -> e st ^ i}
compAlg :: ExprAlg [StackCom]
compAlg = ExprAlg {con = \i -> [Push i],
var = \x -> [Load x],
sum_ = \es -> concat es++[Add $ length es],
prod = \es -> concat es++[Mul $ length es],
158
sub = \e e' -> e++e'++[Sub],
scal = \i e -> Push i:e++[Mul 2],
expo = \e i -> e++[Push i,Pow]}
Aus exprP leiten wir den folgenden generischen Compiler für arithmetische Ausdrücke
ab, der syntaktisch korrekte Eingabestrings in Elemente der als Parameter übergebenen
Algebra übersetzt:
exprC :: ExprAlg expr -> Compiler expr
exprC alg = do e <- summand; moreSummands e
where summand = do e <- factor; moreFactors e
factor = msum [do x <- token identifier; power $ var alg x,
do i <- token int; scalar i,
do tchar '('; e <- exprC alg; tchar ')'
power e]
moreSummands e = msum [do tchar '-'; e' <- summand
moreSummands $ sub alg e e',
do es <- some $ do tchar '+'; summand
moreSummands $ sum_ alg $ e:es,
return e]
159
moreFactors e
= msum [do es <- some $ do tchar '*'; factor
moreFactors $ prod alg $ e:es,
return e]
power e = msum [do tchar '^'; i <- token int;
return $ expo alg e i,
return e]
scalar i = msum [do tchar '*'; e <- summand
return $ scal alg i e,
power $ con alg i]
Der Interpreter exprC(evalAlg) ist korrekt bzgl. des Interpreters
evalE : Expr → Store → Int
(siehe Arithmetische Ausdrücke interpretieren), d.h.
∀ str, str0 ∈ String, a : Store → Int :
runP (exprComp(evalAlg))(str) = Just(a, str0)
=⇒ ∃ e ∈ Expr : runP (expr)(str) = Just(e, str0) ∧ evalE(e) = a.
Der Compiler exprC(compAlg) ist korrekt bzgl. des Compilers
compileE : Expr → StackCom∗
160
(siehe Arithmetische Ausdrücke compilieren), d.h.
∀ str, str0 ∈ String, cs ∈ StackCom∗ :
runP (exprC(compAlg))(str) = Just(cs, str0)
=⇒ ∃ e ∈ Expr : runP (expr)(str) = Just(e, str0) ∧ compileE(e) = cs.
Testumgebung für Expr-Parser, -Interpreter und -Compiler
(Der Haskell-Code steht hier.)
compile :: String -> Int -> IO ()
compile file n = readFileAndDo file h
siehe Transitionsmonaden
where h str = case n of 0 -> act (exprC parsAlg) $ showExp id
1 -> act (exprC parsAlg) $ showExp reduce
2 -> act (exprC evalAlg) loopE
3 -> act exprEval loopE
4 -> act (exprC compAlg) loopC
_ -> act exprComp loopC
where act comp continue =
case runP comp str of
Just (a,"") -> continue a
Just (a,str)
-> do putStrLn $ "unparsed suffix: "++str
161
continue a
_ -> putStrLn "syntax error"
compile(file)(n) erwartet Quellcode für ein Expr-Objekt in der Datei file, parsiert es
und interpretiert bzw. compiliert es im Fall n > 1.
Im Fall n = 0 wird das von expr berechnete Expr-Objekt exp in die Datei
PainterPix/exp.svg gezeichnet. Im Fall n = 1 wird exp vorher reduziert.
Im Fall n ∈ {2, 3} wird die Schleife loopE gestartet, die in jeder Iteration eine Variablenbelegung store einliest und den Interpreter exprC(evalAlg) bzw. evalE auf exp und store
anwendet. Durch Vergleich der Ergebnisse von compile(file)(2) bzw. compile(file)(3) kann
man die o.g. Korrektheit von exprI bzgl. evalE testen.
Im Fall n > 3 wird exp mit exprC(compAlg) bzw. compileE in Zielcode übersetzt und
dann die Schleife loopC betreten, die in jeder Iteration eine Variablenbelegung store
einliest und mit execute den Zielcode von exp auf store ausführt. Durch Vergleich der
Ergebnisse von compile(file)(4) bzw. compile(file)(5) kann man die o.g. Korrektheit von
exprC bzgl. compileE testen.
162
Hilfsfunktionen von compile
showExp :: (Expr -> Expr) -> Expr -> IO ()
showExp f exp = do writeFile "exp" $ show $ f exp; drawTerm "exp"
showCode :: [StackCom] -> IO ()
showCode cs = writeFile "code" $ fold2 f "" [0..] cs
where f str n c = str++'\n':replicate (5-length lab) ' '
++lab++": "++show c where lab = show n
exprEval :: Parser (Store -> Int)
exprEval = do exp <- exprC parsAlg; return $ \store -> evalE exp store
exprComp :: Parser [StackCom]
exprComp = do exp <- exprC parsAlg; return $ compileE exp
loopE :: (Store -> Int) -> IO ()
loopE val = do (store,b) <- input
when b $ do putStrLn $ "result = "++show (val store)
loopE val
163
loopC :: [StackCom] -> IO ()
loopC code = do showCode code; loop
where loop = do (store,b) <- input
let (result:_,_) = execute code ([],store)
when b $ do putStrLn $ "result = "++show resu
loop
input :: IO (Store,Bool)
input = do putStrLn "Enter variables!"; str <- getLine
let vars = words str
putStrLn "Enter values!"; str <- getLine
return (listsToFun 0 vars $ map read $ words str,
not $ null str)
Aufgabe
Erweitere den Datentyp Expr, seine Zielsprache StackCom, seine Modelle vom Typ
ExprAlg und seinen generischen Compiler exprC und um die ganzzahlige Division.
164
Felder
Ix, die Typklasse für Indexmengen
class Ord a => Ix a
where range :: (a,a) -> [a]
index :: (a,a) -> a -> Int
inRange :: (a,a) -> a -> Bool
instance Ix Int
where range (a,b) = [a..b]
index (a,b) c = c-a
inRange (a,b) c =
a <= c && c <= b
rangeSize :: (a,a) -> Int
rangeSize (a,b) = b-a+1
rangeSize (a,b) = index (a,b) b+1
Die Standardfunktion array bildet eineListe von (Index,Wert)-Paare auf ein Feld ab:
array :: Ix a => (a,a) -> [(a,b)] -> Array a b
mkArray (a,b) wandelt die Einschränkung einer Funktion f : A → B auf das Intervall
[a, b] ⊆ A in ein Feld um:
mkArray :: Ix a => (a,a) -> (a -> b) -> Array a b
mkArray (a,b) f = array (a,b) [(x,f x) | x <- range (a,b)]
165
Zugriffsoperator für Felder:
(!) :: Ix a => Array a b -> a -> b
Funktionsapplikation wird zum Feldzugriff: Für alle i ∈ [a, b], f (i) = mkArray(f )!i.
Update-Operator für Felder:
(//) :: Ix a => Array a b -> [(a,b)] -> Array a b
Für alle Felder arr mit Indexmenge A und Wertemenge B,
s = [(a1, b1), . . . (an, bn)] ∈ (A × B)∗ and a ∈ A gilt also:
bi
falls a = ai für ein 1 ≤ i ≤ n,
(arr//s)!a =
arr!a sonst.
a1, . . . , an sind genau die Positionen des Feldes arr, an denen es sich von arr//s unterscheidet.
166
Matrizenrechnung auf Basis von Feldern
Funktionen mit einem endlichem Definitionsbereich, dessen Elemente einem Indextyp (=
Typ der Klasse Ix) angehören, sollten als Felder implementiert werden. Die benötigen weniger Speicherplatz als die ursprünglichen Funktionen, weil diese durch – manchmal sehr
umfangreiche – λ-Ausdrücke repräsentiert werden. Der im Abschnitt Matrizenrechnung
definierte Datentyp für Matrizen hat z.B. folgende Felddarstellung:
data Mat r = Mat {dim :: Pos, mat :: Array Pos r}
mkMat :: Pos -> (Pos -> r) -> Mat r
mkMat dim = Mat dim . mkArray ((1,1),dim)
Die Definitionen der Matrixoperationen im Abschnitt Matrizenrechnung können übernommen werden. Es ist dort lediglich jeder Funktionsaufruf mat(m)(p) durch den entsprechenden Feldzugriff mat(m)!p zu ersetzen und jedes Vorkommen des Konstruktors
Mat durch die Funktion mkMat zu ersetzen.
167
Dynamische Programmierung
verbindet die rekursive Implementierung einer oder mehrerer Funktionen mit Memoization, das ist die Speicherung der Ergebnisse rekursiver Aufrufe in einer Tabelle (die
üblicherweise als Feld implementiert wird), so dass diese nur einmal berechnet werden
müssen, während weitere Vorkommen desselben rekursiven Aufrufs durch Tabellenzugriffe ersetzt werden können. Exponentieller Zeitaufwand wird auf diese Weise oft auf
linearen heruntergedrückt.
Beispiel Fibonacci-Zahlen
fib 0 = 1
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
Wegen der binärbaumartigen Rekursion in der Definition von fib benötigt fib(n) 2n Rechenschritte. Ein äquivalentes dynamisches Programm lautet wie folgt:
fibA = mkArray (0,1000000) fib where fib 0 = 1
fib 1 = 1
fib n = fibA!(n-1) + fibA!(n-2)
168
fibA!n benötigt nur O(n) Rechenschritte. Der Aufruf führt zur Anlage des Feldes fibA,
in das nach Definition von mkArray (s.o) hintereinander die Werte der Funktion fib
von fib(0) bis fib(n) eingetragen werden. Für alle i > 1 errechnet sich fib(i) aus Funktionswerten an Stellen j < i. Diese stehen aber bereits in fibA, wenn der i-te Eintrag
vorgenommen wird. Folglich sind alle rekursiven Aufrufe in der ursprünglichen Definition
von fib als Zugriffe auf bereits belegte Positionen von fibA implementierbar.
ghci gibt z.B. 19,25 Sekunden als Rechenzeit für fib(33) an. Für fibA!33 liegt sie hingegen
unter 1/100 Sekunde.
Beispiel Alignments
(Der Haskell-Code steht hier. Die algebraische Behandlung bioinformatischer Probleme
geht zurück auf: R. Giegerich, A Systematic Approach to Dynamic Programming in
Bioinformatics, Bioinformatics 16 (2000) 665-677.)
Zwei Listen xs und ys des Typs String ∗ sollen in die Menge alis(xs, ys) der Alignments
von xs und ys übersetzt werden. Wir setzen eine Boolesche Funktion
compl : String 2 → Bool
voraus, die für je zwei Strings x und y angibt, ob x und y komplementär zueinander sind
und deshalb aneinander “andocken” können (was auch im Fall x = y möglich ist).
169
Zwei Alignments von a c t a c t g c t und a g a t a g
Ein Alignment von a d f a a a a a a und a a a a a a d f a
170
Tripellistendarstellung von Alignments
Sei A = String ] {Nothing} und h : A∗ → String ∗ die Funktion, die aus einem Wort
über A alle Vorkommen von Nothing streicht. Dann ist für alle xs, ys ∈ String ∗ die
Menge der Alignments von xs und ys wie folgt definiert:
S|xs|+|ys|
alis(xs, ys) =def
n=max(|xs|,|ys|)
{[(a1, b1, c1), . . . , (an, bn, cn)] ∈ (A × A × RGB)n |
h(a1 . . . an) = xs ∧ h(b1 . . . bn) = ys ∧
∀ 1 ≤ i ≤ n : (ci = red ∧ compl(ai, bi)) ∨
(ci = green ∧ ai = bi 6= Nothing) ∨
(ci = white ∧ ((ai = Nothing ∧ bi 6= Nothing)
∨ (bi = Nothing ∧ ai 6= Nothing))}
Alignments lassen sich demnach durch folgenden Datentyp implementieren:
type Alignment = [(Maybe String,Maybe String,RGB)]
Nur Alignments mit maximalem Matchcount und darunter diejenigen mit maximalen
zusammenhängenden Matches sollen berechnet werden.
171
matchcount(s) zählt die Vorkommen von red und green im Alignment s:
matchcount :: Alignment -> Int
matchcount = length . filter (/= white) . map (\(_,_,c) -> c)
maxmatch(s) liefert die Länge der maximalen zusammenhängenden Teillisten von s mit
ausschließlich grünen oder roten Farbkomponenten:
maxmatch :: Alignment -> Int
maxmatch s = max m n
where (_,m,n) = foldl f (False,0,0) s
f (b,m,n) (_,_,c) = if c == white then (False,0,max m n)
else (True,if b then m+1 else 1,n)
maxima(f )(s) ist die Teilliste aller a ∈ s mit maximalem Wert f (a):
maxima :: Ord b => (a -> b) -> [a] -> [a]
maxima f s = filter ((== m) . f) s where m = maximum $ map f s
Aufgabe Definiere
maximum . map maxmatch :: [Alignment] -> [Alignment]
durch zwei ineinandergeschachtelte Faltungen ohne Verwendung von maximum.
172
Eine Signatur für Alignment-Darstellungen
Ähnlich wie die Übersetzung arithmetischer Ausdrücke im Abschnitt Arithmetische Ausdrücke compilieren II, so kann auch die Berechnung von Alignments generisch erfolgen,
wenn wir bei den verwendeten Hilfsfunktionen den monomorphen Typ Alignment durch
eine Typvariable ali ersetzen und die Hilfsfunktionen in einer Signatur zusammenfassen,
deren Algebrenklasse hier folgendermaßen aussieht:
data AliAlg ali = AliAlg {compl :: String -> String -> Bool,
matchcount,maxmatch :: ali -> Int,
empty :: ali,
appendx,appendy :: String -> ali -> ali,
equal,match :: String -> String -> ali -> ali}
Damit berechnet optAlis(n)(xs, ys)(alg), n = 0, 1, 2, alle optimalen Alignments von xs
und ys und stellt sie gemäß alg dar:
type Genes = ([String],[String])
type Relation a = a -> a -> Bool
optAlis :: Int -> Genes -> AliAlg ali -> [ali]
optAlis 0 (xs,ys) alg = maxima (maxmatch alg) $ align (xs,ys)
where align :: Genes -> [ali]
173
align = maxima (matchcount alg) . f
f (xs,ys) = if null xs
then if null ys then [empty alg] else alis4
else if null ys then alis3
else if x == y then alis1++alis3++alis4
else if compl alg x y then alis2++alis3++alis4
else alis3++alis4
where x:xs' = xs; y:ys' = ys
alis1 = map (equal alg x y) $ align (xs',ys')
alis2 = map (match alg x y) $ align (xs',ys')
alis3 = map (appendx alg x) $ align (xs',ys)
alis4 = map (appendy alg y) $ align (xs,ys')
optAlis 1 (xs,ys) alg = maxima (maxmatch alg) $ align (0,0)
where lg1 = length xs; lg2 = length ys
align :: Pos -> [ali]
align = maxima (matchcount alg) . f
f (i,j) = if i == lg1
then if j == lg2 then [empty alg] else alis4
else if j == lg2 then alis3 else ... s.o. ...
where x = xs!!i; y = ys!!j
alis1 = map (equal alg x y) $ align (i+1,j+1)
174
alis2 = map (match alg x y) $ align (i+1,j+1)
alis3 = map (appendx alg x) $ align (i+1,j)
alis4 = map (appendy alg y) $ align (i,j+1)
optAlis 2 (xs,ys) alg = maxima (maxmatch alg) $ align!(0,0)
where lg1 = length xs; lg2 = length ys
align :: Array Pos [ali]
align = mkArray ((0,0),(lg1,lg2)) $ maxima (matchcount alg) . f
f (i,j) = ... s.o. ...
where x = xs!!i; y = ys!!j
... s.o. ...
alis1 = map (equal alg x y) $ align!(i+1,j+1)
alis2 = map (match alg x y) $ align!(i+1,j+1)
alis3 = map (appendx alg x) $ align!(i+1,j)
alis4 = map (appendy alg y) $ align!(i,j+1)
optAlis(0)(xs, ys) arbeitet direkt auf den Eingabelisten xs und ys. optAlis(1)(xs, ys)
operiert auf Paaren von Positionen der Elemente von xs bzw. ys und macht align damit
zu einer rekursiv definierten Funktion auf der Indexmenge (!) Pos, die mit optAlis(2)
– nach dem Schema von fibA (s.o.) – als Feld dargestellt wird. Wegen der baumartigen
Rekursion benötigt align(0, 0) O(3lg1+lg2) Rechenschritte, während beim entsprechenden
Feldzugriff align!(0, 0) nur das Feld zu füllen ist, was in O(lg1 + lg2) Schritten erfolgt.
175
Testumgebung zur Alignment-Erzeugung (Der Haskell-Code steht hier.)
drawAlis(file) lädt zwei untereinander geschriebene Stringlisten aus der Datei file und
schreibt ihre Alignments in die Datei fileAlign, wo sie drawLGraph liest und in die Datei
PainterPix/fileAlign.svg zeichnet.
drawAlis ruft optAlis mit der Algebra aliList auf, die auf der oben beschriebenen Tripellistendarstellung von Alignments basiert und compl durch den genetischen Code interpretiert
drawAlis :: Int -> String -> IO ()
drawAlis n file = readFileAndDo file f
where f str = do writeFile alignFile $ show $ concat $
zipWith mkPaths [0..] $
optAlis n (words str1,words str2) aliList
drawLGraph alignFile
where alignFile = file++"Align"
(str1,str2) = break (== '\n') str
176
mkPaths :: Int -> Alignment -> [LCPath]
mkPaths j ali = hor 0:hor 1:zipWith ver [0..] cols
where (s1,s2,cols) = unzip3 ali
hor k = ([p "" 0 k,p "" (length s1-1) k],blue)
ver i col = ([q s1 i 0,q s2 i 1],col)
p str i k = (str,float i,float $ j+j+k)
q s i = p (case s!!i of Just a -> a; _ -> "") i
aliList :: AliAlg Alignment
aliList = AliAlg {compl
= \x y -> compl x y || compl y x,
matchcount = matchcount
maxmatch
= maxmatch,
empty
= [],
appendx
= \x ali
-> (Just x,Nothing,white):ali,
appendy
= \y ali
-> (Nothing,Just y,white):ali,
equal
= \x y ali -> (Just x,Just y,green):ali,
match
= \x y ali -> (Just x,Just y,red):ali}
where compl x y = x == "a" && y == "t" || x == "c" && y == "g"
genetischer Code
177
Konstruktoren und Destruktoren*
Um Modelle schrittweise aufzubauen, wird die Menge der Signaturen induktiv definiert:
• Σ = (S, F ) ist eine Signatur, falls S eine endliche Menge von Sorten und F eine
endliche Menge von Funktionssymbolen f : s1 × · · · × sn → s ist.
• Σ = (S, F, BΣ) ist eine Signatur, falls BΣ eine Signatur (die Basissignatur von Σ)
und (S, F ) eine Signatur ist, wobei S und F die Sorten- bzw. Funktionssymbolmengen
BS bzw. BF der Basissignatur enthalten.
f : s1 × · · · × sn → s ∈ F mit s ∈ S \ BS heißt Konstruktor.
f heißt Destruktor, falls o.B.d.A. s1 ∈ S \ BS und s2, . . . , sn ∈ BS.
Konstruktoren dienen der Erzeugung von Elementen der Menge, die eine Σ-Algebra (s.u.)
der Wertesorte von f zuordnet. Destruktoren deinen der Veränderung oder Beobachtung
von Elementen der Menge, die eine Σ-Algebra der ersten Argumentsorte von f zuordnet.
Eine Σ-Algebra interpretiert jede Sorte s von Σ als Menge As, die Trägermenge von
s, und jedes Funktionssymbol f : s1 × · · · × sn → s von Σ als Funktion
f A : As1 × . . . × Asn → As.
178
Zwei Beispielsignaturen
Sei Σ = (S, F ) und S = {entry, tree, treelist}. F bestehe aus den Konstruktoren
join : entry × treelist → tree
nil : 1 → treelist
cons : tree × treelist → treelist
A mit
• Atree = Menge der endlichen Bäume mit Knoteneinträgen aus Aentry ,
• Atreelist = Menge der endlichen Listen von Elementen aus Atree,
• joinA(e, ts) = Baum mit Wurzeleintrag e ∈ Aentry und Unterbaumliste s ∈ Atreelist,
• nilA = [] und consA(t, ts) = t : ts
ist eine Σ-Algebra. Implementierung in Haskell:
data Tree entry
= Join entry (Treelist entry)
data Treelist entry = Nil | Cons (Tree entry) (Treelist entry)
A interpretiert alle Konstruktoren von Σ durch Haskell-Konstruktoren.
Sei A eine Σ-Algebra und f : s1 × · · · × sn → s ∈ F . Wird f in A wie folgt interpretiert:
∀ a1, . . . , an ∈ As1 × . . . × Asn : f A(a1, . . . , an) = f (a1, . . . , an),
dann nennen wir f einen Konstruktor von A.
179
Sei Σ = (S, F ) und S = {entry, tree, treelist, pair, 1 + pair}. F bestehe aus den Konstruktoren
root : tree → entry
subtrees : tree → treelist
split : treelist → 1 + pair
fst : pair → tree
snd : pair → treelist
A mit
• Atree = Menge der Bäume mit Knoteneinträgen aus Aentry , endlicher oder unendlicher
Tiefe und endlichem oder unendlichen Knotenausgrad,
• Atreelist = Menge der endlichen oder unendlichen Listen von Elementen aus Atree,
• Apair = Atree × Atreelist,
• A1+pair = Apair ] {∗},
• rootA(t) = Wurzeleintrag von t ∈ Atree,
• subtreesA(t) = Unterbaumliste s ∈ Atreelist,
• splitA([]) = ∗, splitA(t : ts) = (t, ts),
• fst A(t, ts) = t und sndA(t, ts) = ts
ist eine Σ-Algebra.
180
Implementierung von A in Haskell:
data Tree entry
= Tree {root :: entry, subtrees :: Treelist entry}
data Treelist entry = Treelist {split :: Maybe (Tree entry,
Treelist entry)}
Der Typkonstruktor Maybe dient hier der Implementierung des Typs 1 + pair. Die Destruktoren fst und snd werden durch die gleichnamige Standardfunktionen von Haskell
implementiert.
Synthese und Analyse algebraischer Modelle sind Thema der Lehrveranstaltungen Einführung in den logisch-algebraischen Systementwurf und Logisch-algebraischer Systementwurf.
181
Semantik funktionaler Programme*
Jeder Aufruf eines Haskell-Programms ist ein Term, der aus Standard- und selbstdefinierten Funktionen zusammengesetzt ist. Demzufolge besteht die Ausführung von HaskellProgrammen in der Auswertung funktionaler Ausdrücke. Da sowohl Konstanten als auch
Funktionen rekursiv definiert werden können, kann es passieren, dass die Auswertung eines Terms – genauso wie die Ausführung eines imperativen Programms – nicht terminiert.
Das kann und soll grundsätzlich auch nicht verhindert werden. Z.B. muss die Funktion,
die den Interpreter einer Programmiersprache mit Schleifenkonstrukten darstellt, auch
im Fall einer unendlichen Zahl von Schleifendurchläufen eine Semantik haben.
Selbstverständlich spielt die Auswertungsstrategie, also die Auswahl des jeweils nächsten Auswertungsschritts eine wichtige Rolle, nicht nur bezüglich des Ergebnisses der
Auswertung, sondern auch bei der Frage, ob überhaupt ein Ergebnis erreicht wird. So
kann mancher Term mit der einen Strategie in endlicher Zeit ausgewertet werden, mit
einer anderen jedoch nicht. Und stellt der Term eine (partielle) Funktion dar, dann kann
können sich die Ergebnisse seiner Auswertung mit verschiedenen Strategien in der Größe
des Definitionsbereiches der Funktion unterscheiden.
182
Um alle diese Fragen präzise beantworten und Auswertungsstrategien miteinander vergleichen zu können, benötigt man eine vom Auswertungsprozess, der operationellen
Semantik, unabhängige denotationelle Semantik von Termen, egal ob diese Konstanten oder Funktionen darstellen.
Ein Haskell-Programm besteht i.w. aus Gleichungen. Diese beschreiben zunächst lediglich
Anforderungen an die in ihnen auftretenden Konstanten oder Funktionen. Deren denotationelle Semantik besteht in Lösungen der Gleichungen. Lösungen erhält man aber nur in
bestimmten mathematischen Strukturen wie z.B. den im Abschnitt CPOs und Fixpunkte
definierten CPOs.
Das relationale Berechnungsmodell
In der logischen oder relationalen Programmierung ist das Lösen von Gleichungen
und anderen prädikatenlogischen Formeln das eigentliche Berechnungsziel: Die Ausführung eines logisches Programms besteht in der schrittweisen Konstruktion von Belegungen
der freien Formelvariablen durch Werte, welche die Formel gültig machen. Kurz gesagt,
Formeln werden zu sie erfüllende Belegungen ausgewertet. Sie werden schrittweise konstruiert, indem die auszuwertende Formel ϕ mit Gleichungen der Form x = t konjunktiv
verknüpft wird. x = t beschreibt eine Belegung der Variablen x durch den aus Konstruktoren und Variablen bestehenden (!) Term t.
183
Ist die Auswertung von ϕ erfolgreich, dann endet sie mit einer Konjunktion ψ der Form
x1 = t1 ∧ · · · ∧ xn = tn. ϕ, ψ und die von den einzelnen Auswertungschritten erzeugten
Zwischenergebnisse bilden eine logische Reduktion, also eine Folge von Formeln, die
von ihren jeweiligen Nachfolgern in der Folge impliziert werden. Damit ist klar, dass
ψ Belegungen repräsentiert, die ϕ erfüllen. Logische Reduktionen und die Regeln, die
sie erzeugen (Simplifikation, Co/Resolution und relationale Co/Induktion), spielen in der
Verifikation logisch-algebraischer Modelle eine entscheidende Rolle (siehe Algebraic Model
Checking and more).
Den Auswertung eines Terms t durch Anwendung der Gleichungen eines Haskell-Programms entspricht einer logischen Reduktion der Formel x = t.
Beispiel client/server
0
client
requests
server
184
client :: a -> [b] -> [a]
client a s = a:client (f b) s' where b:s' = s
server :: [a] -> [b]
server (a:s) = g a:server s
requests :: [a]
requests = client 0 $ server requests
Wir wollen den Term take(3)(requests) auswerten und konstruieren dazu eine logische
Reduktion der Gleichung s = take(3)(requests) mit der freien Variable s. Jeder Reduktionsschritt besteht in der Anwendung einer der obigen Gleichungen von links nach rechts,
wobei das jeweilige Ergebnis im Fall der client-Gleichung mit der entsprechenden Instanz der lokalen Definition b : s0 = s konjunktiv verknüpft wird. Die Variablen b und
s0 sind implizit existenzquantifiziert und müssen daher werden bei jeder Anwendung der
client-Gleichung durch neue Variablen ersetzt werden (a0, . . . , a5 bzw. s0, . . . , s5). Für
jeden Reduktionsschritt ϕ → ψ gilt:
∃ a0, . . . , a5, s0, . . . , s5 : ψ ⇒ ∃ a0, . . . , a5, s0, . . . , s5 : ϕ.
185
Sei f = (∗2) und g = (+1). Dann lautet die gesamte Reduktion wie folgt:
→
→
→
→
→
→
→
→
→
→
s = take 3 requests
s = take 3 $ client 0 $ server requests
s = take 3 $ 0:client (f a0) s0 ∧ a0:s0 = server requests
s = 0:take 2 (client (f a0) s0) ∧ a0:s0 = server requests
s = 0:take 2 (f a0:client (f a1) s1) ∧ a1:s1 = s0 ∧
a0:s0 = server requests
s = 0:f a0:take 1 (client (f a1) s1) ∧ a1:s1 = s0 ∧
a0:s0 = server requests
s = 0:f a0:take 1 (f a1:client (f a2) s2) ∧ a2:s2 = s1 ∧
a1:s1 = s0 ∧ a0:s0 = server requests
s = 0:f a0:f a1:take 0 (client (f a2) s2) ∧ a2:s2 = s1 ∧
a1:s1 = s0 ∧ a0:s0 = server requests
s = 0:f a0:f a1:[] ∧ a2:s2 = s1 ∧ a1:s1 = s0 ∧
a0:s0 = server requests
s = [0,f a0,f a1] ∧ a2:s2 = s1 ∧ a1:s1 = s0 ∧
a0:s0 = server requests
s = [0,f a0,f a1] ∧ a2:s2 = s1 ∧ a1:s1 = s0 ∧
a0:s0 = server $ client 0 $ server requests
186
→
→
→
→
→
→
→
→
→
s = [0,f a0,f a1] ∧ a2:s2 = s1 ∧ a1:s1 = s0 ∧
a0:s0 = server $ 0:client (f a3) s3 ∧ a3:s3 = server requests
s = [0,f a0,f a1] ∧ a2:s2 = s1 ∧ a1:s1 = s0 ∧
a0:s0 = g 0:server (client (f a3) s3) ∧ a3:s3 = server requests
s = [0,f a0,f a1] ∧ a2:s2 = s1 ∧ a1:s1 = s0 ∧
a0:s0 = 1:server (client (f a3) s3) ∧ a3:s3 = server requests
s = [0,f 1,f a1] ∧ a2:s2 = s1 ∧ a1:s1 = s0 ∧
s0 = server $ client (f a3) s3 ∧ a3:s3 = server requests
s = [0,2,f a1] ∧ a2:s2 = s1 ∧
a1:s1 = server $ client (f a3) s3 ∧ a3:s3 = server requests
s = [0,2,f a1] ∧ a2:s2 = s1 ∧
a1:s1 = server $ f a3:client (f a4) s4 ∧ a4:s4 = s3 ∧
a3:s3 = server requests
s = [0,2,f a1] ∧ a2:s2 = s1 ∧
a1:s1 = g (f a3):server (client (f a4) s4) ∧ a4:s4 = s3 ∧
a3:s3 = server requests
s = [0,2,f (g (f a3))] ∧ a2:s2 = s1 ∧
s1 = server $ client (f a4) s4 ∧ a4:s4 = s3 ∧
a3:s3 = server requests
s = [0,2,f (g (f a3))] ∧ a2:s2 = server $ client (f a4) s4 ∧
a4:s4 = s3 ∧ a3:s3 = server requests
187
→
→
→
→
→
→
s = [0,2,f (g (f a3))] ∧ a2:s2 = server $ client (f a4) s4 ∧
a4:s4 = s3 ∧ a3:s3 = server $ client 0 $ server requests
s = [0,2,f (g (f a3))] ∧ a2:s2 = server $ client (f a4) s4 ∧
a4:s4 = s3 ∧ a3:s3 = server $ 0:client (f a5) s5 ∧ a5:s5 = s4
s = [0,2,f (g (f a3))] ∧ a2:s2 = server $ client (f a4) s4 ∧
a4:s4 = s3 ∧ a3:s3 = g 0:server $ client (f a5) s5 ∧ a5:s5 = s4
s = [0,2,f (g (f a3))] ∧ a2:s2 = server $ client (f a4) s4 ∧
a4:s4 = s3 ∧ a3:s3 = 1:server (client (f a5) s5) ∧ a5:s5 = s4
s = [0,2,f (g (f 1))] ∧ a2:s2 = server $ client (f a4) s4 ∧
a4:s4 = s3 ∧ s3 = server $ client (f a5) s5 ∧ a5:s5 = s4
s = [0,2,6] ∧ a2:s2 = server $ client (f a4) s4 ∧
a4:s4 = server $ client (f a5) s5 ∧ a5:s5 = s4
Die logische Reduktion zeigt die prinzipielle Vorgehensweise bei der Auswertung eines
Terms durch Anwendung von Gleichungen eines Haskell-Programms. Da funktionale Programme, als prädikatenlogische Formeln betrachtet, nur ein einziges Relationssymbol enthalten, nämlich das Gleichheitssymbol, können sie – wie in den folgenden Abschnitten
ausgeführt wird – selbst als Terme repräsentiert werden. Logische Reduktionen können
dann durch Termreduktionen simuliert werden. Das spart Platz und Zeit, insbesondere
weil die häufige Erzeugung neuer Variablen (siehe obiges Beispiel) überflüssig wird.
188
Das funktionale Berechnungsmodell
An die Stelle der obigen logischen Reduktion
s = take(3)(requests) → . . . → s = [0, 2, 6] ∧ ϕ(a2, a4, a5, s2, s4, s5)
(1)
tritt eine Termreduktion:
take(3)(requests) → . . . → [0, 2, 6]
(2)
Während die logische Reduktion (1) als umgekehrte Implikation interpretiert wird:
s = take(3)(requests)
⇐= s = [0, 2, 6] ∧ ∃ a2, a4, a5, s2, s4, s5 : ϕ(a2, a4, a5, s2, s4, s5),
(3)
ist die Semantik einer Termreduktion t →∗ t0 durch die Äquivalenz der Terme t und t0
bzgl. ihrer Interpretationen in einer bestimmten Σ-Algebra A gegeben (s.u.).
Um (1) in (2) umwandeln zu können, müssen wir zunächst die in (1) verwendeten Konstanten- und Funktionsdefinitionen in λ- und µ-Abstraktionen transformieren. λ-Abstraktionen kennen wir schon aus dem Abschnitt Funktionen. Eine µ-Abstraktion µp.t bezeichnet die kleinste Lösung der Gleichung p = u. Sie existiert, wenn t in einem CPO (siehe
CPOs und Fixpunkte) interpretiert wird.
189
Wir müssen zunächst grundlegende Begriffe, auf denen das Rechnen mit Termen basiert,
präzisieren.
Terme und Substitutionen
Sei Σ = (S, F, BΣ) eine Signatur, BS die Menge der Sorten von BΣ und C ⊆ F eine
(S \ BS)-sortige Menge von Konstruktoren derart, dass für alle s ∈ S \ BS, Cs nicht leer
ist (siehe Konstruktoren und Destruktoren).
Eine S-sortige Menge A ist eine Mengenfamilie: A = {As | s ∈ S}. Man sagt “Mengenfamilie” und nicht, was die Schreibweise nahelegt, “Menge von Mengen”, um Antinomien
(logische Widersprüche) wie die Menge aller Mengen zu vermeiden.
Die Menge types(S) der Typen über S ist induktiv definiert:
• S ⊆ types(S),
• e1, . . . , en ∈ types(S) =⇒ e1 × · · · × en ∈ types(S),
• e, e0 ∈ types(S) =⇒ e → e0 ∈ types(S).
Die Interpretation von S in einer Σ-Algebra A wird wie folgt auf types(S) fortgesetzt:
Ae1×···×en =def As1 × . . . × Asn ,
Ae→e0
=def Ae → Ae0 .
190
Sei X eine types(S)-sortige Menge. Die S-sortige Menge P (X, C) der Σ-Muster über X
und C und die types(S)-sortige Menge TΣ(X, C) der Σ-Terme sind induktiv definiert:
• Für alle e ∈ types(S), Xe ⊆ P (T, X)e ∩ TΣ(X, C)e.
• Für alle e = e1 × · · · × en ∈ types(S), p1 ∈ P (X, C)e1 , . . . , pn ∈ P (X, C)en ,
∀ 1 ≤ i < j ≤ n : var(pi) ∩ var(pj ) = ∅ =⇒ (p1, . . . , pn) ∈ P (X, C)e.
• Für alle f : e → s ∈ C and p ∈ P (X, C)e, f (p) ∈ P (X, C)s.
• Für alle f : e → s ∈ F , f ∈ TΣ(X, C)e→s.
• Für alle e = e1 × · · · × en ∈ types(S), t1 ∈ TΣ(X, C)e1 , . . . , tn ∈ TΣ(X, C)en ,
(t1, . . . , tn) ∈ TΣ(X, C)e.
• Für alle e, e0 ∈ types(S), t ∈ TΣ(X, C)e→e0 und u ∈ TΣ(X, C)e, t(u) ∈ TΣ(X, C)e0 .
• Für alle e, e0 ∈ types(S), p ∈ P (X, C)e und t ∈ TΣ(X, C)e0 , λp.t ∈ TΣ(X, C)e→e0 .
• Für alle e ∈ types(S), p ∈ P (X, C)e und t ∈ TΣ(X, C)e, µp.t ∈ TΣ(X, C)e.
Sei t = λp.u oder t = µp.u. λp bzw. µp nennt man den Kopf und u den Rumpf von t.
Die Variablen des Kopfes heißen gebunden in t, die restlichen Variablen sind frei in t.
Für alle t ∈ TΣ(X, C) ist var(t) die Menge aller Variablen von t und free(t) die Menge
aller Variablen von t, die in keiner in t enthaltenen Abstraktion gebunden sind.
191
Sei s ∈ BS. x ∈ Xs heißt Σ-primitiv. BTΣ(X, C) bezeichnet die Menge der Σ-Terme
über X und C, deren freie Variablen Σ-primitiv sind.
Seien A und B S-sortige Mengen. Eine S-sortige Funktion f : A → B ist eine Menge
von Funktionen: f = {fs : As → Bs | s ∈ S}.
Eine S-sortige Funktion σ : X → TΣ(X, C) heißt Substitution. σ wird wie folgt zur
Funktion σ ∗ : TΣ(X, C) → TΣ(X, C) fortgesetzt:
σ ∗(x)
σ ∗(f )
σ ∗((t1, . . . , tn))
σ ∗(t(u))
σ ∗(λp.t)
σ ∗(µp.t)
= σ(x)
für alle x ∈ X
=f
für alle f ∈ F
= (σ ∗(t1), . . . , σ ∗(tn))
= σ ∗(t)(σ ∗(u))
∗
= λρ∗var(p)(p).σvar(p)
(t)
∗
= µρ∗var(p)(p).σvar(p)
(t)
Für alle V ⊆ X sind ρV : X → X bzw. σV : X → TΣ(X, C) wie folgt definiert:
0
x falls x ∈ V ∩ free(σ(free(t))) und x 0 6∈ V ∩ free(σ(free(t))),
ρV (x) =def
x sonst,
σV (x) =def
x0
falls x ∈ V,
σ(x) sonst.
192
Die Variablenumbenennung ρV stellt sicher, dass die Instanziierung des Rumpfes einer Abstraktion keine zusätzlichen Vorkommen ihrer gebundenen Variablen in den Term
einführt. Im klassischen λ-Kalkül (in dem es anstelle beliebiger Muster nur einzelne Variablen gibt) spricht man von α-Konversion.
σ ∗(t) ist die σ-Instanz von t. Aus Instanziierungen ergibt sich die Subsumptionsordnung ≤ auf Termen:
u ≤ t ⇐⇒def
t ist eine Instanz von u.
In entsprechender Weise müssen machmal gebundene Variablen quantifizierter prädikatenlogischer Formeln vor der Substitution ihrer freien Variablen umbenannt werden.
Für alle σ : X → TΣ(X, C), t, u ∈ TΣ(X, C) und x ∈ X sind σ[u/x] : X → TΣ(X, C)
bzw. t[u/x] ∈ TΣ(X, C) wie folgt definiert:
u
falls z = x,
σ[u/x](z) =
σ(z) sonst,
t[u/x]
= id[u/x]∗(t).
193
Termreduktionen
t → t0 ist eine Reduktionsregel, falls t und t0 Terme mit free(t 0) ⊆ free(t) sind.
Sei Red eine Menge von Reduktionsregeln. Die Reduktionsrelation →Red ⊆ BTΣ(X, C)2
ist wie folgt definiert:
∃ v → v 0 ∈ Red, u ∈ TΣ(X, C), x ∈ free(u),
0
t →Red t ⇐⇒def
σ : X → TΣ(X, C) : t = u[σ ∗(v)/x] ∧ t0 = u[σ ∗(v 0)/x].
v und t0 nennt man einen Red-Redex bzw. ein Red-Redukt von t.
Reduktionsregeln für einige Standardfunktionen
x+0
x∗0
True && x
False && x
πi(x1, . . . , xn)
head(x : xs)
tail(x : xs)
if True then x else y
if False then x else y
→
→
→
→
→
→
→
→
→
x
0
x
False
xi
x
xs
x
y
1≤i≤n
Regeln für Standardfunktionen werden im klassischen λ-Kalkül δ-Regeln genannt.
194
Regeln für λ-Applikationen
Sei p ∈ P (X, C), t, u ∈ TΣ(X, C), x ∈ X, f ∈ F und σ : X → TΣ(X, C).
λx.f (x)
→ f
(λp.t)(pσ)
→ tσ
(λp.t)(u)
→ fail
falls u ∈ P (X, C) und p 6≤ u
(if b then x else y)(z) → if b(z) then x(z) else y(z)
...
Die ersten beiden Regeln heißen im klassischen λ-Kalkül η-Regel bzw. β-Regel.
Die Konstante fail ähnelt der Konstanten Nothing einer Instanz des Haskell-Datentyps
Maybe. Der Operator mplus der Typklasse MonadPlus war für Maybe wie folgt definiert:
m `mplus` m' = case m of Nothing -> m'; _ -> m
Umgekehrt werden bei der Reduktion von case-Ausdrücken und induktiv definierten
Funktionen in λ-Ausdrücke (s.u.) Ausdrücke der Form eke0 eingeführt. Die Regeln für den
Operator k entsprechen der Definition von mplus: Reduktionsregeln: Sei x, x1, . . . , xn, z ∈
X und p ∈ P (X, C).
fail kx
→ x
pkx
→ t
(x1k . . . kxn)(z) → x1(z)k . . . kxn(z)
195
falls p 6= fail
Reduktionsregel für case-Ausdrücke


case t of p1 | b1 → t1 
 (λp1.if b1 then t1 else fail )(t)
...
...
→


pn | bn → tn
k (λpn.if bn then tn else fail )(t)
Korrektheit von Termreduktionen
Sei A eine Σ-Algebra derart, dass C aus Konstruktoren von A besteht (siehe Konstruktoren und Destruktoren).
Eine S-sortige Funktion β : X → A heißt Variablenbelegung in A. Für alle Variablenbelegungen γ : X → A und x ∈ X ist β[γ/V ] : X → A wie folgt definiert:
γ(x) falls x ∈ V,
β[γ/V ](x) =
β(x) sonst.
Die von β abhängige Auswertungsfunktion β ∗ : TΣ(X, C) → A ist induktiv definiert:
• Für
• Für
• Für
• Für
alle
alle
alle
alle
x ∈ X, β ∗(x) = β(x).
f ∈ F , β ∗(f ) = f A.
t = (t1, . . . , tn) ∈ TΣ(X, C), β ∗(t) = (β ∗(t1), . . . , β ∗(tn)).
t(u) ∈ TΣ(X, C), β ∗(t(u)) = β ∗(t)(β ∗(u)).
196
• Für alle e ∈ types(s), p ∈ P (X, C)e, t = λp.u ∈ TΣ(X, C) und a ∈ Ae,
β[γ/var(p)]∗(u) falls ∃ γ : X → A : a = γ ∗(p),
∗
β (t)(a) =
fail
sonst.
• Für alle t = µp.u ∈ TΣ(C, X), β ∗(t) = β[γ/var(p)]∗(u), wobei γ : X → A die kleinste
Variablenbelegung mit γ ∗(p) = γ ∗(u) ist (siehe Partiell-rekursive Funktionen).
R ⊆ TΣ(X, C)2 heißt korrekt bzgl. A, falls für alle (t, t0) ∈ R, β : X → A und V ⊆ X
β ∗(t) = β ∗(t0).
Ist Red korrekt bzgl. A, dann ist auch →∗Red korrekt bzgl. A.
Induktiv definierte Funktionen
Um die Ausführung von Haskell-Programmen mit Hilfe von Termreduktionen beschreiben
zu können, müssen wir alle Funktionsdefinitionen in λ- oder µ-Abstraktionen überführen.
Wir betrachten zunächst den Fall der simultanen Definition der Elemente einer Menge
Φ ⊆ F totaler (ggf. durch die Hinzunahme von fail totalisierter) Funktionen durch
Gleichungen: Für alle f ∈ Φ gebe es k > 0 und für alle 1 ≤ i ≤ k eine Gleichung der
Form:
197
f (pi0) | bini = tini where pi1 | bi0
...
= ti0
(1)
pini | bi(ni−1) = ti(ni−1)
mit pi0, . . . , pini ∈ P (X, C), bi0, . . . , bini ∈ TΣ\Φ(X, C)bool , ti0, . . . , tini ∈ TΣ(X, C) und
var(bij ) ∪ var(tij ) ⊆ ∪jr=0var(pir ) für alle 0 ≤ j ≤ ni.
Wir setzen voraus, dass die Basissignatur von Σ die Sorte bool und die üblichen aussagenlogischen Operationen enthält.
Beispiel Listenrevertierung mit Palindromtest
revEq :: Eq a => [a] -> [a] -> ([a],Bool)
revEq (x:s1) (y:s2) = (r++[x],x==y && b) where (r,b) = revEq s1 s2
revEq _ _
= ([],True)
Die folgende Version vermeidet Listenkonkatenationen:
revEqI :: Eq a => [a] -> [a] -> [a] -> ([a],Bool)
revEqI (x:s1) (y:s2) acc = (r,x==y && b)
where (r,b) = revEqI s1 s2 (x:acc)
revEqI _ _ acc
= (acc,True)
198
Beispiel Operationen auf binären Bäumen mit Blatteinträgen
data Btree a = L a | Btree a :# Btree a
foldRepl (t)(x) ersetzt alle Blatteinträge von t durch x:
foldRepl :: (a -> a -> a) -> Btree a -> a -> (a,Btree a)
foldRepl _ (L x) y
= (x,L y)
foldRepl f (t1:#t2) x = (f y z,u1:#u2) where (y,u1) = foldRepl f t1 x
(z,u2) = foldRepl f t2 x
tipsReplRest(t)(s) liefert gleichzeitig die Blatteinträge von t, einen modifizierten Baum,
in dem alle Blatteinträge von t durch die ersten Elemente der Liste s ersetzt sind, und
die restlichen Elemente von s:
tipsReplRest :: Btree a -> [a] -> ([a],Btree a,[a])
tipsReplRest (L x) (y:s) = ([x],L y,s)
tipsReplRest (t1:#t2) s = (ls1++ls2,u1:#u2,s2)
where (ls1,u1,s1) = tipsReplRest t1 s
(ls2,u2,s2) = tipsReplRest t2 s1
199
Die folgende Version vermeidet Listenkonkatenationen:
tipsReplRestI :: Btree a -> [a] -> [a] -> ([a],Btree a,[a])
tipsReplRestI (L x) (y:s) acc = (x:acc,L y,s)
tipsReplRestI (t1:#t2) s acc = (ls2,u1:#u2,s2)
where (ls1,u1,s1) = tipsReplRestI t1 s acc
(ls2,u2,s2) = tipsReplRestI t2 s1 ls1
Ein lazy pattern ist ein Muster, dem das Symbol ∼ vorangestellt ist. Eine λ-Applikation
mit einem lazy pattern kann auch dann zum Rumpf der angewendeten Abstraktion reduziert werden, wenn deren Argument keine Instanz des Musters ist:
(λ ∼p.t)(u) → tσ
(λ ∼p.t)(u) → t[(λp.x)(u)/x | x ∈ var(p)]
falls pσ = u
falls p 6≤ u
Zum Beispiel gilt (λ(x : s).5)[] → fail , aber (λ ∼(x : s).5)[] → 5.
Weitere Beispiele mit lazy patterns finden sich im Abschnitt Unendliche Objekte.
200
Reduktionsregel für f ∈ Φ (siehe Schema (1))
































λp10. (λ ∼p11.
(λ ∼p12.
...
(λ ∼p1n1 .if b1n1 then t1n1 else fail )
(if b1(n1−1) then t1(n1−1) else fail ))
...
(if b11 then t11 else fail ))
(if b10 then t10 else fail )
...
f →



k λpk0. (λ ∼pk1.




(λ ∼pk2.



...





(λ ∼pknk .if bknk then tknk else fail )





(if bk(nk −1) then tk(nk −1) else fail ))


...






(if bk1 then tk1 else fail ))



(if bk0 then tk0 else fail )
201
Termination und Konfluenz
Um sicherzustellen, dass alle mit den Regeln für Φ durchgeführten Reduktionen terminieren, setzen wir eine Termrelation voraus, für die Folgendes gilt:
• ist wohlfundiert, d.h. es gibt keine unendliche Termfolge (ti)i∈N mit ti ti+1 für
alle i ∈ N.
• Für alle 1 ≤ i ≤ k, g ∈ Φ, 0 ≤ j ≤ ni und alle Teilterme g(t) von tij gilt:
g + fi =⇒ pi0 t,
wobei die Relation ⊆ Φ2 wie folgt definiert ist: Sei 1 ≤ i ≤ k und g ∈ Φ.
fi g ⇐⇒def
es gibt 0 ≤ j ≤ ni derart, dass g in tij vorkommt.
Kurz gesagt: Entweder wird f in der Definition von g nicht benutzt oder die Argumente
der rekursiven Aufrufe von g in (1) sind kleiner als p0.
Sei Red die Menge der Regeln für Φ.
Aus und lässt sich eine transitive und wohlfundierte Termrelation konstruieren,
die →Red und die Teiltermrelation ⊃ enthält:
t ⊃ u ⇐⇒def
u ist ein echter Teilterm von t.
202
Die Termination von Reduktionen, in denen die β-Regel:
(λp.t)(pσ) → tσ
verwendet wird, folgt aus der impliziten Voraussetzung, dass alle Σ-Terme in ihrem jeweiligen Kontext eindeutig typisierbar sind. Damit ist insbesondere die Anwendung einer
Funktion auf sich selbst ausgeschlossen, also auch die unendliche Reduktion
(λx.(x(x)))(λx.(x(x))) → (λx.(x(x)))(λx.(x(x))) →
...
Hat umgekehrt der Redex (λp.t)(pσ) der β-Regel einen eindeutigen Typ, dann repräsentiert er eine Funktion n-ter Ordnung. Da deren Bilder Funktionen (n − 1)-ter Ordnung
sind, sind auch alle Funktionen, die durch Teilterme des Reduktes tσ dargestellt werden,
von höchstens (n − 1)-ter Ordnung.
Folglich bleiben →Red und →+
Red wohlfundiert, auch wenn man die Regeln für λ-Applikationen zu Red hinzunimmt.
→+
Red ist nicht nur wohlfundiert, sondern auch konfluent, d.h. für je zwei Reduktionen
t →∗Red u und t →∗Red u0 desselben Terms t gibt es einen Term v mit u →∗Red v und
u0 →∗Red v.
Die Konfluenz von →∗Red folgt aus der Konfluenz der induktiv definierten Relation ⇒Red,
die simultan auf einem Term durchgeführte Reduktionsschritte beschreibt und wie folgt
definiert ist:
203
• Red ⊆ ⇒Red.
• Für alle t ∈ TΣ(X, C), t ⇒Red t.
• Für alle Applikationen t = t0(t1, . . . , tn) und t0 = t00(t01, . . . , t0n),
t0 ⇒Red t00 ∧ . . . ∧ tn ⇒Red t0n impliziert t ⇒Red t0.
• Für alle λ-Abstraktionen t = λp.u und t0 = λp.u0, u ⇒Red u0 impliziert t ⇒Red t0.
• Für alle µ-Abstraktionen t = µp.u und t0 = µp.u0, u ⇒Red u0 impliziert t ⇒Red t0.
Aus →Red ⊆ ⇒Red ⊆ →∗Red folgt →∗Red = ⇒∗Red.
Sei t ⇒∗Red u und t ⇒∗Red u0. Die Existenz eines Terms v mit u ⇒∗Red v und u0 ⇒∗Red v
erhält durch Induktion über die Anzahl der ⇒Red-Schritte, aus denen sich die Reduktionen
t ⇒∗Red u und t ⇒∗Red u0 zusammensetzen.
Ein Term t ∈ BTΣ(X, C), auf den keine Regel von Red anwendbar ist (formal: t →∗Red
t0 ⇒ t = t0), heißt Red-Normalform über X und C. Die S-sortige Menge der RedNormalformen wird mit NFRed (X, C) bezeichnet.
Ein Term t ∈ BTΣ(X, C). Eine Red-Normalform t0 mit t →Red t0 heißt Red-Normalform
von t.
204
Da →+
Red wohlfundiert ist, hat jeder Term von BTΣ (X, C) eine Red-Normalform.
Da →∗Red konfluent ist, stimmen die Normalformen zweier Terme von BTΣ(X, C) mit
gemeinsamer unterer Schranke bzgl. →∗Red überein.
Zusammengenommen folgt aus der Wohlfundiertheit und Konfluenz von →+
Red , dass jeder
Term von genau eine Red-Normalform nf (t) hat, die man auch als Reduktions- oder
operationelle Semantik von t bezeichnet.
Da →∗Red konfluent ist, sind Term t ∈ BTΣ(X, C) genau eine Red-Normalform nf (t), die
man auch als Reduktions- oder operationelle Semantik von t bezeichnet.
Mehr noch: Da Red keine Regeln enthält, deren linke Seiten Σ-Muster sind, sind alle
Σ-Muster von t ∈ BTΣ(X, C) Red-Normalformen. Umgekehrt ist auf jeden Term von
BTΣ(X, C) eine Regel von Red anwendbar. Also gilt:
NFRed (X, C) = P (X, C) ∩ BTΣ(X, C).
Seien BS und BF die Mengen der Sorten bzw. Funktionssymbole von BΣ und B eine
BΣ-Algebra. Für alle s ∈ BS, sei Xs = Bs. Dann bildet NFRed (X, C) die Trägermenge
einer Σ-Algebra A:
• Für alle s ∈ S, As = NFRed (X, C)s.
• Für alle f : s1 . . . sn → s ∈ F und t ∈ NFRed (X, C)si , 1 ≤ i ≤ n,
f A(t1, . . . , tn) =def nf (f (t1, . . . , tn)).
205
Die Auswertung in A eines Terms von BTΣ(X, C) liefert seine Red-Normalform, d.h. für
alle t ∈ BTΣ(X, C) gilt:
id∗(t) = nf (t).
Beweis durch Induktion über size(t).
Fall 1: t ∈ B. Dann gilt id∗(t) = id(t) = t = nf (t).
Fall 2: t = f (t1, . . . , tn) für ein f ∈ F . Dann gilt nach Induktionsvoraussetzung:
id∗(t) = f A(id∗(t1), . . . , id∗(tn)) = f A(nf (t1), . . . , nf (tn)) = nf (f (nf (t1), . . . , nf (tn)))
= nf (f (t1, . . . , tn)) = nf (t).
o
Partiell-rekursive Funktionen
Es fehlen noch Reduktionsregeln für µ-Abstraktionen. Diese beschreiben partiell-rekursive
Funktionen, das sind partielle Funktionen, die berechenbar sind, obwohl ihr Definitionsbereich möglicherweise nicht entscheidbar ist. Das zeigt sich bei ihrer Auswertung durch
Termreduktion darin, dass manche Reduktionen einiger Aufrufe solcher Funktionen nicht
terminieren. Schuld daran ist gerade die – unvermeidliche – Regel zur Reduktion von µAbstraktionen (deren Korrektheit sich direkt aus der Interpretation von µp.t als kleinste
Lösung der Gleichung p = t ergibt):
µp.t → t[(λp.x)(µp.t)/x | x ∈ var(p)]
206
Expansionsregel
Man sieht sofort, dass diese Regel unendlich oft hintereinander angewendet werden kann.
Eine Reduktionsstrategie legt für jeden Term t fest, welcher Teilterm von t durch welche (anwendbare) Regel in einem Reduktionsschritt ersetzt wird. Da Konfluenz eindeutige
Normalformen impliziert, unterscheiden sich Reduktionsstrategien bezüglich der jeweils
erzielten Ergebnisse nur in der Anzahl der Terme, die sie zu Normalformen reduzieren.
Eine Reduktionsstrategie heißt vollständig, wenn sie jeden Term, der eine Normalform
hat, dort auch hinführt. Da nur die Anwendung der Expansionsregel unendliche Reduktionen erzeugt, hängt die Vollständigkeit der Strategie i.w. davon ab, wann und wo sie
die Expansionsregel anwendet.
Sind alle Reduktionen eines Terms f (t) unendlich, dann ist f an der Stelle t nicht definiert.
Andererseits muss f in einer Σ-Algebra A als totale Funktion interpretiert werden. Dazu
werden die Trägermengen von A zu CPOs erweitert (siehe CPOs Fixpunkte). Einem
“undefinierten” Term f (t) des Typs e wird das kleinste – durch ⊥e bezeichnete – Element
des CPOs Ae zugeordnet.
Für Sorten e ∈ S wird die Existenz von ⊥e ∈ Ae vorausgesetzt und Ae als flacher CPO
angenommen, d.h. für alle a, b ∈ Ae,
a ≤ b ⇐⇒def
a = ⊥e ∨ a = b.
207
Die Halbordnungen auf As, s ∈ S, werden wie folgt auf Produkte und Funktionenräume
fortgesetzt: Für alle e1, . . . , en, e, e0 ∈ types(S), a1, b1 ∈ As1 , . . . , an, bn ∈ Asn und f, g :
Ae → Ae0 ,
(a1, . . . , an) ≤ (b1, . . . , bn) ⇐⇒def ∀ 1 ≤ i ≤ n : ai ≤ bi,
f ≤ g ⇐⇒def ∀ a ∈ A : f (a) ≤ g(a).
********
Sind alle in t1, . . . , tn auftretenden Funktionen in dieser oder anderer Weise zu stetigen
Funktionen erweitert worden, dann ist auch
Φ : A1 × . . . × An → A1 × . . . × An
Φ(a1, . . . , an) =def t[ai/xi | 1 ≤ i ≤ n]A1×...×An
stetig und wir können den Fixpunktsatz von Kleene anwenden, nach dem
ti∈NΦi(⊥) die kleinste Lösung von (x1, . . . , xn) = t
in A1 × . . . × An ist. Daraus ergibt sich die Interpretation einer µ-Abstraktion:
(µx1 . . . xn.t)A1×...×An =def ti∈N Φi(⊥).
Betrachten wir nun das Schema der nicht-rekursiven Definition einer Funktion f , deren
lokale Definitionen in einer Gleichung der Form (1) zusammengefasst sind.
208
Seien x1, . . . , xm, z1, . . . , zn paarweise verschiedene Variablen und e, t beliebige Terme, in
denen f nicht vorkommt und deren freie Variablen zur Menge {x1, . . . , xm, z1, . . . , zn}
gehören.
f (x1, . . . , xm) = e where (z1, . . . , zn) = t
Aus (7) und (8) ergibt sich die folgende Reduktionsregel zur Auswertung von f :
f (x1, . . . , xm) → e[πi$µz1 . . . zn.t/zi | 1 ≤ i ≤ n]
209
δ-Regel
Beispiele
replace :: (a -> a -> a) -> Btree a -> Btree a
replace f t = u where (x,u) = foldRepl f t x
replace min ((L 3:#(L 22:#L 4)):#(L 2:#L 11))
===> ((2#(2#2))#(2#2))
replace (+) ((L 3:#(L 22:#L 4)):#(L 2:#L 11))
===> ((42#(42#42))#(42#42))
pal, palI :: Eq a => [a] -> Bool
pal s = b where (r,b) = revEq s r
palI s = b where (r,b) = revEqI s r []
sort, sortI :: Ord a => Btree a -> Btree a
sort t = u where (ls,u,_) = tipsReplRest t (sort ls)
sort ((L 3:#(L 22:#L 4)):#((L 3:#(L 22:#L 4)):#(L 2:#L 11)))
===> ((2#(3#3))#((4#(4#11))#(22#22)))
sortI t = u where (ls,u,_) = tipsReplRestI t (sort ls) []
210
Schema 3 einer Funktionsdefinition
Bevor wir auf vollständige Strategien eingehen, definieren wir induktiv das allgemeine
Schema der rekursiven Definition DS(F, globals) einer Menge F funktionaler oder nichtfunktionaler Objekte mit lokalen Definitionen, die selbst diesem Schema genügen.
globals bezeichnet die Menge der globalen (funktionalen oder nichtfunktionalen) Objekte
die in DS(F, globals) vorkommen.
• Sei eqs eine Definition von F , die aus Gleichungen der Form
f (p) = e
mit f ∈ F besteht, wobei p ein Pattern ist und alle in e verwendeten Funktionen
Standardfunktionen sind oder zur Menge globals ∪ F gehören.
Dann gilt eqs ∈ DS(F, globals).
(a)
• Sei δ eine Definition von F , die aus Gleichungen der Form
f (p) = e where eqs
(eq)
mit f ∈ F besteht, wobei p ein Pattern ist und es Mengen Geq und globalseq von
Funktionen gibt mit eqs ∈ DS(Geq , globalseq ∪ F ).
Dann gilt eqs ∈ DS(F, ∪eq∈δ globalseq ).
(b)
211
Sei F = {f1, . . . , fk } und eqs ∈ DS(F, ∅).
Im Fall (a) kann jedes f ∈ F durch eine einzige µ-Abstraktion dargestellt werden: Für
alle 1 ≤ i ≤ k seien
fi(pi1) = ei1, . . . , fi(pini ) = eini
die Gleichungen für fi innerhalb von eqs. Mit
µ(eqs) =def µf1 . . . fk .( λp11.e11k . . . kp1n1 .e1n1 ,
...
λpk1.ek1k . . . kpknk .eknk )
liefert die Gleichung (f1, . . . , fk ) = µ(eqs) eine zu eqs äquivalente Definition von F .
212
Im Fall (b) seien für alle 1 ≤ i ≤ k
fi(pi1) = ei1 where eqsi1,
...
fi(pini ) = eini where eqsini
die Gleichungen für fi innerhalb von eqs. Für alle 1 ≤ i ≤ k und 1 ≤ j ≤ ni gibt
es Mengen Gij und globalsij von Funktionen mit eqsij ∈ DS(Gij , globalsij ∪ F ). Die
Substitution σij ersetze jede Funktion g ∈ Gij in eij durch ihre äquivalente µ-Abstraktion
πg (µ(eqsij )). Mit
µ(eqs) =def µf1 . . . fk .( λp11.σ11(e11)k . . . kp1n1 .σ1n1 (e1n1 ),
...
λpk1.σk1(ek1)k . . . kpknk .σknk (eknk ))
liefert die Gleichung (f1, . . . , fk ) = µ(eqs) eine zu eqs äquivalente Definition von F . Aus
ihr ergibt sich die folgende Reduktionsregel zur Auswertung von fi, 1 ≤ i ≤ k:
fi
→ πi$µ(eqs)
δ-Regel
213
Die lazy-evaluation-Strategie
Nach einem auf getypte λ- und µ-Abstraktionen übertragenen Resultat von Jean Vuillemin ist die folgende parallel-outermost, call-by-need oder lazy evaluation (verzögerte
Auswertung) genannte Reduktionsstrategie vollständig:
• β- und δ-Regeln werden stets vor der Expansionsregel angewendet.
(A)
• Die Expansionsregel wird immer parallel auf alle bzgl. der Teiltermordnung maximalen
µ-Abstraktionen angewendet.
(B)
Der Beweis basiert auf der Beobachtung, dass die Konstruktion der kleinsten Lösung
von (x1, . . . , xn) = t nach dem Fixpunktsatz von Kleene selbst eine Reduktionsstrategie
wiederspiegelt, die full-substitution genannt wird. Diese wendet die Expansionsregel im
Unterschied zu (B) parallel auf alle µ-Abstraktionen an. Da schon die parallele Expansion
aller maximalen µ-Abstraktionen viel Platz verbraucht, wird sie in der Regel nicht durchgeführt. Stattdessen wird nur die erste auf einem strikten Pfad gelegene µ-Abstraktion
expandiert. Enthält dieser eine kommutative Operation, dann gibt es möglicherweise mehrere solche Pfade, so dass die Strategie unvollständig wird.
214
Ein Pfad (der Baumdarstellung von) t ist strikt, wenn jeder Pfadknoten die Wurzel eines
Teilterms von t ist, der zur Herleitung einer Normalform von t reduziert werden muss,
m.a.W.: jeder Pfadknoten ist ein striktes Argument der Funktion im jeweiligen Vorgängerknoten (s.o.).
Sei RS eine Reduktionsstrategie mit (A). Da β- und δ-Regeln niemals unendlich oft hintereinander angewendet werden können, lässt sich jede gemäß RS durchgeführte Termreduktion eindeutig als Folge
t0 →RS t1 →RS t2 →RS . . .
von Termen repräsentieren derart, dass für alle i ∈ N ti+1 durch parallele Anwendungen
der Expansionsregel aus ti hervorgeht. Wertet man alle Terme in einem CPO aus, der die
Funktionssymbole in den Termen durch monotone Funktionen interpretiert, dann wird
aus der obigen Termreduktion eine Kette von Werten in A:
A
A
tA
0 ≤ t1 ≤ t2 ≤ . . .
Der von RS berechnete Wert von t0 in A wird dann definiert durch:
A
tA
0,RS =def ti∈N ti .
215
Diese Definition kann auf Funktionen höherer Ordnung erweitert werden: Sei A ein und
t0 ein Term eines Typs F T = A1 \ {⊥} → . . . → Ak \ {⊥} → A. Dann nennen wir die
für alle 1 ≤ i ≤ k und ai ∈ Ai durch
A
tA
RS (a1 ) . . . (ak ) =def (t(a1 ) . . . (ak ))RS
definierte Funktion tA
RS : F T den von RS berechneten Wert von t in A.
Offenbar stimmt der von der full-substitution-Strategie (F S) berechnete Wert von xi,
1 ≤ i ≤ n, mit der (i-ten Projektion der) kleinsten Lösung von (1) in A überein:
j
A
xA
i,F S = πi (tj∈N Φ (⊥)) = πi (µx1 . . . xn .t) .
Das impliziert u.a., dass die kleinste Lösung von (1) niemals kleiner als der von RS
berechnete Wert von (x1, . . . , xn) ist:
A
A
A
A
(xA
1,RS , . . . , xn,RS ) ≤ (x1,F S , . . . , xn,F S ) = (µx1 . . . xn .t) .
RS ist also genau dann vollständig, wenn der von RS berechnete Wert von (x1, . . . , xn)
mit der kleinsten Lösung von (x1, . . . , xn) = t übereinstimmt.
Aus der o.g. Voraussetzung, dass die Terme einer Reduktion in einem CPO mit flacher
Halbordnung interpretiert werden, folgt:
Eine Reduktion t0 →RS t1 →RS t2 →RS . . . terminiert ⇐⇒ tA
0,RS 6= ⊥.
216
Zunächst einmal terminiert die Reduktion genau dann, wenn es k ∈ N gibt, so dass tk
keine der Variablen von x1, . . . , xn enthält. Wie t, so ist dann auch tk bottomfrei. Also
gilt tA
k 6= ⊥ und damit
A(⊥)
⊥=
6 tA
k = tk
A(⊥)
≤ ti∈Nti
= tA
0,RS .
Enthält für alle i ∈ N ti Variablen von {x1, . . . , xn}, dann gilt für alle i ∈ N tA
i (⊥) = ⊥
und damit
A(⊥)
tA
= ⊥.
0,RS = ti∈N ti
A(⊥)
Ein i ∈ N mit ti
6= ⊥ würde nämlich zu einem Widerspruch führen: Sei j das kleinste
A(⊥)
i mit ti
6= ⊥. Es gäbe eine aus Funktionen von ti gebildete monotone Funktion f
sowie a1, . . . , am ∈ A mit
A(⊥)
f (a1, . . . , am, ⊥, . . . , ⊥) = tj
6= ⊥.
Aus der Monotonie von f und der Flachheit der Halbordnung des CPOs, in dem tj
interpretiert wird, würde folgen, dass es k < j und b1, . . . , br ∈ A gibt mit
A(⊥)
tk
= f (a1, . . . , am, b1, . . . , br ) = f (a1, . . . , am, ⊥, . . . , ⊥) 6= ⊥
A(⊥)
im Widerspruch dazu, dass j das kleinste i mit ti
6= ⊥ ist. (Ein ähnliches Argument
wird verwendet, um zu zeigen, dass parallel-outermost vollständig ist; siehe Zohar Manna,
Mathematical Theory of Computation, Theorem 5-4.)
217
Eine Reduktionsstrategie bevorzugt Anwendungen von δ-Regeln, um danach µ-Abstraktionen eliminieren zu können. Dazu müssen vorher Expansionsschritte die Redexe dieser
Regeln erzeugen. Tun sie das nicht, dann kann die Reduktion nicht terminieren, da jedes Expansionsredukt einen neuen Expansionsredex enthält. Neben diesem sollte es also
auch einen neuen δ- (oder β-) Redex enthalten. Diese Bedingung ist z.B. in der obigen
Definition von pal verletzt, sofern dort die obige Definition von revEq verwendet wird:
pal :: Eq a => [a] -> Bool
pal s = b where (r,b) = revEq s r
revEq :: Eq a => [a] -> [a] -> ([a],Bool)
revEq (x:s1) (y:s2) = (r++[x],x==y && b) where (r,b) = revEq s1 s2
revEq _ _
= ([],True)
Die Definition von pal liefert die δ-Regel
pal(s) → π2$µ r b.revEq(s, r).
218
(1)
Parallel-outermost-Reduktionen von pal terminieren nicht, weil die Expansionsschritte
keine Redexe für die obige Definition von revEq liefern:
(1)
pal[1, 2, 1] → π2$µ r b.revEq([1, 2, 1], r)
Expansion
→
Expansion
→
π2$revEq([1, 2, 1], π1$µ r b.revEq([1, 2, 1], r))
π2$revEq([1, 2, 1], π1$revEq([1, 2, 1], π1$µ r b.revEq([1, 2, 1], r)))
219
Auswertung durch Graphreduktion
Manche Compiler funktionaler Sprachen implementieren µ-Abstraktionen durch Graphen: µx1, . . . , xn.t wird zunächst als Baum dargestellt. Dann werden alle identischen
Teilbäume von t zu jeweils einem verschmolzen (collapsing). Schließlich wird für alle
1 ≤ i ≤ n die Markierung xi in πi umbenannt und von dem mit πi markierten Knoten
eine Kante zur Wurzel von t gezogen.
Expansionsschritte verändern den Graphen nicht, sondern die Position eines Zeigers • auf
die Wurzel des nächsten Redex. Jedes Fortschreiten des Zeigers auf einer Rückwärtskante
implementiert einen Expansionsschritt. Die obige Reduktion von pal[1, 2, 1] entspricht
folgender Graphtransformation:
(1)
•pal[1, 2, 1] → π2$• ↓ revEq([1, 2, 1], π1 ↑)
• moves up
→
• moves up
→
• moves down
→
π2$• ↓ revEq([1, 2, 1], π1 ↑)
• moves down
π2$• ↓ revEq([1, 2, 1], π1 ↑)
• moves down
→
→
π2$ ↓ revEq([1, 2, 1], • π1 ↑)
π2$ ↓ revEq([1, 2, 1], • π1 ↑)
...
Die Pfeile ↑ und ↓ zeigen auf die Quelle bzw. das Ziel der einen Rückkante in diesem Beispiel.
220
Wie muss die Definition von revEq repariert werden, damit die Auswertung von pal[1, 2, 1]
terminiert? Trifft der Zeiger • auf den Ausdruck revEq([1, 2, 1], π1 ↑), dann muss auf
diesen wenigstens ein Reduktionsschritt anwendbar sein, damit er modifiziert und damit
der Zyklus, den der Zeiger durchläuft, durchbrochen wird. Man erreicht das mit der
folgenden Definition von revEq, deren Anwendbarkeit im Gegensatz zur obigen Definition
kein pattern matching des zweiten Arguments verlangt:
revEq :: Eq a => [a] -> [a] -> ([a],Bool)
revEq (x:s1) s = (r++[x],x==y && b) where y:s2 = s
(r,b) = revEq s1 s2
revEq _ _
= ([],True)
Diese Definition von revEq folgt Schema 1, so dass bei ihrer Überführung in Reduktionsregeln die lokalen Definitionen wie folgt entfernt werden können:
revEq(x : s1, s) → λ ∼y : s2.(λ ∼(r, b).(r ++[x], x = y&&b)$revEq(s1, s2))$s (2)
revEq([], s)
→ ([], True)
(3)
221
Hiermit erhalten wir eine terminierende Reduktion von pal[1, 1], die als Graphtransformation so aussieht: Die Pfeile ↑, ↓, -, &, % und . zeigen auf die Quelle bzw. das Ziel
von drei verschiedenen Kanten. Redexe sind rot, die zugehörigen Redukte grün gefärbt.
•pal[1, 1]
(1)
→
π2$• ↓ revEq([1, 1], π1 ↑)
(2)
→
π2$• ↓ (λ ∼y : s2.(λ ∼(r, b).(r ++[1], 1 = y&&b)$revEq([1], s2))$π1 ↑
β−Regel
π2$• ↓ λ ∼(r, b).(r ++[1], 1 = head$π1 ↑&&b)$revEq([1], tail$π1 ↑)
split term
π2$• ↓ λ ∼(r, b).(r ++[1], 1 = head$π1 ↑ &&b)$ & revEq([1], tail$π1 ↑)
β−Regel
π2$• ↓ (π1 - ++ [1], 1 = head$π1 ↑ &&π2 -)
& revEq([1], tail$π1 ↑)
• moves down
π2$ ↓ (π1 - ++ [1], 1 = head$π1 ↑ &&π2 -)
• & revEq([1], tail$π1 ↑)
→
→
→
→
(2)
→
π2$ ↓ (π1 - ++ [1], 1 = head$π1 ↑ &&π2 -)
• & (λ ∼y : s2.(λ ∼(r, b).(r ++[1], 1 = y&&b)$revEq([], s2))$tail$π1 ↑
β−Regel
π2$ ↓ (π1 - ++ [1], 1 = head$π1 ↑ &&π2 -)
• & λ ∼(r, b).(r ++[1], 1 = head$tail$π1 ↑&&b)$revEq([], tail$tail$π1 ↑)
→
222
split term
π2$ ↓ (π1 - ++ [1], 1 = head$π1 ↑ &&π2 -)
• & λ ∼(r, b).(r ++[1], 1 = head$tail$π1 ↑ &&b)$ %
. revEq([], tail$tail$π1 ↑)
β−Regel
π2$ ↓ (π1 - ++ [1], 1 = head$π1 ↑ &&π2 -)
• & (π1 % ++ [1], 1 = head$tail$π1 ↑ &&π2 %)
. revEq([], tail$tail$π1 ↑)
• moves down
π2$ ↓ (π1 - ++ [1], 1 = head$π1 ↑ &&π2 -)
& (π1 % ++ [1], 1 = head$tail$π1 ↑ &&π2 %)
• . revEq([], tail$tail$π1 ↑)
→
→
→
(3)
→
π2$ ↓ (π1 - ++ [1], 1 = head$π1 ↑ &&π2 -)
& (•π1 % ++[1], 1 = head$tail$π1 ↑ && • π2 %)
. ([], True)
δ−Regeln
π2$ ↓ (π1 - ++ [1], 1 = head$π1 ↑ &&π2 -)
& (•[] ++[1], •1 = head$tail$π1 ↑ &&True)
δ−Regeln
π2$ ↓ (•π1 - ++[1], 1 = head$π1 ↑ && • π2 -)
& ([1], 1 = head$tail$π1 ↑)
δ−Regeln
π2$ ↓ (•[1] ++[1], 1 = head$ • π1 ↑&&1 = head$tail$ • π1 ↑)
δ−Regeln
π2$ ↓ ([1, 1], 1 = head$ • π1 ↑&&1 = head$tail$ • π1 ↑)
→
→
→
→
223
δ−Regel
•π2$([1, 1], 1 = head$[1, 1]&&1 = head$tail$[1, 1])
δ−Regel
1 = •head$[1, 1]&&1 = head$ • tail$[1, 1]
β−Regeln
•1 = 1&&1 = head$[1]
δ−Regel
•True&&1 = head$[1]
δ−Regel
1 = •head$[1]
β−Regel
•1 = 1
→
→
→
→
→
→
δ−Regel
→
True
In Expander2 sieht die aus den obigen Regeln (1)-(3) bestehende Definition von pal und
revEq folgendermaßen aus:
pal2(s) == get1(mu r b.revEq2(s)(r)) &
revEq2[]
== fun(~[],([],bool(True))) &
revEq2(x:s1) == fun(~(y:s2),fun((r,b),(r++[x],bool(x=y & Bool(b))))
(revEq2(s1)(s2))) &
Die darauf basierende Reduktion von pal2[1,1] enthält zwar z.T. größere Terme als die
obige Graphreduktion von pal[1,1]. Dafür entfällt aber die dort erforderliche Zeigerverwaltung:
224
pal2[1,1]
get1(mu r b.(revEq2[1,1](r)))
get1(mu r b.(fun(~(y:s2),
fun((r,b),(r++[1],bool(1 = y & Bool(b))))
(revEq2[1](s2)))
(r)))
get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b))))
(revEq2[1](tail(r)))))
get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b))))
(fun(~(y:s2),
fun((r,b),(r++[1],bool(1 = y & Bool(b))))
(revEq2[](s2)))
(tail(r)))))
get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b))))
(fun((r0,b),(r0++[1],bool(1 = head(tail(r)) & Bool(b))))
(revEq2[](tail(tail(r)))))))
225
get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b))))
(fun((r0,b),(r0++[1],bool(1 = head(tail(r)) & Bool(b))))
(fun(~[],([],bool(True)))
(tail(tail(r)))))))
get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b))))
(fun((r0,b),(r0++[1],bool(1 = head(tail(r)) & Bool(b))))
([],bool(True)))))
get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b))))
([]++[1],bool(1 = head(tail(r)) & Bool(bool(True))))))
get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b))))
([1],bool(1 = head(tail(r))))))
get1(mu r b.(([1]++[1],bool(1 = head(r) & Bool(bool(1 = head(tail(r))))))))
get1(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r)))))
226
bool(1 = head(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r)))))) &
1 = head(tail(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r))))))))
bool(1 = head[1,1] &
1 = head(tail(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r))))))))
bool(1 = 1 &
1 = head(tail(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r))))))))
bool(1 = head(tail(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r))))))))
bool(1 = head(tail[1,1]))
bool(1 = head[1])
bool(1 = 1)
bool(True)
Number of steps: 19
227
Unendliche Objekte*
Auch im Fall, dass einige Datenbereiche aus unendlichen Objekten bestehen (wie im
Client/Server-Beispiel (siehe Das relationale Berechnungsmodell), können die obigen Ergebnisse verwendet werden. Allerdings macht es i.d.R. keinen Sinn, solche Datenbereiche
in der oben beschriebenen Weise zu einem CPO zu vervollständigen.
Nimmt man z.B. die Gleichung ones = 1:ones, deren kleinste Lösung die unendliche
Liste von Einsen repräsentieren soll, dann kann diese Lösung nicht in einem flachen CPO
liegen. Der Konstruktor (:) müsste als monotone Funktion interpretiert werden, was
⊥ ≤ 1 : ⊥ ≤ 1 : (1 : ⊥)
impliziert und daher in einem flachen CPO 1 : ⊥ = ⊥ oder 1 : ⊥ = 1 : (1 : ⊥) zur Folge
hätte. Beide Gleichheiten sind sicher nicht gewollt.
Die Menge endlicher und unendlicher Listen wird also nicht durch das Hinzufügen von ⊥
zum CPO. Stattdessen gilt hier, kurz gesagt, L ≤ L0 genau dann, wenn L ein Präfix von
L0 ist. Eine unendliche Liste wird als Supremum ihrer endlichen Präfixe aufgefasst.
228
Bevor wir eine allgemeine Struktur zur Modellierung von Mengen unendlicher Objekte behandeln, verweisen wir auf die Expander2-Version des Client/Server-Beispiels (siehe Das
relationale Berechnungsmodell), mit deren Hilfe die oben angekündigte Termreduktion
take 3 requests –> ... –> [0,2,6]
durchgeführt werden kann:
CSR = µ client server requests.(λa.λ ∼(b:s).(a:client(mkRequest $ b)(s)),
λ ∼(a:s).(mkResponse(a):server(s)),
client(0) $ server $ requests) &
mkRequest = (*2) & mkResponse = (+1)
CSR fasst die Definitionen von client, server und requests zu einer µ-Abstraktion
zusammen.
Der Term take(3) requests = take(3) $ get2 CSR wird in 66 Reduktionsschritten
zu [0,2,6] reduziert (siehe Algebraic Model Checking and more, §25).
229
Die Semantik unendlicher Listen als Suprema endlicher Approximationen kann auf unendliche Objekte eines beliebigen (konstruktorbasierten) Datentyps fortgesetzt werden.
Auch diese Objekte lassen sich partiell ordnen, wenn man sie als partielle Funktionen
definiert:
Sei Σ = (S, F ) eine konstruktive Signatur mit Basismengen BS (siehe Das Tai Chi ...).
Die (BS ∪ S)-sortige Menge CTΣ der Σ-Bäume besteht aus allen partiellen Funktionen
t : N∗ → F ∪ (∪BS)
derart, dass gilt:
• für alle B ∈ BS, CTΣ,B = B,
• für alle s ∈ S, t ∈ CTΣ,s gdw ran(t()) = s und für alle w ∈ N∗,
dom(t(w)) = e1 × · · · × en → s ⇒ ∀ 0 ≤ i < n : (t(wi) ∈ ei+1 ∨ ran(t(wi)) = ei+1).
230
Wir setzen voraus, dass es für alle s ∈ S eine Konstante ⊥s : → s in F gibt und
definieren damit eine S-sortige Halbordnung auf CTΣ: Für alle s ∈ S und t, u ∈ CTΣ,s,
t ≤ u ⇐⇒def ∀w ∈ N∗ : t(w) 6= ⊥ ⇒ t(w) = u(w).
Bezüglich dieser Halbordnung ist der Σ-Baum Ωs mit
(
⊥s
falls w = Ωs(w) =def
undefiniert sonst
das kleinste Element von CTΣ,s. Außerdem hat jede Kette t1 ≤ t2 ≤ t3 ≤ . . . von
Σ-Bäumen ein Supremum: Für alle w ∈ N∗,
(
ti(w) falls ti(w) 6= ⊥ für ein i ∈ N,
(ti∈Nti)(w) =def
⊥
sonst.
Zum Spezialfall unendlicher Listen, siehe Bird, Introduction to Functional Programming
using Haskell, Kapitel 9.
231
Verifikation*
Die folgenden drei Methoden dienen dem Beweis von Eigenschaften der kleinsten bzw.
größten Lösung einer Gleichung der Form
(x1, . . . , xn) = t.
(1)
232
Fixpunktinduktion
ist anwendbar, wenn es einen CPO gibt, in dem sich (1) interpretieren lässt und die
Funktionen von t monoton bzw. ω-stetig sind. Die Korrektheit der Fixpunktinduktion
folgt im ersten Fall aus dem Fixpunktsatz von Knaster und Tarski, im zweiten aus dem
Fixpunktsatz von Kleene.
Fixpunktinduktion ist durch folgende Beweisregel gegeben:
µx1 . . . xn.t ≤ u
⇑
t[πi(u)/xi | 1 ≤ i ≤ n] ≤ u
(2)
Der Pfeil deutet die Schlußrichtung in einem Beweis an, in dem die Regel angewendet
wird. Hier impliziert demnach als der Sukzedent der Regel ihren Antezedenten.
Der Fixpunktsatz von Knaster und Tarski besagt, dass die kleinste Lösung von (1) dem
kleinsten t-abgeschlossenen Objekt entspricht. Ein Objekt heißt t-abgeschlossen, wenn
es die Konklusion von (2) erfüllt.
Zur Anwendung der Fixpunktinduktion muss das Beweisziel die Form der Prämisse von
(2) haben.
233
Berechnungsinduktion
ist anwendbar, wenn es einen CPO gibt, in dem sich (1) interpretieren lässt und die
Funktionen von t ω-stetig sind. Die Korrektheit der Berechnungsinduktion folgt aus dem
Fixpunktsatz von Kleene und erfordert die Zulässigkeit des Beweisziels ϕ, d.h. für alle
aufsteigenden Ketten a0 ≤ a1 ≤ a2 ≤ . . . muss aus der Gültigkeit von ϕ(ai) für alle i ∈ N
die Gültigkeit von ϕ(ti∈Nai) folgen. Beispielsweise sind Konjunktionen von Gleichungen
oder Ungleichungen zulässig.
Berechnungsinduktion ist durch folgende Beweisregel gegeben:
ϕ(µx1 . . . xn.t)
⇑
ϕ(⊥) ∧ ∀x1, . . . , xn : (ϕ(x1, . . . , xn) ⇒ ϕ(t))
234
(3)
Coinduktion
ist anwendbar, wenn sich Gleichung (1) in einer finalen Coalgebra lösen lässt (siehe Das
Tai Chi ...). Die Trägermengen dieser Coalgebra stimmen mit denen von CTΣ überein
(siehe Unendliche Objekte). Ihre Destruktoren sind
• für alle s ∈ RS eine Funktion
a
ds : s →
s1 × · · · × sn ,
c:s1 ×···×sn →s∈C
deren Interpretation in CTΣ einen Σ-Baum t in seine Wurzel und seine Unterbäume
zerlegt:
Σ (t) =
dCT
def (t() : s1 × · · · × sn → s, (λw.t(0w), . . . , λw.t((n − 1)w))),
s
• für alle n > 1, s1, . . . , sn ∈ S und 1 ≤ i ≤ n, eine Funktion πi : s1 × · · · × sn → si,
deren Interpretation in CTΣ ein Baumtupel auf seine i-te Komponenete projiziert:
CTΣ
πi
(t1, . . . , tn) = ti.
Z.B. ist CTΣ im Fall der Listensignatur Σ = ({entry}, {list}, E ∪ {[], (:)}) isomorph zur
Menge der endlichen und unendlichen Wörter über E.
235
Aus der Finalität von CTΣ folgt u.a., dass für alle s ∈ S zwei Σ-Bäume t und u der Sorte
s genau dann gleich sind, wenn sie bzgl. der oben definierten Destruktoren verhaltensäquivalent sind. D.h. (t, u) liegt in der größten binären Relation ∼ von CTΣ, welche die
Implikation
x ∼ y ⇒ ds(x) ∼ ds(y)
(4)
erfüllt.
Ein coinduktiver Beweis von t ∼ u besteht darin, eine binäre Relation ≈ zu finden,
die das Paar (t, u) enthält und (4) erfüllt. Man geht aus von ≈ = {(t, u)}, wendet (4)
von links nach rechts auf die Paare von ≈ an und erhält damit Instanzen der rechten
Seite von (4), die zu ≈ hinzugenommen werden. Auf die neuen Paare von ≈ wird wieder
(4) angewendet, usw. Das Verfahren terminiert, sobald alle durch Anwendungen von (4)
auf ≈ erzeugten Paare bereits im Äquivalenzabschluss von ≈ liegen. Dann gilt (4) für ≈
und wir schließen t ∼ u daraus, dass ∼ die größte Relation ist, die (4) erfüllt.
236
Dieses Verfahren basiert auf der zur Fixpunktinduktion dualen Regel:
u ≤ νx1 . . . xn.t
⇑
u ≤ t[πi(u)/xi | 1 ≤ i ≤ n]
(5)
(5) ist anwendbar, wenn es einen ω-covollständigen poset, kurz: coCPO, gibt, in dem
sich (1) interpretieren lässt und die Funktionen von t monoton bzw. ω-costetig sind. Die
Korrektheit der Coinduktion folgt im ersten Fall aus dem Fixpunktsatz von Knaster und
Tarski, im zweiten aus dem Fixpunktsatz von Kleene.
Die im oben skizzierten coinduktiven Beweis verwendete Variante von (5) basiert auf
dem Potenzmengenverband der durch prädikatenlogische Formeln gegebenen Relationen auf einer – ggf. mehrsortigen – Menge A. Die Halbordnung ≤ entspricht dort
der Mengeninklusion, das kleinste Element ist die leere Menge, das größte die Menge A.
Damit wird (5) zur Beweisregel für Implikationen:
Relationale Coinduktion
ψ ⇒ (νx1 . . . xn.ϕ)(~x)
⇑
∀~x (ψ ⇒ ϕ[πi(λ~x.ψ)/xi | 1 ≤ i ≤ n](~x))
237
(6)
ϕ und ψ sind hier n-Tupel prädikatenlogischer Formeln, x1, . . . , xn Prädikatvariablen und
~x ein Tupel von Individuenvariablen. νx1 . . . xn.ϕ wird interpretiert als das n-Tupel der
größten Relationen, das die logische Äquivalenz
hx1, . . . , xni(~x) ⇐⇒ ϕ(~x)
(7)
erfüllt, die der Gleichung (1) entspricht.
Substitution, Implikation und andere aussagenlogische Operatoren werden komponentenweise auf Formeltupel fortgesetzt:
hϕ1, . . . , ϕni(~x)
=def (ϕ1(~x), . . . , ϕn(~x)),
(ϕ1, . . . , ϕn) ⇒ (ψ1, . . . , ψn) =def (ϕ1 ⇒ ψ1) ∧ · · · ∧ (ϕn ⇒ ψn)
...
Die oben definierte s-Verhaltensäquivalenz ∼s auf CTΣ,s ist durch die Formel
ν ≈s .λ(x, y).ds(x) ≈ran(ds) ds(y)
als größte Lösung der Instanz
x ≈s y ⇐⇒ ds(x) ≈ran(ds) ds(y)
(8)
von (7) definiert. Die entsprechende Instanz der Coinduktionsregel (6) lautet demnach
wie folgt:
238
x ≈s y ⇒ x ∼s y
⇑
∀ x, y : (x ≈s y ⇒ ds(x) ≈ran(ds) ds(y))
(9)
M.a.W.: Alle Paare von ≈s sind s-äquivalent, wenn ≈s den Sukzedenten von (9) erfüllt,
welcher der Bedingung entspricht.
Da die größte Lösung von (8) eine Äquivalenzrelation ist, also mit ihrem Äquivalenzabschluss übereinstimmt, bleibt Regel (9) korrekt, wenn ihr Sukzedent zu
∀(x, y) (x ≈s y ⇒ ds(x) ≈eq
ran(ds ) ds (y))
(10)
abgeschwächt wird. Deshalb können wir die oben beschriebene schrittweise Konstruktion
von ≈s bereits dann beenden, wenn sich der Äquivalenzabschluss von ≈s nicht mehr
verändert.
Alle wichtigen Induktions- und Coinduktionsregeln sowie zahlreiche Beispiele ihrer Anwendung finden sich in Algebraic Model Checking and more sowie Expander2: Program
Verification between Interaction and Automation.
239
Zum Schluss noch die beiden zur relationalen Coinduktion bzw. Berechnungsinduktion
dualen Regeln:
Relationale Fixpunktinduktion
(µx1 . . . xn.ϕ)(~x) ⇒ ψ
⇑
∀~x (ϕ[πi(λ~x.ψ)/xi | 1 ≤ i ≤ n](~x) ⇒ ψ)
(11)
Mit dieser Regel beweist man u.a. Eigenschaften einer Funktion f , die durch ein rekursives, ggf. bedingtes, Gleichungssystem, also z.B. ein Haskell-Programm, definiert ist. ϕ
bezeichnet dann die Ein/Ausgabe-Relation von f , hat also die Form f (x) = y, während ψ den erwarteten – nicht notwendig funktionalen – Zusammenhang zwischen den
Argumenten und Werten von f beschreibt.
240
Berechnungscoinduktion
ist anwendbar, wenn es einen coCPO gibt, in dem sich (1) interpretieren lässt und die
Funktionen von t costetig sind. Die Korrektheit der Berechnungscoinduktion folgt aus
dem Fixpunktsatz von Kleene und erfordert die Zulässigkeit des Beweisziels ϕ, d.h. für
alle absteigenden Ketten a0 ≥ a1 ≥ a2 ≥ . . . muss aus der Gültigkeit von ϕ(ai) für
alle i ∈ N die Gültigkeit von ϕ(ui∈Nai) folgen. Beispielsweise sind Konjunktionen von
Gleichungen oder Ungleichungen zulässig.
Berechnungscoinduktion ist durch folgende Beweisregel gegeben:
ϕ(νx1 . . . xn.t)
⇑
ϕ(>) ∧ ∀x1, . . . , xn : (ϕ(x1, . . . , xn) ⇒ ϕ(t))
Anwendungen dieser Regel sind mir nicht bekannt.
241
(12)
Bücher
Richard Bird, Introduction to Functional Programming using Haskell, Prentice Hall 1998
Richard Bird, Pearls of Functional Algorithm Design, Cambridge University Press 2010
Marco Block, Adrian Neumann, Haskell-Intensivkurs, Springer 2011
Manuel M. T. Chakravarty, Gabriele C. Keller, Einführung in die Programmierung mit
Haskell, Pearson Studium 2004
Ernst-Erich Doberkat, Haskell: Eine Einführung für Objektorientierte, Oldenbourg 2012
Kees Doets, Jan van Eijck, The Haskell Road to Logic, Maths and Programming, Texts
in Computing Vol. 4, King’s College 2004
Paul Hudak, The Haskell School of Expression: Learning Functional Programming through
Multimedia, Cambridge University Press 2000
Paul Hudak, John Peterson, Joseph Fasel, A Gentle Introduction to Haskell, Yale and
Los Alamos 2000
Graham Hutton, Programming in Haskell, Cambridge University Press 2007
Peter Pepper, Petra Hofstedt, Funktionale Programmierung: Sprachdesign und Programmiertechnik, Springer 2006
242
Fethi Rabhi, Guy Lapalme, Algorithms: A Functional Programming Approach, AddisonWesley 1999
Simon Thompson, Haskell: The Craft of Functional Programming, 3. Auflage, AddisonWesley 2011
Raymond Turner, Constructive Foundations for Functional Languages, McGraw-Hill 1991
243
Index
A∗, 18
BΣ, 178
Red-Normalform, 204
S-sortige Funktion, 192
S-sortige Menge, 190
BTΣ(X), 192
TΣ(X, C), 191
NFRed (X, C), 204
P (X, C), 191
Σ-Algebra, 178
Σ-Baum, 230
Σ-Term, 191
Σ-primitiv, 192
α-Konversion, 193
β-Regel, 195
δ-Regel, 194
η-Regel, 195
free(t), 191
λ-Abstraktion, 10
λ-Applikation, 11
µ-Abstraktion, 189
nf (t), 205
ω-covollständig, 237
σ ∗, 192
→Red, 194
var(t), 191
(++), 19
(//), 166
(;), 124
(<-), 124
Algebra, 157
all, 32
any, 32
Applikationsoperator, 14
Array, 165
array, 165
244
Attribut, 42
Auswertungsfunktion, 196
Basissignatur, 178
bind, 122
Bintree, 70
co-CPO, 77
coCPO, 237
Cokette, 77
compileE, 115
Compiler, 152
const, 15
costetig, 77, 237
covollständig, 77
CPO, 77
curry, 16
denotationelle Semantik, 183
Destruktor, 178
do-Notation, 124
drop, 20
elem, 32
Eq, 61
Expr, 44
fail, 122, 195
filter, 32
Fixpunkt, 77
Fixpunktsatz von Kleene, 78
flacher CPO, 207
flip, 16
fold2, 28
foldl, 26
foldr, 29
freie Variable, 191
Functor, 121
Funktion höherer Ordnung, 12
Funktionsapplikation, 11
gebundene Variable, 191
getSubterm, 59
guard, 124
Halbordnung, 77
245
head, 19
Individuenvariable, 17
init, 20
Instanz, 11, 193
Instanz eines Terms, 138
Instanz eines Typs, 17
iterate, 34
Ix, 165
Kellermaschine, 115
Kette, 77
Kind, 119
Kompositionsoperator, 14
konfluent, 203
Konstruktor, 178
Konstruktor einer Algebra, 179
Kopf einer Abstraktion, 191
last, 20
lazy pattern, 200
lines, 30
Listenkomprehension, 33
logische Programmierung, 183
logische Reduktion, 184
lookup, 25
lookupM, 135
many, 130
map, 24
mapM, 132
Matching, 11
mkArray, 165
Monad, 122
MonadPlus, 123
monomorph, 17
monoton, 77
mplus, 123
msum, 131
mzero, 123
newtype, 79
notElem, 32
operationelle Semantik, 183
246
polymorph, 17
poset, 77
PTrans, 150
putSubterm, 59
range, 165
Read, 71
Redex, 194
reduce, 111
Redukt, 194
Reduktionsregel, 194
Reduktionsrelation, 194
Reduktionsstrategie, 207
reflexiver Abschluss, 93
relationale Programmierung, 183
repeat, 34
replicate, 34
return, 122
Ring, 95
root, 57
Rumpf einer Abstraktion, 191
sat, 130
Sektion, 13
Semiring, 94
sequence, 131
Set, 79
Show, 75
showE, 110
Signatur, 157, 178
some, 130
splitAt, 22
StackCom, 115
stetig, 77
Substitution, 138
Subsumptionsordnung, 193
subterms, 57
tail, 19
take, 20
Teiltermrelation, 202
Term, 57
Termreduktion, 189
247
Termunifikation, 140
Trägermenge, 178
Trans, 142
transitiver Abschluss, 93
Typ über S, 190
Typinferenzregeln, 11
typisierbar, 203
Typkonstruktor, 41
Typvariable, 17
when, 130
Wildcard, 17
wohlfundiert, 202
words, 30
zip, 24
zipWith, 24
zipWithM, 132
uncurry, 16
unlines, 30
unwords, 30
update, 15
updList, 21
Variablenbelegung, 196
Variablenumbenennung, 193
vollständig, 77
vollständige Reduktionsstrategie, 207
vollständiger Semiring, 95
Wörter, 18
248
Herunterladen