Modellieren und Implementieren in Haskell - fldit

Werbung
4 Datentypen
Modellieren und Implementieren in Haskell
fldit-www.cs.tu-dortmund.de/∼peter/Essen.pdf
Webseite zur LV: fldit-www.cs.tu-dortmund.de/fpba.html
Peter Padawitz
TU Dortmund
8. Februar 2011
1 Schema einer Datentypdefinition
33
2 Arithmetische Ausdrücke
77
3 Boolesche Ausdrücke
38
4 Symbolische Differentiation
40
5 Expr-Interpreter
41
6 Hilbertkurven
42
7 Farbkreise
47
5 Termbäume
50
6 Typklassen
54
1 Mengenoperationen auf Listen
55
2 Damenproblem
56
3 Quicksort
59
4 Mergesort
60
5 Binäre Bäume
61
6 Einlesen
62
7 Ausgeben
66
1
!
3
#
Inhalt
"
$
1 Vorbemerkung
7
2 Funktionen
9
3 Listen
14
1 Listenteilung
17
2 Listenintervall
18
3 Listenmischung
18
4 Relationsdarstellung von Funktionen
22
5 Listenfaltung
23
6 Listenerzeugende Funktionen
20
7 Listenlogik
26
8 Listenkomprehension
27
9 Länge eines Linienzuges
28
10 Minimierung eines Linienzuges
28
11 Pascalsches Dreieck
30
12 Strings sind Listen von Zeichen
32
2
33
8 Set, ein Datentyp für Mengen
68
9 Kleinste und größte Fixpunkte
69
10 Reflexiver und transitiver Abschluss
74
7 Rund um den Datentyp Expr
77
1 Arithmetische Ausdrücke ausgeben
77
2 Arithmetische Ausdrücke reduzieren
78
3 Arithmetische Ausdrücke in Assemblerprogramme übersetzen
81
8 Monaden
85
1 do-Notation
88
2 Monaden-Kombinatoren
89
3 Die Identitätsmonade
94
4 Die Maybe-Monade
96
5 Die Listenmonade
99
6 Tiefen- und Breitensuche in Bäumen
103
7 Eine Termmonade
105
9 Transitionsmonaden
108
1 Trans, eine Monade totaler Transitionsfunktionen
4
109
2 Die IO-Monade
109
3 IOstore, eine selbstdefinierte Trans-Instanz
114
4 PTrans, eine Monade partieller Transitionsfunktionen
116
10 Monadische Parser
118
1 Monadische Scanner
119
2 Bintree-Parser
121
3 Expr-Parser
122
4 Auswertender Expr-Parser
124
5 Testumgebung für Expr-Interpreter, -Compiler und -Parser
126
11 Felder
130
1 Ix, die Typklasse für Indexmengen
130
2 Dynamische Programmierung
132
3 Alignments
134
12 Das Tai Chi formaler Modellierung*
140
13 Mathematische Semantik funktionaler Programme*
144
1 Das relationale Berechnungsmodell
145
2 Das funktionale Berechnungsmodell
150
3 Schema 1 einer Funktionsdefinition
154
4 λ-Applikationen
160
5 Termination und Konfluenz
164
6 µ-Abstraktionen
167
7 Schema 2 einer Funktionsdefinition
169
8 Schema 3 einer Funktionsdefinition
171
9 Die lazy-evaluation-Strategie
174
180
14 Unendliche Objekte*
188
15 Verifikation*
192
16 Haskell-Lehrbücher
202
17 Index
6
#
Vorbemerkung
"
$
Die Inhalte der gesternten Kapitel werden in der Bachelor-LV Funktionale Programmierung nicht behandelt. Vielmehr sind sie Gegenstand meiner Wahlveranstaltungen Funktionales und regelbasiertes Programmieren (Master und Diplom) bzw. Einführung in den
logisch-algebraischen Systementwurf (Bachelor und Diplom) und Logisch-algebraischer
Systementwurf (Master und Diplom).
HörerInnen der LV Funktionale Programmierung rate ich dringend, diese Folien zum Vor(!) und Nacharbeiten von Vorlesungen zu verwenden, nicht jedoch zu glauben, dass sie
Vorlesungsbesuche ersetzen können. Der Einstieg in die funktionale Programmierung ist
für jeden mit anderen Problemen verbunden:
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.
7
5
10 Auswertung durch Graphreduktion
!
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 Haskell-Lehrbücher und fldit-www.cs.tu-dortmund.de/fpba.html). Alle Hilfsfunktionen
und -datentypen, die in den Beispielen verwendet werden, sind hier auch – manchmal in
früheren 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!
Interne Links sind an ihrer braunen Färbung erkennbar. Jede Kapitelüberschrift ist mit
dem Inhaltsverzeichnis verlinkt. Namen von Haskell-Modulen sind mit den jeweiligen
Programmdateien verknüpft.
8
!
#
Funktionen
"
$
Neben
• Produkten A1 × . . . × An (Haskell-Notation: (A1,...,An)) und
• Summen A1 + · · · + An
(werden in Haskell als Datentypen implementiert; s.u.)
werden in Haskell auch Funktionen als Objekte betrachtet und u.a. als λ-Abstraktionen
\p -> e (mathematisch: λp.e) dargestellt. p ist ein Muster für die möglichen Argumente
der Funktion und e ein Ausdruck, der die Funktionswerte beschreibt und i.d.R. Variablen
enthält, die in p vorkommen. Eine Funktion f wird mit Hilfe von Gleichungen definiert:
f p = e (applikative Definition)
ist äquivalent zu
f = \p -> e
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 der Aufruf
((+) 5) 6
äquivalent zu
Der Ausdruck, der die Anwendung einer λ-Abstraktion \p -> e auf ein Argument repräsentiert, heißt λ-Applikation, z.B. (\p -> e)(e’). Die Applikation führt nur dann
zu einem Ergebnis, wenn der Ausdruck e’ das Muster p trifft (matcht).
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
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
(+) 5 6
f1 (f2 (...(fn a)...)))
!
f1 (f2 (...(fn a)...)))
!
9
11
(+) ist als Präfixfunktion definiert, kann aber auch infix verwendet werden. Dann entfallen
die runden Klammern:
U.a. benutzt man den Kompositionsoperator, um in einer applikativen Definition Argumentvariablen einzusparen:
5 + 6
Eine Funktion eines Typs a -> b -> c, deren Name mit einem Buchstaben beginnt,
kann ebenfalls infix verwendet werden, muss dann aber in Hochkommas eingeschlossen
werden. Beispiel:
mod :: Int -> Int -> Int
mod 11 5 ist äquivalent zu 11 `mod` 5
Die Infixnotation wird auch verwendet, um die in einer Funktion eines Typs
a -> b -> c
enthaltenen Sektionen (Teilfunktionen) des Typs a -> c bzw. b -> c zu benennen.
Z.B. sind die Ausdrücke
(+ 5)
(+) 5
(11 +)
mod 5
(`mod` 5)
(11 `mod`)
Funktionen des Typs Int -> Int. Hier sind alle angegebenen Klammern notwendig!
10
f a b = g (h a) b ist äquivalent zu f a = g $ h a ist äquivalent zu f = g . h
f a b = g $ h a b ist äquivalent zu f a = g . h a ist äquivalent zu f = (g.).h
Weitere nützliche Funktionsgeneratoren, -transformatoren und kombinatoren:
const :: a -> b -> a
const a _ = 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
flip :: (a -> b -> c) -> b -> a -> c
flip f b a = f a b
Vertauschung der Argumente
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
12
(***) :: (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 ||
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 -> b ->
c 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 man t durch Ersetzung der Typvariablen aus u erhält.
Individuenvariablen können durch Elemente eines Typs ersetzt werden. Man erkennt
sie ebenfalls daran, dass sie mit einem Kleinbuchstaben beginnen. Eine besondere Individuenvariable ist der Unterstrich _. Er darf nur auf der linken Seite einer Funktionsdefinition verwendet werden und wird dort für ein Argument benutzt, von dem die jeweils
definierte Funktion nicht abhängt.
length :: [a] -> [a]
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,8,4,5]
(!!) :: [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]
15
13
!
#
Listen
"
$
Sei A eine Menge. Wörter über A sind Ausdrücke der Form a1 . . . an mit a1, . . . , an ∈ A.
Sie werden in Haskell als Listen implementiert: a1 . . . an wird zu [a1, . . . , an].
Die Menge der Wörter über A wird in der Mathematik mit A∗ bezeichnet. Der entsprechende Haskell-Typ der Listen mit Elementtyp(variable) a lautet [a].
Haskell führt die extensionale Darstellung [a1, . . . , an] auf ihre Konstruktordarstellung
zurück:
[a1, . . . , an]
! a1 : (a2 : (. . . (an : []) . . . ))
Hierbei sind [] und (:) Konstruktoren genannte Objekte des Typs [a] bzw.
a -> [a] -> [a], welche die leere Liste bzw. die Funktion, die ein Element vorn an einen
Liste anfügt, bezeichnen. Da sie sich aus dem Typ von (:) ergibt, ist die Klammerung
in der Konstruktordastellung überflüssig.
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. Dann ist
s 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.
14
last :: [a] -> a
last [a]
= a
last (_:s) = last s
last [3,2,8,4] ! 4
take
take
take
take
:: Int -> [a] -> [a]
0 _
= []
n (a:s) | n > 0 = a:take (n-1) s
_ []
= []
take 3 [3,2,4,8,4,5] ! [3,2,4]
drop
drop
drop
drop
:: Int -> [a] -> [a]
0 s
= s
n (_:s) | n > 0 = drop (n-1) s
_ []
= []
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]
16
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
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 _ []
= ([],[])
map-Funktionen
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 $ \a b -> (a,b)
zip [3,2,8,4] [8,9,35] ! [(3,8),(2,9),8,35)]
17
19
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 =
_ _ _
=
Unendliche Listen erzeugende Funktionen
Standardfunktionen:
[a]
a:sublist s 0 $ j-1
sublist s (i-1) $ j-1
[]
Beispiel Mischung zweier Listen
Sind s1 und s2 zwei geordnete Listen mit eindeutigen Vorkommen ihrer jeweiligen Elemente, dann ist auch merge(s1, s2) eine solche Liste.
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
18
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 Datei der Lazy.hs):
blink :: [Int]
blink = 0:1:blink
blink
nats :: Int -> [Int]
nats n = n:map (+1) (nats n)
nats 3
20
!
!
0:1:0:1:...
3:4:5:6:...
fibs :: Int -> [Int]
fibs = 1:tailfibs where tailfibs = 1:zipWith (+) fibs tailfibs
take 11 fibs
!
Beschränkte Iterationen (for-Schleifen) entsprechen meistens Listenfaltungen.
Faltung einer Liste von links her
f
f
primes :: [Int]
primes = sieve $ nats 2
f
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 !
f
f
[1,1,2,3,5,8,13,21,34,55,89]
[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]
a
b1
b2
b4
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f a (b:s) = foldl f (f a b) s
foldl _ a _
= a
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
21
Relationsdarstellung von Funktionen
b3
=
=
=
=
foldl (*) 1
foldl (||) False
foldl1 max
concat . map f
23
Parallele Faltung zweier Listen von links her
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:
f
f
f
f
f
type Relation a b = [(a,b)]
lookup :: Relation a b -> a -> Maybe b
lookup ((a,b):r) c = if a == c then Just b else lookup r c
lookup _ _
= Nothing
updRel :: Relation a b -> a -> b -> Relation 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
die Gleichung f(a) = Nothing als Nichtdefiniertheit von f an der Stelle a interpretiert
wird.
22
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},
listsToFun b as bs a =
b
sonst.
24
Jeder Aufruf von map, zipWith, filter oder einer Komposition dieser Funktionen entspricht einer Listenkomprehension:
Faltung einer Liste von rechts her
f
f
f
f
f
b1
b2
b3
b4
b5
a
map f s
= [f a | a <- s]
zipWith f s s' = [f a b | (a,b) <- zip s s']
Beachte : zip s s' =
& [(a,b) | a <- s, b <- s']
= kartesisches Produkt von s und s'
filter f s
= [a | a <- s, f a]
allgemein:
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
[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.
Mit der Listenkomprehension lassen sich u.a. Relationen (= Teilmengen eines kartesischen
Produktes) definieren, sofern die die Komponentenmengen als Listen gegeben sind, z.B.
die Menge aller Tripel (a, b, c) ∈ A1 × A2 × A3, die p : A1 × A2 × A3 → Bool erfüllen:
[(a,b,c) | a <- a1, b <- a2, c <- a3, p(a,b,c)].
27
25
Listenlogik
Beispiel Länge eines Linienzuges
any :: (a -> Bool) -> [a] -> Bool
any f = or . map f
any (>4) [3,2,8,4] ! True
type Point = (Float,Float)
type Path = [Point]
all :: (a -> Bool) -> [a] -> Bool
all f = and . map f
all (>2) [3,2,8,4] ! False
length :: Path -> Float
length ps = sum $ zipWith distance ps $ tail ps
elem :: a -> [a] -> Bool
elem a = any (a ==)
elem 2 [3,2,8,4] ! True
distance :: Point -> Point -> Float
distance (x1,y1) (x2,y2) = sqrt $ (x2-x1)^2+(y2-y1)^2
notElem :: a -> [a] -> Bool
notElem a = all (a /=)
notElem 9 [3,2,8,4] ! True
Beispiel Minimierung eines Linienzuges
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]
26
minimize :: Path -> Path
minimize (p:ps@(q:r:s)) | straight p q r = minimize $ p:r:s
| True
= p:minimize ps
minimize ps
= ps
28
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:
Die Binomialfunktion hat dieselben Eigenschaften – modulo der Bijektivität zwischen
den Typen Int -> [Int] und Int -> Int -> Int von pascal bzw. binom:
binom :: Int -> Int -> Int
binom n k = product[k+1..n]`div`product[1..n-k]
=
n!
k!(n − k)!
Da die Lösung von (1) und (2) in pascal (als Funktionsvariable) eindeutig ist, gilt für
alle n ∈ N und k ≤ n:
pascal(n)!!k = binom(n)(k).
Die obige Darstellung des Pascalschen Dreiecks wurde übrigens mit Expander2 erzeugt
durch Auswertung des Ausdrucks
shelf 1$one 1:map(pasc)[1..10]
und die graphische Interpretation des Ergebnisses, wobei
one x = turtle[rect(15,11),text x]
pasc n = shelf(x+1)$map(one)$pascal n
Zwei geglättete Linienzüge links vor und rechts nach der Minimierung
31
29
Beispiel Pascalsches Dreieck stellt eine Aufzählung der Binomialkoeffizienten dar.
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.
pascal :: Int -> [Int]
pascal 0 = [1]
pascal n = zipWith (+) (s++[0]) (0:s) where s = pascal $ n-1
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.
pascal(n)!!k ist die Anzahl der k-elementigen Teilmengen einer n-elementigen Menge.
Eigenschaften:
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)
30
32
!
#
Datentypen
"
Zunächst das allgemeine Schema einer Datentypdefinition:
$
data DT a1 ... am = Constructor_1 e_11 ... e_1n_1 | ... |
Constructor_k e_k1 ... e_kn_k
e_11,...,e_kn_k sind beliebige Typausdrücke, die aus Typkonstanten (z.B. Int), Typvariablen a1,...,am und Typkonstruktoren (Produktbildung, Listenbildung, Datentypen) zusammengesetzt sind.
Kommt DT selbst in einem dieser Typausdrücke vor, dann spricht man von einem rekursiven Datentyp.
Die Elemente von DT a1 ... am sind alle funktionalen Ausdrücke, die aus den ObjektKonstruktoren Constructor_1,...,Constructor_k und Elementen von Instanzen
von a1,...,am zusammengesetzt sind. Als Funktion hat Constructor_i den Typ
e_i1 -> ... -> e_in_i -> DT a1 ... am.
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', ...}
35
33
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 e_i1 ... e_in_i ersetzt durch
Constructor_i {attribute_i1 :: e_i1, ..., attribute_in_i :: e_in_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 -> e_ij
Attribute sind also invers zu Konstruktoren. Man nennt sie deshalb auch Destruktoren.
attribute_ij (Constructor_i t1 ... tn_i)
34
hat den Wert tj.
Beispiel Arithmetische Ausdrücke (Haskell-Code in: Expr.hs)
data Expr = Con Int | Var String | Sum [Expr] | Prod [Expr] |
Expr :- Expr | Int :* Expr | Expr :^ Int
oder (mit der Option -fglasgow-exts beim Aufruf des ghc-Interpreters):
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"]]
36
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]
39
37
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
38
Funktionen mit Argumenten eines Datentyps werden in Abhängigkeit vom jeweiligen
Muster (pattern) der Argumente definiert:
Beispiel 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
40
Beispiel Expr-Interpreter
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 im Abschnitt Monadische Parser vorgestellt.
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
Zum Zeichnen einer Hilbertkurve benötigt man nur die folgenden Aktionen:
up,down,back,forth :: Action
up = Turn (-90); down = Turn 90; back = Move (-1); forth = Move 1
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
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
41
Beispiel Hilbertkurven
43
move :: Direction -> [Action]
move dir = case dir of North -> [up,forth,down]; East -> [forth]
South -> [down,forth,up]; West -> [back]
Unter Verwendung dieser Hilfsfunktionen lässt sich die Aktionsliste für eine Hilbertkurve
der Tiefe n aus den Aktionslisten von vier Hilbertkurven der Tiefe n − 1 konstruieren:
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
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.
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.
42
44
Mit Hilfe von foldl (s.o.) kann eine Aktionsliste vom Typ [Action] leicht in eine Punktliste vom Typ Path (s.o.) überführt werden. Wir können aber auch hilbertActs so
abwandeln, dass nicht erst am Ende des rekursiven Aufbaus einer Aktionsliste die Transformation in eine Punktliste erfolgt, sondern die Punktliste selbst rekursiv erzeugt wird.
Dazu wird move in eine Funktion des Typs Point -> Direction -> Point umgewandelt, die für jede Punktliste ps und jede Himmelsrichtung dir den direkten Nachfolger
des letzten Elementes von ps berechnet:
move :: Path -> Direction -> Point
move ps dir = case dir of North -> (x,y-1); East -> (x+1,y)
South -> (x,y+1); West -> (x-1,y)
where (x,y) = last ps
Beispiel Farbkreise
Zur Repräsentation von Farben wird häufig der folgende Datentyp verwendet:
data Color = RGB Int Int Int
red
= RGB 255 0 0;
magenta = RGB 255 0 255; blue
= RGB 0 255 0
cyan = RGB 0 255 255; green
= RGB 0 0 255;
yellow = RGB 255 255 0
black = RGB 0 0 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 Color also insgesamt
1530 darstellen und mit folgender Funktion aufzählen:
nextCol
nextCol
nextCol
nextCol
nextCol
nextCol
nextCol
:: Color -> Color
(RGB 255 0 n) | n
(RGB n 0 255) | n
(RGB 0 n 255) | n
(RGB 0 255 n) | n
(RGB n 255 0) | n
(RGB 255 n 0) | n
<
>
<
>
<
>
255
0
255
0
255
0
=
=
=
=
=
=
RGB
RGB
RGB
RGB
RGB
RGB
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
45
47
Der von move aus der k-ten Hilbertkurve (k = 1, 2, 3) der Tiefe n − 1 berechnete Punkt
ist der Anfangspunkt der (k + 1)-ten Kurve der Tiefe n − 1:
Jedes RGB-Element außerhalb des Definitionsbereiches von nextCol repräsentiert eine
aufgehellte bzw. abgedunkelte Hue-Farbe.
hilbertPoints :: Int -> Path
hilbertPoints n = f n (0,0) East
where f 0 p _
= [p]
f i p dir = ps1++ps2++ps3++ps4
where g = f $ i-1
ps1 = g p sdir
ps2 = g (move ps1 dir) dir
ps3 = g (move ps2 sdir) dir
ps4 = g (move ps3 $ flip' dir) $ flip' sdir
sdir = swap dir
nextCol induziert einen Farbkreis, der sich am einfachsten – für jede Startfarbe – als
unendliche Liste darstellen lässt:
colorCirc :: Color -> [Color]
colorCirc = iterate nextCol
Damit kann man z.B. jedem Element einer n-elementigen Liste (n ≤ 1530) eine von den
Farben seiner jeweiligen Nachbarn soweit wie möglich entfernte Farbe zuordnen:
addColor :: [a] -> [(a,Color)]
addColor s = zip s $ map f [0..n-1]
where f i = colorCirc red!!round (float i*1530/float n)
n = length s
Den Elementen einer 11-elementigen Liste ordnet addColor die folgenden Farben zu:
46
48
size, height ::
size (F _ ts) =
size _
=
height (F _ ts)
height _
Term f a -> Int
sum (map size ts)+1
1
= foldl max 0 (map height ts)+1
= 1
positions :: Term f a -> [[Int]]
positions (F _ ts) = []:concat (zipWith f ts [0..length ts-1])
where f t i = map (i:) $ positions t
positions _
= [[]]
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]]
Anwendung von addColor auf die Hilbertkurve der Tiefe 5
51
49
!
#
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 (Haskell-Code in: Examples.hs).
data Term f a = F f [Term f a] | V a
getSubterm t p liefert den Unterbaum von t, dessen Wurzel an Position p von t steht:
getSubterm :: Term f a -> [Int] -> Term f a
getSubterm t []
= t
getSubterm (F _ ts) p = getSubterm' ts p
getSubterm' :: [Term f a] -> [Int] -> Term f a
getSubterm' (t:_) (0:p) = getSubterm t p
getSubterm' (_:ts) (n:p) = getSubterm' ts $ (n-1):p
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]]
50
52
mapT h t wendet h auf die Markierungen der F-Knoten von t an:
Beispiel Mengenoperationen auf Listen
mapT :: (f -> g) -> Term f a -> Term g a
mapT h (F f ts) = F (h f) $ map (mapT h) ts
mapT _ (V a)
= V a
insert :: Eq a => a -> [a] -> [a]
insert a s@(b:s') = if a == b then s else b:insert a s'
insert a _
= [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
remove :: Eq a => a -> [a] -> [a]
remove = filter . (/=)
union :: Eq a => [a] -> [a] -> [a]
union = foldl $ flip insert
Mengenvereinigung
diff :: Eq a => a -> [a] -> [a]
diff = foldl $ flip remove
Mengendifferenz
inter :: Eq a => [a] -> [a] -> [a]
inter = filter . flip elem
Mengendurchschnitt
unionMap :: Eq a => (a -> [b]) -> [a] -> [b]
unionMap = (foldl union [] .) . map
concatMap für Mengen
55
53
!
#
Typklassen
"
$
Beispiel Damenproblem
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
queens 5
!
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.
54
56
Zwischenwerte von queens 4
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
max x y = if x >= y then x else y
min x y = if x <= y then x else y
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
anstelle von filter und der conquer-Operation merge anstelle von ++:
59
57
queens :: Int -> [[Int]]
queens n = loop [1..n] []
where loop :: [Int] -> [Int]
->
[[Int]]
freie
vergebene
Liste aller (Teil-)Lösungen
Spalten
Spalten
loop [] ys = [ys]
loop xs ys = concatMap (uncurry loop)
[(remove x xs,x:ys) |
x <- xs,
let noDiag y i = x /= y+i && x /= y-i,
x und y liegen nicht auf einer Diagonalen
and $ zipWith noDiag ys [1..length ys]]
Zeilenindizes
loop berechnet die Spaltenpositionen der Damen zeilenweise von unten nach oben.
58
Beispiel Mergesort
mergesort :: Ord a => [a] -> [a]
mergesort (x:y:s) = merge (mergesort $ x:s1) (mergesort $ y:s2)
where (s1,s2) = split s
oder
(\(s1,s2) -> merge (mergesort $ x:s1)
(mergesort $ y:s2)) $ split s
mergesort s
= s
split :: [a] ->
split (x:y:s) =
oder
split s
=
([a],[a])
(x:s1,y:s2) where (s1,s2) = split s
(\(s1,s2) -> (x:s1,y:s2)) $ 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
60
Beispiel Binäre Bäume
data Bintree a = Empty | Fork (Bintree a) a (Bintree a)
subtrees :: Bintree a -> [Bintree a]
subtrees (Fork left _ right) = [left,right]
subtrees _
= []
lex :: ReadS String ist eine Standardfunktion, die ein evtl. aus mehreren Zeichen bestehendes Symbol erkennt, vom Eingabestring abspaltet und das Paar (Symbol,Resteingabe)
ausgibt.
Den 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).
leaf :: a -> Bintree a
leaf = flip (Fork Empty) Empty
insertTree :: Ord a => a -> Bintree a ->
insertTree a t@(Fork t1 b t2) | a == b =
| a < b =
| True
=
insertTree a _
=
Bintree a
t
Fork (insertTree a t1) b t2
Fork t1 b $ insertTree a t2
leaf a
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.
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
61
63
Beispiel Binäre Bäume
Einlesen (Haskell-Code in: Examples.hs)
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"
_
-> error "PreludeText.read: ambiguous parse"
reads :: ReadS a
reads = readsPrec 0
instance Read a => Read (Bintree a) where readsPrec _ = readTree
readTree :: Read a => ReadS (Bintree a)
readTree s = parses1++parses2++parses3
where parses1 = [(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]
parses2 = [(Fork left a Empty,s4) | (a,s1) <- reads s,
("(",s2) <- lex s1,
(left,s3) <- readTree s2,
(")",s4) <- lex s3]
parses3 = [(leaf a,s1) | (a,s1) <- reads s]
Die Instanzen von reads in der Definition von readTree haben den Typ ReadS a.
readsPrec :: Int -> ReadS a
62
64
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.
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
read "5(7(3, 8),6( 2) ) " :: Bintree Int
! 5(7(3,8),6(2))
instance Show a => Show (Bintree a) where show = showTree0
bzw.
showsPrec _ = showTree
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")]
showTree0
showTree0
showTree0
showTree0
read "5(7(3,8),6(2))hh"
:: Bintree Int
! Exception: PreludeText.read:
no parse
:: 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 _ = ""
67
65
Set, ein Datentyp für Mengen
Ausgeben (Haskell-Code in: Examples.hs)
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 ""
newtype Set a = Set {list :: [a]}
instance Eq a => Eq (Set a) where s == s' = s <= s' && s' <= s
instance Eq a => Ord (Set a) where s <= s' =
all (`elem` list s') $ list s
instance Show a => Show (Set a) where show = show . list
Set[1,2,3] <= Set[3,4,2,5,1,99]
Set[1,2,3] >= Set[3,4,2,5,1,99]
!
!
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.
66
True
False
eliminiert Mehrfachvorkommen
mkSet :: Eq a => [a] -> Set a
mkSet = Set . union []
shows :: a -> ShowS
shows = showsPrec 0
newtype kann data ersetzen, wenn der
Datentyp genau einen Konstruktor hat
68
Kleinste und größte Fixpunkte (Haskell-Code in: Examples.hs)
Da -a. eine Teilmenge von P(N ) ist, implementieren wir sie durch den oben eingeführten
Typ Set a:
Eine Menge A heißt ω-vollständiger *-Halbverband (ω-complete *-semilattice oder
complete partial order, kurz: CPO), falls A eine Halbordnung ≤, ein kleinstes Element
⊥ (bottom) und Suprema *i∈Nai aller aufsteigenden Ketten a1 ≤ a2 ≤ a3 ≤ . . . von A
besitzt.
Eine Funktion f : A → B zwischen zwei CPOs A und B heißt aufwärtsstetig, falls für
alle aufsteigenden Ketten a1 ≤ a2 ≤ . . . von A
f (*i∈Nai) = *i∈Nf (ai)
gilt.
reachables :: Eq a => Graph a -> a -> Set a
reachables graph a = lfp f least
where f (Set as) = Set $ union as $ unionMap graph as
least = Set [a]
Hier wird die Funktion graph in mehreren Iterationen auf dieselben Knoten angewendet.
Um das zu vermeiden, wählen wir einen anderen CPO, nämlich -a. × P(N ), mit komponentenweiser Mengeninklusion als Halbordnung und ({a}, ∅) als kleinstem Element.
a ∈ A heißt Fixpunkt von f : A → A, falls f (a) = a gilt.
Die Menge der von a aus erreichbaren Knoten ist die erste Komponente des kleinsten
Fixpunkts der Funktion
Fixpunktsatz von Kleene
f : -a. × P(N ) → -a. × P(N )
"
(M, used) 0→ (M ∪ {graph(b) | b ∈ M \ used}, used ∪ M )
Sei f : A → A aufwärtsstetig. Dann ist
der kleinste Fixpunkt von f .
type Graph a = a -> [a]
lfp(f ) =def *i∈N f i(⊥)
!
69
Ist f aufwärtsstetig, dann ist f monoton, d.h. für alle a ∈ A gilt:
a ≤ b ⇒ f (a) ≤ f (b).
Daraus folgt f i(⊥) ≤ f i+1(⊥) für alle i ∈ N, so dass es, falls A endlich ist, ein i ∈ N gibt
mit f i(⊥) = f i+1(⊥).
In diesem Fall lässt sich lfp(f ) sehr einfach berechnen:
lfp :: Ord a => (a -> a) -> a -> a
lfp f a = if fa <= a then a else lfp f fa where fa = f a
71
Wir implementieren sowohl -a. als auch P(N ) durch Set a:
reachables :: Eq a => Graph a -> a -> Set a
reachables graph a = fst $ lfp f least
where f (Set as,Set used) = (Set $ union as $ unionMap graph
$ diff as used,
Set $ union used as)
least = (Set [a],Set [])
Nach dem Fixpunktsatz von Kleene liefert lfp f a den kleinsten Fixpunkt von f , falls
<= eine Halbordnung auf A, a das kleinste Element von A und f aufwärtsstetig ist.
Beispiel
Die Knotenmengen eines Graphen graph : N → N ∗, die einen bestimmten Knoten a ∈ N
enthalten, bilden den CPO -a. ⊆ P(N ), dessen Halbordnung die Mengeninklusion und
dessen kleinstes Element die Menge {a} ist. Die Menge der von a aus erreichbaren Knoten
von graph ist der kleinste Fixpunkt der Funktion
f : -a. → -a.
"
M 0→ M ∪ {graph(b) | b ∈ M }.
70
72
Beispiele
Abgesehen von der unterschiedlichen Mengenrepräsentation ([a] bzw. Set a) liefern sowohl reachables graph a als auch
rClosure (tClosure graph nodes) nodes a
die Menge der von a aus erreichbaren Knoten von graph.
graph1
graph1 a = case a of 1 -> [2,3]; 3 -> [1,4,6]; 4 -> [1]; 5 -> [3,5]
6 -> [2,4,5]; _ -> []
reachables graph1 1
!
[1,2,3,4,6,5]
graph2 a = if a > 0 && a < 7 then [a+1] else []
reachables graph2 1
!
[1,2,3,4,5,6,7]
73
Reflexiver und transitiver Abschluss
Der reflexive Abschluss G eines Graphen G erweitert G für jeden Knoten a von G
um eine Kante von a nach a:
eq
rClosure :: Eq a => Graph a -> [a] -> Graph a
rClosure graph nodes = fold2 update graph nodes $ map graph' nodes
where graph' a = insert a $ graph a
Der transitive Abschluss G+ eines Graphen G erweitert G für jeden Weg von a nach
b in G um eine Kante von a nach b. Der Floyd-Warshall-Algorithmus berechnet G+,
indem er, ausgehend von G, iterativ für jeweils einen Knoten b von G aus dem zuvor
berechneten Graphen G3 einen neuen, trans(G3, b), berechnet, der für je zwei Kanten von
G3 von a nach b bzw. von b nach c zusätzlich eine Kante von a nach c enthält:
tClosure :: Eq a => Graph a -> [a] -> Graph a
tClosure graph nodes = foldl trans graph nodes
where trans graph b = fold2 update graph nodes $ map graph' nodes
where graph' a = if b `elem` sucs
then union sucs $ graph b else sucs
where sucs = graph a
Ist n die Anzahl der Knoten von G, also die Länge der Liste nodes, dann ist O(n3) die
Komplexität von tClosure: Das äußere fold durchläuft nodes, das innere ebenfalls, und
die Abfrage b ‘elem‘ sucs kann im worst case, wenn sucs alle Knoten von G enthält,
dazu führen, dass jeder einzelne Update von G3 aus n Vergleichen mit b besteht.
Die Repräsentation von Graphen (= binären Relationen auf einer Menge A) durch Objekte vom Typ Graph a entspricht der Darstellung durch Adjazenzlisten. Die alternative
Repräsentation bilden Adjazenzmatrizen. Der entsprechende Funktionstyp ist
type GraphM a = a -> a -> Bool
Mit dieser Darstellung eines Graphen lassen sich rClosure bzw. tClosure erheblich
vereinfachen:
rClosureM :: Eq a => GraphM a -> GraphM a
rClosureM graph a b = a == b || graph a b
75
tClosureM :: GraphM a -> [a] -> GraphM a
tClosureM = foldl $ \graph b a c -> graph a c || graph a b && graph b c
Viele Algorithmen – wie z.B. die Berechnung kürzester Wege – lassen sich als Instanzen
der Verallgemeinerung von tClosureM zu closure darstellen, wobei Bool durch einen
beliebigen Semiring ersetzt wird:
class Semiring a where add,mul :: a -> a -> a
type Matrix a b = a -> a -> b
closure :: Semiring b => Matrix a b -> [a] -> Matrix a b
closure = foldl $ \mat b a c -> mat a c `add` (mat a b `mul` mat b c)
Eine Menge A mit Addition und Multiplikation ist ein Semiring, wenn A eine Null und eine Eins enthält,
so dass für alle a, b, c ∈ A 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
Distribution von ∗ über +
Um ein Ring zu sein, muss A außerdem inverse Elemente bzgl. + besitzen.
74
76
!
#
Rund um den Datentyp Expr
"
$
Arithmetische Ausdrücke ausgeben
instance Show Expr where showsPrec _ = showE
showE :: Expr -> ShowS
showE (Con i)
= (show i++)
showE (Var x)
= (x++)
showE (Sum es) = foldShow '+' es
showE (Prod es) = foldShow '*' es
showE (e :- e') = ('(':) . showE e . ('-':) . showE e' . (')':)
showE (n :* e) = ('(':) . (show n++) . ('*':) . showE e . (')':)
showE (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
Prod[Con 3,Con 5,x,Con 11]
Sum [11 :* (x :^ 3),5 :* (x :^ 2),16 :* x,Con 33]
showE
!
!
showE
(3*5*x*11)
((11*(x^3))+(5*(x^2))+(16*x)+33)
reduce (Sum es)
= mkSum $ map mkScal dom ++ if c == 0 then [] else [Con c]
where (c,dom,g :: Expr -> Int) :: Estate
= foldl f (0,[],const 0) $ map reduce es
f :: Estate -> Expr -> Estate
f st (Con 0)
= st
f (c,dom,g) (Con i) = (c+i,dom,g)
f (c,dom,g) (i:*e) = (c,insert e dom,update g e $ g e+i)
f st e
= f st (1:*e)
mkScal e = reduce $ g e :* e
mkSum [] = zero
mkSum [e] = e
mkSum es = Sum es
reduce (Prod es) = mkProd $ map mkExpo dom ++ if c == 1 then [] else [Con c]
where (c,dom,g :: Expr -> Int) :: Estate
= foldl f (0,[],const 0) $ map reduce es
f :: Estate -> Expr -> Estate
f st (Con 1)
= st
f (c,dom,g) (Con i) = (c*i,dom,g)
f (c,dom,g) (e:^i) = (c,insert e dom,update g e $ g e+i)
f st e
= f st (e:^1)
mkExpo e
= reduce $ e :^ g e
mkProd [] = one
mkProd [e] = e
mkProd es = Prod es
reduce e = e
77
79
Arithmetische Ausdrücke reduzieren
Die folgende Funktion reduce wendet die Gleichungen
0+e=e
e+e=2∗e
(m ∗ e) + (n ∗ e) = (m + n) ∗ e
m ∗ (n ∗ e) = (m ∗ n) ∗ e
0∗e=0
e ∗ e = e2
em ∗ en = em+n
(em)n = em∗n
auf einen arithmetischen Ausdruck an.
type Estate = (Int, [Expr], Expr -> Int)
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
reduce
:: Expr -> Expr
(e :- e')
(0 :* e)
(1 :* e)
(i :* Con j)
(i :* (j :* e))
(e :^ 0)
(e :^ 1)
(Con i :^ j)
((e :^ i) :^ j)
=
=
=
=
=
=
=
=
=
reduce $ Sum [e,(-1):*e']
zero
reduce e
Con $ i*j
reduce $ (i*j) :* e
one
reduce e
Con $ i^j
reduce $ e :^ (i*j)
78
1∗e=e
e0 = 1
e1 = e
reduce (Sum es) wendet zunächst reduce auf alle Ausdrücke der Liste es an. Die Ergebnisliste res = map reduce es wird dann, ausgehend vom Anfangszustand (0,[],const
0) mit der Zustandsüberführung f zum Endzustand (c,dom,g) gefaltet, der schließlich
mit mkScal in eine reduzierte Version 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 Skalarprodukten m ∗ e mit
derselben Basis e gemäß der Gleichung
summiert.
(m ∗ e) + (n ∗ e) = (m + n) ∗ e
Im Endzustand (c,dom,g) ist c ist die Summe aller Konstanten von res und dom die Liste
aller Basen von Skalarprodukten von res. g :: Expr -> Int ordnet jedem Ausdruck e
die Summe der Skalarfaktoren der Skalarprodukte von res mit der Basis e zu. Nur im
Fall c &= 0 wird Con c in den reduzierten Summenausdruck eingefügt.
reduce (Prod es) arbeitet völlig analog mit Produkten von Potenzen anstelle von Summen von Skalarprodukten.
80
Arithmetische Ausdrücke in Assemblerprogramme übersetzen und in einer
Kellermaschine ausführen
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 einem Keller für ganze Zahlen und 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)
type Store = String -> Int
Die Ausführung einer Kommandoliste besteht in der Hintereinanderausführung ihrer Elemente:
execute :: [StackCom] -> State -> State
execute = flip $ foldl $ flip executeCom
Die Übersetzung eines arithmetischen Ausdrucks von seiner Baumdarstellung in eine
Befehlsliste erfolgt wie die Definition aller Funktionen auf Expr-Daten 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]
Beginnt die Ausführung des Zielcodes eines Ausdrucks e im Zustand (stack, store), dann
endet sie im Zustand (a : stack, store), wobei a der Wert von e unter der Variablenbelegung store ist:
executeE (compileE e) (stack,store) = (evalE e store:stack,store)
81
83
Die Bedeutung der einzelnen Zielkommandos wird durch einen Interpreter auf State
definiert:
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)
Sub st
= executeOp (foldl1 (-)) 2 st
(Add n) st
= executeOp sum n st
(Mul n) st
= executeOp product n 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 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:
Pow
Mul 2
Push 16
Load "x"
Mul 2
Push 33
Add 4
Eine Testumgebung für execute und compileE wird im Abschnitt Monadische Parser
vorgestellt.
82
84
!
#
Monaden
"
$
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b sequentielle Komposition
(>>)
:: 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'
(m >>= f) >>= g = m >>= \a -> f a >>= g
m >>= return
= m
return a >>= f
= f a
(1)
Die roten Gleichungen sind kein Teil der Klassendefinition. Sie beschreiben Bedingungen
an Instanzen von Monad, die aber nicht überprüft werden (können).
Die Verwendung von Funktionen oder Instanzen der folgenden Unterklasse von Monad
erfordert den Import des Standardmoduls Monad.hs, also die Zeile
Die Typisierung von Typen durch Kinds erlaubt es, 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!
Einfacher ist es, 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.
Typdeklarationen und Typklassen mit mehreren Typvariablen erfordern beim Aufruf des
ghc-Interpreters die Option -fglasgow-exts.
import Monad
am Anfang des Programms, das ihn verwendet.
85
class Monad m => MonadPlus m where
mzero :: m a
mplus :: m a -> m a -> m a
87
scheiternde Berechnung heißt zero in hugs
parallele Komposition heißt (++) in hugs
mzero >>= f
m >>= \a -> mzero
mzero `mplus` m
m `mplus` mzero
=
=
=
=
mzero
mzero
m
m
(2)
Alle bisher deklarierten Typen sind Typen erster Ordnung und haben den Kind ∗.
Kinds sind Typen von Typen. Alle Kinds sind aus ∗ und → gebildet.
Die Typvariable m (s.o.) und die Ausdrücke Bintree, Term (siehe Datentypen), Graph
und Matrix a (siehe Typklassen) sind Typen zweiter Ordnung und haben daher den
Kind ∗ → ∗. Sie bezeichnen also Funktionen, die Typen erster Ordnung auf Typen erster
Ordnung abbilden.
Matrix bildet Typen erster Ordnung auf Typen zweiter Ordnung ab und hat deshalb den
Kind ∗ → ∗ → ∗.
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
Ein Ausdruck der Form
do a <- m; m'
oder
ist semantisch äquivalent zu:
m >>= \a -> m'
Wird die “Ausgabe” a von m nicht benötigt, dann genügt m anstelle der Zuweisung
a <- m. Außerdem schreibt man
do a <- m; b <- m'; m''
do a <- m; (do b <- m'; m'')
anstelle von
Aus Gleichung (1) (s.o.) folgt:
do a <- m; return a
86
do a <- m
m'
=
m
88
Monaden-Kombinatoren
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
when :: Monad m => Bool -> m () -> m ()
when b m = if b then m else return ()
guard :: MonadPlus m => Bool -> m ()
guard b = if b then return () else mzero
(1)
(2)
⇒
⇒
(do guard True; m1; ...; mn) = (do m1; ...; mn)
(do guard False; m1; ...; mn) = mzero
sat :: m a -> (a -> Bool) -> m a
sat m f = do a <- m; guard $ f a; return a
sequence ms führt die monadischen Objekte der Liste ms hintereinander aus. Wie bei
some m und many m werden die dabei erzeugten Ausgaben vom Typ a aufgesammelt. Im
Gegensatz zu some m und many m ist die Ausführung von sequence ms beendet, wenn
ms leer ist und nicht, 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 dabei erzeugten Ausgaben
vom Typ a.
sat m f (m satisfies f) bildet jede Berechnung m, deren Ergebnis die Bedingung f verletzt,
auf die scheiternde Berechnung mzero ab.
89
91
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 m, bis m scheitert, d.h. das Ergebnis mzero liefert. Das
Ergebnis von some m und many m ist die Liste der bei den Wiederholungen von m erzeugten Ausgaben vom Typ a.
some m scheitert, falls schon die erste Ausführung von m scheitert. many m hingegen gibt
in diesem Fall die leere Liste von Ausgaben zurück.
msum :: MonadPlus m => [m a] -> m a
msum = foldr mplus mzero
heißt concat in hugs
msum wendet mplus auf eine Liste monadischer Objekte an.
90
Die folgenden Funktionen führen mit map bzw. zipWith erzeugten Listen monadischer
Objekte aus:
mapM :: Monad m => (a -> m b) -> [a] -> m [b]
mapM f s = sequence $ map f s
mapM_ :: Monad m => (a -> m b) -> [a] -> m ()
mapM_ = sequence_ $ map f s
zipWithM :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m [c]
zipWithM = sequence $ zipWith f s s'
zipWithM_ :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m ()
zipWithM_ = sequence_ $ zipWith f s s'
92
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
Die Maybe-Instanz von lookupM entspricht der Funktion lookup (siehe Listen): lookupM
s a liefert die zweite Komponente des ersten Paares von s, dessen erste Komponente mit
a übereinstimmt. Ist m jedoch durch die Listenmonade instanziiert, dann liefert lookupM
s a liefert die Liste der zweiten Komponente aller Paare von s, deren erste Komponenten
mit a übereinstimmen.
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
Eine monadische Version von sumTree lautet wie folgt:
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
93
Die Identitätsmonade
95
Die Maybe-Monade
newtype Id a = Id {run :: a}
data Maybe a = Just a | Nothing
instance Monad Id where Id a >>= f = f a
return
= Id
instance Monad Maybe where Just a >>= f = f a
_ >>= _
= Nothing
return = Just
erfüllt (1)
fail _ = mzero
Einen Datentyp mit genau einer Typvariablen a zur Monade zu machen, zielt darauf ab,
über die Komposition >>= festzulegen, wie Werte (jeder Instanz) des Typs a berechnet
werden sollen. So gesehen entspricht der Zugriff auf diese Werte einer Prozedurausführung und wird deshalb oft mit run bezeichnet. Tatsächlich liefert die Formulierung einer
a-wertigen als (Id a)-wertige Funktion praktisch eine Prozedur, wie sie auch in einer
imperativen Sprache aussehen würde.
94
instance MonadPlus Maybe where mzero = Nothing
erfüllt (2)
Nothing `mplus` m = m
m `mplus` _
= m
96
Verwendung der Maybe-Monade
Die Listenmonade
Rechnen mit partiellen Funktionen
Eine partielle Funktion f : A → B wird in Haskell durch f :: A -> Maybe B implementiert. Das Ergebnis der Komposition zweier partieller Funktionen f und g wird durch
f ‘comp‘ g implementiert:
comp f g :: (a -> Maybe b) -> (b -> Maybe c) -> a -> Maybe c
comp f g = do b <- f a; g b
instance Monad [ ] where (>>=) = flip concatMap
return a = [a]
fail _ = mzero
instance MonadPlus [ ] where mzero = []
mplus = (++)
erfüllt (1)
erfüllt (2)
Nach Definition von >>= in der Maybe-Monade ist (f ‘comp‘ g) a genau dann definiert
(hat also einen Wert der Form Just c), wenn f (a) und f (g(a) definiert sind. c ist dann
gleich f (g(a):
do b <- f a; g b
ist äquivalent zu:
f a >>= \b -> g b
ist äquivalent zu:
case f a of Just b -> g b
_ -> Nothing
97
99
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:
case f a of Nothing -> g a
b -> b
ist äquivalent zu:
do b <- f a
guard $ p b
g b
f a `mplus` g a
Verwendung der Listenmonade
Rechnen mit nichtdeterministischen Funktionen
Eine nichtdeterministische Funktion f : A → P(B) kann in Haskell durch
f :: A -> [B] implementiert werden. Das Ergebnis der Komposition zweier nichtdeterministischer Funktionen f und g wird durch f ‘comp‘ g implementiert:
comp f g :: (a -> [b]) -> (b -> [c]) -> a -> [c]
comp f g = do b <- f a; g b ist äquivalent zu: f a >>= \b -> g b
Nach Definition von >>= in der Listenmonade gilt:
Beispiel
Die folgende Variante split2 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):
split2 :: (a -> Bool) -> (a -> Bool) -> [a] -> Maybe ([a],[a])
split2 f g (x:s) = do (s1,s2) <- split2 f g s
if f x then Just (x:s1,s2)
else do guard $ g x; Just (s1,x:s2)
split2 _ _ _
= Just ([],[])
98
(f `comp` g) a
ist äquivalent zu:
concat [g b | b <- f a]
Die Listeninstanz der Monadenfunktion sequence :: [m a] -> m [a] (s.o.) liefert das
- als Liste von Listen dargestellte - kartesische Produkt seiner Argumentlisten as1, . . . , asn:
sequence[as1, . . . , asn] = [[a1, . . . , an] | ai ∈ asi, 1 ≤ i ≤ n] = as1 × · · · × asn.
Unter Verwendung der Listenkomprehension ist die Listeninstanz von sequence demnach
folgendermaßen definiert:
100
sequence :: [[a]] -> [[a]]
sequence (as:bss) = [a:bs | a <- as, bs <- sequence bss]
sequence _
= [[]]
Beispiel sequence $ replicate(3)[1..4] ! [[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]]
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]
Beispiel Tiefen- und Breitensuche in Bäumen (Haskell-Code in: Examples.hs)
Je nach Instanziierung von m liefern searchDF h t und searchBF h t einen oder mehrere Knoteneinträge von t, welche die Bedingung h erfüllen (siehe Termbäume):
searchDF, searchBF :: 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
t :: Term Int Int
t = F 1 [F 2 [F 2 [V 3 ,V (-1)],V (-2)],F 4 [V (-3),V 5]]
searchDF
searchDF
searchBF
searchBF
(<
(<
(<
(<
0)
0)
0)
0)
t
t
t
t
::
::
::
::
Maybe Int !
[Int]
!
Maybe Int !
[Int]
!
Just (-1)
[-1,-2,-3]
Just (-2)
[-2,-3,-1]
103
101
Für binäre Bäume sehen die Suchfunktionen fast genauso aus:
Beispiel Damenproblem
queens :: Int -> [[Int]]
queens n = loop [1..n] []
where loop :: [Int] -> [Int] -> [[Int]]
loop [] ys = [ys]
loop xs ys = do x <- xs
let noDiag y i = x /= y+i && x /= y-i
guard $ and $ zipWith noDiag ys [1..length ys]
loop (remove x xs) $ x:ys
searchBDF, searchBBF :: 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
102
(>
(>
(>
(>
5)
5)
5)
5)
t
t
t
t
::
::
::
::
Maybe Int !
[Int]
!
Maybe Int !
[Int]
!
Just 8
[8,9,6]
Just 6
[6,8,9]
104
Termunifikation
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. ersetzt dort V x durch den Baum h x.
Das Ergebnis von t >>= h heißt h-Instanz von t.
unify t t’ bildet die Bäume t und t’, falls möglich, auf eine Substitution
f :: a -> Term f a ab, so dass die Instanzen t >>= f und t' >>= f von t bzw. t’
miteinander übereinstimmen:
unify :: (Eq f,Eq a) => Term
unify (V a) (V b) | a == b =
| True
=
unify (V a) t
=
f a -> Term f a -> Maybe (a -> Term f a)
Just $ V
Just $ update V a $ V b
do guard $ checkV (/= a) t
Just $ update V a t
= unify (V a) t
= do guard $ a == b
unifyall ts us
unify t (V a)
unify (F f ts) (F g us)
unifyall :: Eq a => [Term f a] -> [Term f a] -> Maybe (a -> Term a)
unifyall [] []
= Just V
unifyall (t:ts) (u:us) = do f <- unify t u
let g = map (>>= f)
h <- unifyall (g ts) $ g us
Just $ (>>= h) . f
unifyall _ _
= Nothing
107
105
t :: Term String String
t = F "+" [F "*" [c "5",V "x"],V "y",c "11"]
where c = flip F []
h :: String -> Term String String
h 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 >>= h !
F "+" [F "*" [F "5" [],
F "/" [F "-" [F "6" []],F "9" [],V "z"]],
F "-" [F "7" [],F "*" [F "8" [],F "0" []]],
F "11" []]
!
#
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
Parser a
partial
functions
106
108
Trans, eine Monade 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 trans’
sequentiell. Dabei erhält trans’ 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.
109
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.
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.
111
Datei lesen mit Fehlerbehandlung und Inhaltsübergabe an Folgeprozedur
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
test :: (Read a,Show b) => (a -> b) -> IO ()
test f = readFileAndDo "source" $ writeFile "target" . show . f . read
catch hat den Typ IO a -> (IOError -> IO a) -> IO a.
Der Aufruf catch m f fängt einen bei der Ausführung von m auftretenden IO-Fehler err
ab, indem er f auf err anwendet.
writeFile :: String -> String -> IO ()
writeFile "target" schreibt einen String
in die Datei target.
110
112
Noch ein IO-Beispiel
Monadische Version von trace
Die Funktionen des Graphikprogramms Painter.hs rufen die folgende Schleife auf, die
bei jedem Durchlauf ein Objekt vom Typ a unter Berücksichtigung jeweils eingegebener
Skalierungsfaktoren 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
113
IOstore, eine selbstdefinierte Trans-Instanz (Haskell-Code in: Expr.hs)
data DefUse = Def String Int | Use String
type Store = String -> Int
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.
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.
114
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)
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 []
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.
115
PTrans, eine Monade partieller Transitionsfunktionen
Sie unterscheidet sich von Trans dadurch, das deren Wertetyp (a,state) in die MaybeMonade eingebettet wird:
newtype PTrans state a = PT {runP :: state -> Maybe (a,state)}
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
werden.
116
Das Ergebnis von prog = (PT trans ‘mplus‘ PT trans’) erlaubt Backtracking:
Monadische Scanner
Liefert trans ein von Nothing verschiedenes Ergebnis, dann wird dieses auch von prog
zurückgegeben. Scheitert trans jedoch, so liefert prog das Ergebnis von trans’.
getChr liest das erste Zeichen eines Strings und gibt es als Ergebnis aus:
Demnach lassen sich mit PTrans deterministische Automaten implementieren: Die Zustandsmenge ist eine Instanz von state, Übergangs- und Ausgabefunktion sind durch
fst . runP bzw. snd . runP gegeben.
getChr :: Parser Char
getChr = PT $ \str -> do c:str <- return str; return (c,str)
Ein Matchfehler bei der Zuweisung führt zum Wert von fail, also const Nothing.
char chr und string str erwarten das Zeichen chr bzw. den String str:
char :: Char -> Parser Char
char chr = sat getChr (== chr)
string :: String -> Parser String
string = mapM char
token p erlaubt vor und hinter des von p erkannten Strings Leerzeichen, Zeilenumbrüche
oder Tabulatoren:
Parser a -> Parser a
token p = do space; a <- p; space; return a
where space = many $ sat getChr (`elem` " \t\n")
119
117
!
#
Monadische Parser
"
$
Ein Parser liest eine Zeichenfolge (auch Wort genannt) von links nach rechts, übersetzt
das jeweils gelesene Teilwort in ein Objekt eines Typs a und gibt daneben das jeweilige
Restwort aus. Betrachtet man ihn 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 Parser = PTrans String
der Monade partieller Transitionen implementiert werden. Ein vollständiger Parser wird
aufgebaut aus den o.g. Monadenkonstanten bzw. -kombinatoren wie return, mzero, >>=,
mplus, mapM, sat und many sowie den folgenden, die typische Scannerfunktionen realisieren.
Die folgenden Programme stehen in Expr.hs.
Es folgen vier Scanner, die Elemente von Basistypen (Wahrheitswerte, natürliche Zahlen,
ganze Zahlen bzw. Identifier) erkennen und in entsprechende Haskell-Typen übersetzen.
bool :: Parser Bool
bool = msum [do token $ string "True"; return True,
do token $ string "False"; return False]
nat,int :: Parser Int
nat = do ds <- some $ sat getChr (`elem` ['0'..'9']); return $ read ds
int = msum [nat,
do char '-'; n <- nat; return $ -n]
identifier :: Parser 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 einem do.
Die Kommas trennen die Elemente der Argumentliste von msum.
118
120
Bintree-Parser
tchar = token . char
bintree :: Parser a -> Parser (Bintree a)
bintree p = do a <- p
msum [do tchar '('
left <- bintree
msum [do tchar ','
right <- bintree
tchar ')'
return $ Fork left a right,
do tchar ')'
return $ Fork left a Empty],
do return $ leaf a]
Z.B. übersetzt runP (bintree int) str den String str, falls möglich, in einen Baum
vom Typ Bintree Int.
moreSummands, moreFactors, power :: Expr -> Parser Expr
moreSummands a = msum [do tchar '-'
b <- summand
moreSummands $ a :- b,
do as <- some $ do tchar '+'; summand
moreSummands $ Sum $ a:as,
return a]
moreFactors a = msum [do as <- some $ do tchar '*'; factor
moreFactors $ Prod $ a:as,
return a]
power a
= msum [do tchar '^'
i <- token int
return $ a :^ i,
return a]
Die Unterscheidung zwischen Parsern 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 Parsers: 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 Parsers gesichert ist.
123
121
Expr-Parser
Auswertender Expr-Parser
runP expr str übersetzt den String str, falls möglich, in ein Objekt des Typs Expr:
Strings für Ausdrücke mit genau einer Variablen (x), lassen sich nach demselben Schema
in die dem jeweiligen Ausdruck entsprechende einstellige Funktion vom Typ Int -> Int
übersetzen:
expr, summand,
expr
= do a
summand = do a
factor = msum
factor :: Parser Expr
<- summand; moreSummands a
<- factor; moreFactors a
[do x <- token identifier
power $ Var x,
do i <- token int
msum [power $ Con i,
scalar i],
do tchar '('; a <- expr; tchar ')'
power a]
scalar :: Int -> Parser Expr
scalar i = msum [do tchar '*'; a <- summand
return $ i :* a,
return $ Con i]
122
exprF, summandF, factorF :: Parser (Int -> Int)
exprF
= do a <- summandF; moreSummandsF a
summandF = do a <- factorF; moreFactorsF a
factorF = msum [do x <- token identifier
guard $ x == "x"
powerF id,
do i <- token int
msum [powerF $ const i,
scalarF i],
do tchar '('; a <- expr; tchar ')'; powerF a]
scalarF :: Int -> Parser (Int -> Int)
scalarF i = msum [do tchar '*'; a <- summandF
return $ \x -> i*a(x),
return $ const i]
124
moreSummandsF, moreFactorsF, powerF :: (Int -> Int)
-> Parser (Int -> Int)
moreSummandsF a = msum [do tchar '-'
b <- summandF
moreSummandsF $ \x -> a(x)-b(x),
do as <- some $ do tchar '+'; summandF
moreSummands $ \x -> sum $ map ($x) $ a:as,
return a]
moreFactorsF a = msum [do as <- some $ do tchar '*'; factorF
moreFactorsF $
\x -> product $ map ($x) $ a:as,
return a]
powerF a
= msum [do tchar '^'
i <- token int
return $ \x -> a(x)^i,
return a]
125
Zusammmenhang zwischen den beiden Expr-Parsern und dem Expr-Interpreter
(siehe Datentypen)
∀ str ∈ String :
runP (exprF )(str) = Just(f, str)
!
runP (expr)(str) = Just(e, str) ∧
=⇒ ∃ e ∈ Expr :
∀ i ∈ Int : evalE(e)(const(i)) = f (i).
Testumgebung für Expr-Interpreter, -Compiler und -Parser (Haskell-Code in:
Expr.hs)
compile :: String -> Int -> IO
compile file n = readFileAndDo
where h str = case n of 0 ->
1 ->
2 ->
3 ->
()
file h
act str
act str
act str
act str
where g
siehe Transitionsmonaden
expr showExp
expr showRedExp
expr loop1
expr g
exp = do let code = compileE exp
showCode code
loop2 code
4 -> act str exprF loop3
126
compile file n erwartet Quellcode für ein Expr-Objekt in der Datei file, parsiert es
im Fall n < 4 mit expr und im Fall n = 4 mit exprF.
Im Fall n = 0 wird das von expr berechnete Expr-Objekt exp in die Datei exp.svg gezeichnet. Im Fall n = 1 wird exp vorher reduziert. Im Fall n = 2 wird die Schleife loop1
gestartet, die in jedem Durchlauf eine Variablenbelegung store einliest und evalE auf
exp und sore anwendet. Im Fall n = 3 wird exp mit compileE in Zielcode übersetzt und
dann die Schleife loop2 betreten, die in jedem Durchlauf eine Variablenbelegung store
einliest und mit execute den Zielcode von exp auf store ausführt. Im Fall n = 4 wird
die Schleife loop3 gestartet, die in jedem Durchlauf eine Variablenbelegung store einliest
und die von exprF berechnete Funktion f vom Typ Int → Int auf store(x) anwendet.
Hilfsfunktionen von compile
act :: String -> Parser a -> (a
act str parser loop = case runP
Just
Just
->
-> IO ()) -> IO ()
parser str of
(a,"") -> loop a
(a,rest)
do putStrLn $ "unparsed suffix: "++rest
loop a
_ -> putStrLn "syntax error"
127
showExp :: Expr -> IO ()
showExp exp = do writeFile "exp" $ show exp
drawTerm "exp"
showRedExp :: Expr -> IO ()
showRedExp exp = do writeFile "redexp" $ show $ reduce exp
drawTerm "redexp"
loop1 :: Expr -> IO ()
loop1 exp = do (store,b) <- input
let result = evalE exp store
when b $ do putStrLn $ "result = "++show result
loop1 exp
loop2 :: [StackCom] -> IO ()
loop2 code = do (store,b) <- input
let (result:_,_) = execute code ([],store)
when b $ do putStrLn $ "result = "++show result
loop2 code
128
loop3 :: (Int -> Int) -> IO ()
loop3 f = do (store,b) <- input
when b $ do putStrLn $ "result = "++show (f $ store "x")
loop3 f
Zugriffsoperator für Felder:
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,
nonempty str)
Update-Operator 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.
(//) :: 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.
showCode :: [StackCom] -> IO ()
showCode cs = writeFile "code" $ fold2 f "" [0..length cs-1] cs
where f str n c = str++'\n':replicate (5-length lab) ' '
++lab++": "++show c where lab = show n
a1, . . . , an sind genau die Positionen des Feldes arr, an denen es sich von arr//s unterscheidet.
129
!
131
#
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
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
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)]
130
132
Beispiel Fibonacci-Zahlen
Tripeldarstellung von Alignments
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,1000) fib where fib 0 = 1
fib 1 = 1
fib n = fibA!(n-1) + fibA!(n-2)
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.
133
Beispiel Alignments (Haskell-Code in: Align.hs)
Zwei Listen xs und ys des Typs [String] sollen in die Menge alis(xs, ys) der Alignments
von xs und ys übersetzt werden. Vorausgesetzt wird eine Boolesche Funktion compl, die
für je zwei Strings x und y angibt, ob x und y komplementär zueinander sind und deshalb
aneinander “andocken” können. Das ist auch möglich, wenn x und y übereinstimmen.
Sei A = String 7 {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:
"|xs|+|ys|
alis(xs, ys) =def
n=max(|xs|,|ys|)
{[(a1, b1, c1), . . . , (an, bn, cn)] ∈ (A × A × Color)n |
h(a1 . . . an) = xs ∧ h(b1 . . . bn) = ys ∧
∀ 1 ≤ i ≤ n : (ci = red ∧ compl(ai, bi)) ∨
(ci = green ∧ ai = bi &= Nothing) ∨
(ci = white ∧ ((ai = Nothing ∧ bi &= Nothing)
∨ (bi = Nothing ∧ ai &= 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.
135
matchcount(s) zählt die Vorkommen von red oder green im Alignment s:
matchcount :: Alignment -> Int
matchcount (_,_,cs) = length $ filter (/= white) cs
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)
Zwei Alignments von a c t a c t g c t und a g a t a g
maxima(f )(s) ist die Teilliste aller a ∈ s mit maximalem Wert f (a):
maxima :: Ord b => (a -> b) -> [a] -> [a]
maxima f s = [a | a <- s, f a == maximum (map f s)]
Ein Alignment von a d f a a a a a a und a a a a a a d f a
134
136
selectedAlis xs ys compl berechnet alle gewünschten Alignments von xs und ys:
selectedAlisR :: [String] -> [String] -> (String -> String -> Bool)
-> [Alignment]
selectedAlisR xs ys compl = maxima maxmatch $ align (0,0)
where lg1 = length xs; lg2 = length ys
align = maxima matchcount . f
where f (i,j) = if i == lg1
then if j == lg2 then [[]] else appendy
else if j == lg2 then appendx
else equal++match++appendx++appendy
where x = xs!!i; y = ys!!j; alis = align (i+1,j+1)
equal
= [(Just x,Just y,green):s |
s <- alis, x == y]
match
= [(Just x,Just y,red):s |
s <- alis, compl x y || compl y x]
appendx = map ((Just x,Nothing,white):)
$ align (i+1,j)
appendy = map ((Nothing,Just y,white):)
$ align (i,j+1)
Testumgebung zur Alignment-Erzeugung (Haskell-Code in: Align.hs)
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 zeichnet (siehe Painter.hs).
drawAlis :: String -> Bool -> IO ()
drawAlis file b = readFileAndDo file f
where f str = do writeFile alignFile $ show $ concat
$ zipWithIndices $ alis b
drawLGraph alignFile
where alignFile = file++"Align"
(str1,str2) = break (== '\n') str
(xs,ys) = (words str1,words str2)
compl x y = (x == "a" && y == "t") ||
(x == "c" && y == "g")
genetischer Code
alis True = selectedAlis xs ys compl
alis _
= selectedAlisR xs ys compl
zipWithIndices s = zipWith mkPaths [0..length s-1] s
mkPaths :: Int -> Alignment -> [CLPath]
mkPaths j s = hor 0:hor 1:zipWithIndices ver s3
where (s1,s2,s3) = unzip3 s
hor k = ([p 0 k,p (length s1-1) k],["",""],blue)
ver i c = ([p i 0,p i 1],[str s1,str s2],c)
where str s = case s!!i of Just a -> a; _ -> ""
p i k = (float i,float $ j+j+k)
137
Für zwei Positionen i und j von xs bzw. ys liefert align!(i, j) die Alignments von
drop(i)(xs) bzw. drop(j)(ys) mit maximalem Matchcount.
Wegen der tertiärbaumartigen Rekursion in der Definition von align benötigt align(0, 0)
O(3lgxs+lgys) Rechenschritte. Das nach dem Schema von fibA (s.o.) aus selectedAlisR
gebidete äquivalentes dynamische Programm lautet wie folgt:
selectedAlis :: [String] -> [String] -> (String -> String -> Bool)
-> [Alignment]
selectedAlis xs ys compl = maxima maxmatch $ align!(0,0)
where lg1 = length xs; lg2 = length ys
align = mkArray ((0,0),(lg1,lg2)) $ maxima matchcount . f
where f (i,j) = ... s.o. ...
where x = xs!!i; y = ys!!j; alis = align!(i+1,j+1)
equal
= ... s.o. ...
match
= ... s.o. ...
appendx = map ((Just x,Nothing,white):)
$ align!(i+1,j)
appendy = map ((Nothing,Just y,white):)
$ align!(i,j+1)
138
139
!
#
Das Tai Chi formaler Modellierung
"
konstruktorbasiert
Signaturen
F(A)
Algebra
"
!
A
!
initiale
Algebra
Ini
nat
destruktorbasiert
!'
"
!
A
F(Ini') G(Fin')
!
fold
F(Ini)
!'
fold'
Ini !'-konsistent
<=> fold' mono
Ini'
$
Ini !'-erreichbar
<=> fold' epi
unfold'
Fin !'-observabel
<=> unfold' mono
Fin !'-vollständig
<=> unfold' epi
Quotient Ini/~
~ ist !-Kongruenz
G(A)
unfold
!
Fin'
Coalgebra
Fin
!
finale
Coalgebra
G(Fin)
inc
inv Unterstruktur
ist !-Invariante
140
#
Injektionen ιi : Asi →
i∈I Asi , i ∈ I,
a
0→ (a, i)
#
bilden die Konstruktoren einer Summe i∈I Asi .
$
Projektionen πi : i∈I Asi → Asi , i ∈ I,
(a1, . . . , an) 0→ ai
$
bilden die Destruktoren eines Produktes i∈I Asi .
Allgemein besteht eine Konstruktorsignatur Σ =( BS, RS, C) aus einer Menge BS von
Basissorten, einer Menge RS rekursiver Sorten und einer Menge C von Konstruktoren f : s1 × · · · × sn → s mit s1, . . . , sn ∈ BS ∪ RS und s ∈ RS und Konstanten
c : 1 → s mit s ∈ BS.
Dual dazu besteht eine Destruktorsignatur Σ =( BS, CS, D) aus einer Menge BS von
Basissorten, einer Menge CS corekursiver Sorten und einer Menge D von Destruktoren f : s → s1 + · · · + sn mit s ∈ CS, s ∈ CS und s1, . . . , sn ∈ BS ∪ CS.
Implementierung der Beispiele
Endliche Bäume
data Tree entry
= Join entry (Treelist entry)
data Treelist entry = Nil | Cons (Tree entry) (Treelist entry)
Endliche und unendliche Bäume
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 beiden Projektionen fst und snd eines binären Produkttyps sind Standardfunktionen von
Haskell.
Eine Σ-Algebra bzw. Σ-Coalgebra interpretiert jede Sorte von Σ als eine Menge und jeden
Konstruktor bzw. Destruktor von Σ als eine Funktion des jeweiligen in Σ deklarierten
Typs.
143
141
!
Beispiele
1 steht für die Basissorte, die immer als einelementige Menge interpretiert wird.
Die Konstruktoren
join : entry × treelist → tree
nil : 1 → treelist
cons : tree × treelist → treelist
definieren den Typ der endlichen Bäume mit Knoteneinträgen der Sorte entry.
Die Destruktoren
root : tree → entry
subtrees : tree → treelist
split : treelist → 1 + pair
fst : pair → tree
snd : pair → treelist
Mathematische Semantik funktionaler Programme
"
#
$
Jeder Aufruf eines Haskell-Programms ist ein Ausdruck, der aus Standard- und selbstdefinierten Funktionen zusammengesetzt ist. Demzufolge besteht die Ausführung von
Haskell-Programmen in der Auswertung funktionaler Ausdrücke. Da sowohl Konstanten
als auch Funktionen rekursiv definiert werden können, kann es passieren, dass die Auswertung eines Ausdrucks – 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 für den Fall einer unendlichen Zahl von Schleifendurchläufen definiert
werden. Folglich kann ihr Auftreten in einem Ausdruck dazu führen, dass die Auswertung
des Ausdrucks nicht terminiert.
definieren den Typ der endlichen oder unendlichen (!) Bäume mit Knoteneinträgen der
Sorte entry.
Es kann auch passieren, dass sich ein und derselbe Ausdruck mit einer Strategie in endlicher Zeit auswerten lässt, mit einer anderen jedoch nicht. Ist das Ergebnis der Auswertung
selbst eine Funktion, dann kann es sogar sein, dass beide Strategien in endlicher Zeit ein
(funktionales) Ergebnis liefern, die beiden Ergebnisse jedoch verschiedene Funktionen
darstellen: eine hat evtl. einen größeren Definitionsbereich als die andere.
142
144
Um diese Unterschiede präzise fassen und u.a. Auswertungsstrategien miteinander vergleichen zu können, modelliert man Funktionen oder Relationen als Elemente von Halbverbänden, genauer gesagt, als nur dort existierende Lösungen von Gleichungen.
Wir setzen:
Ein Haskell-Programm besteht i.w. aus solchen Gleichungen. Ist seine Bedeutung durch
eine Lösung der Gleichung gegeben, dann muss der Lösungsvorgang in den Auswertungsprozess integriert werden, weil – wie oben gesagt – das Ausführen funktionaler Programme
im Auswerten von Ausdrücken besteht. Demgegenüber ist das Lösen von Gleichungen und
anderen prädikatenlogischen Formeln in der logischen oder relationalen Programmierung
das eigentliche Berechnungsziel: Die Ausführung eines logisches Programms besteht in
der Suche nach Belegungen der Variablen einer Formel durch Werte, welche die Formel
gültig machen.
Gelöst werden soll die Gleichung s = take 3 requests in der Variablen s. Von ihrer
Bedeutung her sind die im Laufe der Transformation eingeführten Variablen
init = 0; mkRequest = (*2); mkResponse = (+1)
x0,...,x5,s0,...,s5
existenzquantifiziert. Jeder Pfeil –> beschreibt dann eine logische Äquivalenz. Jede Zwischenformel ist ein Goal, d.i. eine Konjunktion von Gleichungen. Jede Gleichung der Form
x = t, wobei x eine Variable und t ein variablenfreier Term (funktionaler Ausdruck) ist,
repräsentiert eine Lösung von x.
Im relationalen Berechnungsmodell ist Programmausführung = Formeltransformation:
Das relationale Berechnungsmodell
Da rekursive Gleichungen Formeln sind, ist das schrittweise Lösen von Formeln durch
Anwendung von Transformationsregeln wie Unifikation, Resolution und Narrowing zwar
ein mögliches, aber doch recht ineffizientes Berechnungsmodell, weil sein allgemeiner prädikatenlogischer Ansatz die Besonderheit rekursiver Gleichungen nicht berücksichtigt und
die Anwendung der genannten Regeln platz- und zeitaufwändig ist, u.a. wegen der bei
vielen Regelanwendungen notwendigen Einführung neuer Variablen.
145
Beispiel client/server
-->
-->
-->
-->
init
client
requests
responses
147
server
-->
-->
-->
-->
-->
-->
-->
requests :: [a]
requests = client init responses
responses :: [b]
responses = server requests
-->
client :: a -> [b] -> [a]
client init responses = init:client (mkRequest response) responses'
where response:responses' = responses
server :: [a] -> [b]
server (request:requests) = mkResponse request:server requests
146
-->
-->
-->
-->
s = take 3 requests
s = take 3 (client 0 responses)
s = take 3 (0:client (x0*2) s0) && x0:s0 = responses
s = 0:take 2 (client (x0*2) s0) && x0:s0 = responses
s = 0:take 2 (x0*2:client (x1*2) s1) && x1:s1 = s0 &&
x0:s0 = responses
s = 0:x0*2:take 1 (client (x1*2) s1) && x1:s1 = s0 && x0:s0 = responses
s = 0:x0*2:take 1 (x1*2:client (x2*2) s2) && x2:s2 = s1 && x1:s1 = s0 &&
x0:s0 = responses
s = 0:x0*2:x1*2:take 0 (client (x2*2) s2) && x2:s2 = s1 && x1:s1 = s0 &&
x0:s0 = responses
s = 0:x0*2:x1*2:[] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = responses
s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = responses
s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = server requests
s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 &&
x0:s0 = server (client 0 responses)
s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 &&
x0:s0 = server (0:client (x3*2) s3) && x3:s3 = responses
s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 &&
x0:s0 = mkResponse 0:server (client (x3*2) s3) && x3:s3 = responses
s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 &&
x0:s0 = 1:server (client (x3*2) s3) && x3:s3 = responses
s = [0,1*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 &&
s0 = server (client (x3*2) s3) && x3:s3 = responses
s = [0,2,x1*2] && x2:s2 = s1 &&
148
x1:s1 = server (client (x3*2) s3) && x3:s3 = responses
--> s = [0,2,x1*2] && x2:s2 = s1 && x1:s1 = server (x3*2:client (x4*2) s4) &&
x4:s4 = s3 && x3:s3 = responses
--> s = [0,2,x1*2] && x2:s2 = s1 &&
x1:s1 = x3*2+1:server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = responses
--> s = [0,2,(x3*2+1)*2] && x2:s2 = s1 &&
s1 = server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = responses
--> s = [0,2,x3*4+2] &&
x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = responses
--> s = [0,2,x3*4+2] &&
x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = server requests
--> s = [0,2,x3*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 &&
x3:s3 = server (client 0 responses)
--> s = [0,2,x3*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 &&
x3:s3 = server (0:client (x5*2) s5) && x5:s5 = s4
--> s = [0,2,x3*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 &&
x3:s3 = 1:server (client (x5*2) s5) && x5:s5 = s4
--> s = [0,2,1*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 &&
s3 = server (client (x5*2) s5) && x5:s5 = s4
--> s = [0,2,6] && x2:s2 = server (client (x4*2) s4) &&
x4:s4 = server (client (x5*2) s5) && x5:s5 = s4
Funktionskonstanten und λ-Abstraktionen sind die einzigen Konstruktoren funktionaler
Objekte. Da sie alle nullstellig sind, bilden sie zusammen mit einzelnen Variablen die
einzigen Patterns für funktionale Objekte.
Sei t ein Term. var(t) bezeichnet die Menge der freien Variablen von t. Eine Substitution
σ bildet Variablen auf Terme ab. dom(σ) bezeichnet die Menge aller Variablen x, die
tatsächlich substituiert werden, für die also σ(x) &= x gilt. σ[t/x] (“σ bis auf: t für x”)
bezeichnet die Substitution, die σ an der Stelle x verändert: x bekommt den Wert t.
!
t
falls x = t
σ[t/x](y) =def
σ(y) sonst
σ wird wie folgt zur Funktion σ ∗ fortgesetzt, die Terme auf Terme abbildet:
σ ∗(x)
= σ(x)
für alle Variablen x,
σ ∗(f (t1, . . . , tn)) = f (σ ∗(t1), . . . , σ ∗(tn)) für alle Funktionskonstanten f
und Terme f (t1, . . . , tn),
σ ∗(λp.e)
= λρ∗(p).τ ∗(ρ∗(e))
für alle λ-Abstraktionen λp.e.
Hierbei sind die Substititionen τ und ρ wie folgt definiert:
τ =def σ[x/x | x ∈ var(p)]
bzw.
ρ(x) =def
!
x3 falls x ∈ var(p) ∩ var(τ (var(e) ∩ dom(τ )))
x sonst
151
149
Das funktionale Berechnungsmodell
Dieses ist auf die Lösung rekursiver Gleichungen der Form
(x1, . . . , xn) = t
(1)
zugeschnitten, wobei x1, . . . , xn verschiedene Variablen sind und t ein Term ist, der zu
einem n-Tupel von Konstanten oder Funktionen beliebiger Ordnung auswertbar ist. An
die Stelle der obigen Formelsequenz tritt eine Termreduktion:
Das Renaming ρ der Variablen von var(p) ∩ var(τ (var(e) ∩ dom(τ ))) verhindert die
Bindung in λp.τ ∗(e) ihrer freien Vorkommen in τ (dom(τ )). Die gleichen Variablen müssen
umbenannt werden, wenn eine quantifizierte Formel instanziiert wird (siehe LV Logik
für Informatiker). x3 muss eine frische Variable sein, die weder in p noch in τ (var(e))
vorkommt.
Wie man leicht nachrechnen kann, gilt für je zwei Substitutionen σ und τ die Gleichung
(τ ∗ ◦ σ)∗ = τ ∗ ◦ σ ∗.
Die Semantik des Pfeils –> ist hier nicht logische Äquivalenz, sondern die Gleichheit der
Werte der Terme links bzw. rechts vom Pfeil.
σ ∗(t) wird Instanz von t genannt. Ein Term u matcht t, falls u eine Instanz von t ist.
Die Matching-Relation ist reflexiv, transitiv und – bis auf das Renaming von Variablen
– antisymmetrisch. Ihre Umkehrung ≤ heißt Subsumptionsordnung:
Notationen und Begriffe zu Termreduktionen
Anstelle von σ (t) schreibt man auch:
take 3 requests –> ... –> [0,2,6]
Funktionen tauchen in Termen als Funktionskonstanten oder λ-Abstraktionen λp.e auf
(siehe Funktionen). Hierbei ist e ein beliebiger Term und p ein Pattern, also ein Term,
der aus Konstruktoren und Variablen besteht, von denen jede höchstens einmal in p
vorkommt.
150
∗
u ≤ t ⇐⇒def
t ist eine Instanz von u.
t[σ(x)/x | x ∈ var(t)],
in Worten: t mit σ(x) für x, wobei x die Variablen von t durchläuft.
152
Jeder Term t2 einer Termreduktion entsteht aus seinem Vorgänger t1 durch Anwendung
einer Reduktionsregel
b
v1 → v2
auf einen Teilterm u1 von t1, falls die – als Boolescher Ausdruck formulierte – Bedingung
b gilt. Dazu muss es eine Substitution σ mit σ ∗(v1) = u1 geben. Falls σ ∗(b) = True
gilt, wird der Redex u1 durch das Redukt u2 = σ ∗(v2) ersetzt. Darüberhinaus gibt es
einen Kontextterm c (mit einer Variablen x), den der Reduktionsschritt nicht verändert:
c[u1/x] = t1 und c[u2/x] = t2. Der gesamte Reduktionsschritt kann demnach wie folgt
zerlegt werden:
t1 = c[σ ∗(v1)/x] → c[σ ∗(v2)/x] = t2.
Um aus Regel und Redex ein eindeutiges Redukt zu erhalten, müssen alle Variablen von
b und v2 bereits in v1 vorkommen.
Die Gleichungen einer Funktionsdefinition können also erst dann in Termreduktionen angewendet werden, wenn sie keine lokalen Definitionen enthalten. Wir betrachten mehrere
Schemata einer Funktionsdefinition und transformieren diese in semantisch äquivalente
Reduktionsregeln.
153
Die übliche (wechselseitig rekursive) Definition einer Menge F totaler (oder zumindest
totalisierbarer) Funktionen besteht aus Gleichungen folgender Form:
(2)
Im Fall, dass alle Variablen des Booleschen Ausdrucks en+1 in p0 vorkommen
(let-Bedingung), ist das folgende Schema zu (2) äquivalent:
f (p0) | en+1 = let p1 = e1
...
pn = en
in en+2
• sind p0, . . . , pn Patterns,
• e1, . . . , en+2 beliebige Terme,
• alle Variablen von p0, . . . , pn paarweise verschieden.
(i)
• kommt für alle 1 ≤ i ≤ n + 1 jede Variable von ei in einem der Terme p0, . . . , pi−1
vor.
(ii)
Die binäre Relation >F auf F sei wie folgt definiert:
!
es gibt eine Gleichung der Form (2) so,
f >F g ⇐⇒def
dass g in einem der Terme e1, . . . , en+2 vorkommt.
Um sicherzustellen, dass die u.g. aus (2) bzw. (2’) gebildeten Reduktionsregeln im Laufe
einer Termreduktion nur endlich oft angewendet werden können, soll es eine wohlfundierte
Termordnung < geben, so dass für alle Gleichungen (2) bzw. (2’) der Definition von F
und alle Teilterme g(p) einer der Terme e1, . . . , en+2
gilt.
(iii)
g >+
F f ⇒ p0 < p
155
Schema 1 einer Funktionsdefinition
f (p0) | en+1 = en+2 where p1 = e1
...
pn = en
Dabei
Kurz gesagt: Entweder wird f in der Definition von g nicht benutzt oder die Argumente
der Aufrufe von g in der Definition von f sind kleiner als das Argument von f auf der
linken Seite der jeweiligen Definitionsgleichung.
Dann lässt sich nämlich aus den Relationen >F und < induktiv eine transitive wohlfundierte Termordnung > mit folgenden Eigenschaften konstruieren:
• Für jeden Reduktionsschritt t → t3 gilt t > t3.
• Für alle Terme t und echten Teilterme u von t gilt t > u.
(23)
154
156
Beispiele für Schema 1
mergesort :: [a] -> [a]
siehe Typklassen
data Btree a = L a | Btree a :# Btree a
rep :: (a -> a -> a) -> Btree a -> a -> (a,Btree a)
rep _ (L x) y
= (x,L y)
rep f (t1:#t2) x = (f y z,u1:#u2) where (y,u1) = rep f t1 x
(z,u2) = rep f t2 x
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)
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)
Um Gleichungen der Form (2) in äquivalente ohne lokale Definitionen zu übersetzen,
müssen wir die gesamte Definition von f betrachten:
f (p10) | e1(n1+1) = e1(n1+2) where p11 = e11; . . . ; p1n1 = e1n1
...
(3)
f (pm0) | em(nm+1) = em(nm+2) where pm1 = em1; . . . ; pmnm = emnm
bzw., falls die let-Bedingung erfüllt ist (s.o),
f (p10) | e1(n1+1) = let p11 = e11; . . . ; p1n1 = e1n1
in e1(n1+2)
...
(33)
f (pm0) | em(nm+1) = let pm1 = em1; . . . ; pmnm = emnm in em(nm+2)
Ist die let-Bedingung erfüllt, dann werden die Gleichungen von (3) bzw. (3’) zu bedingten
Reduktionsregeln:
f (p10)
...
f (pm0)
e1(n +1)
1
→
em(nm +1)
→
represttipsI :: Btree a -> [a] -> [a] -> ([a],Btree a,[a])
represttipsI (L x) (y:s) acc = (x:acc,L y,s)
represttipsI (t1:#t2) s acc = (ls2,u1:#u2,s2)
where (ls1,u1,s1) = represttipsI t1 s acc
(ls2,u2,s2) = represttipsI t2 s1 ls1
represttipsI t s acc = (leaves(t)++acc,t[take(n,s)/leaves(t)],drop(n,s))
where n = length(leaves(t))
δ-Regeln
let pm1 = em1; . . . ; pnm+2 = enm+2 in emnm
159
157
represttips :: Btree a -> [a] -> ([a],Btree a,[a])
represttips (L x) (y:s) = ([x],L y,s)
represttips (t1:#t2) s = (ls1++ls2,u1:#u2,s2)
where (ls1,u1,s1) = represttips t1 s
(ls2,u2,s2) = represttips t2 s1
represttips t s = (leaves(t),t[take(n,s)/leaves(t)],drop(n,s))
where n = length(leaves(t))
let p11 = e11; . . . ; pn1+2 = en1+2 in e1n1
λ-Applikationen
Die let-Ausdrücke werden mit folgender Regel in λ-Applikationen übersetzt:
let p1 = e1; . . . ; pn = en in en+1 → λ ∼p1.(. . . (λ ∼pn.en$en+1) . . . )$e1 δ-Regel
Das Symbol ∼ kennzeichnet ein irrefutibles Pattern. Das Matching irrefutibler Patterns
wird so lange wie möglich hinausgezögert, d.h. eine Applikation der Form (λ ∼ p.e)(t)
kann reduziert werden, auch wenn t von p (noch) nicht gematcht wird (s.u.).
In Haskell kann ∼ jedem Pattern vorangestellt werden. Semantisch unterscheidet sich ∼p
natürlich nur dann von p, wenn p keine Variable ist. Deshalb nennen wir auch Variablen
irrefutible Patterns. Steht an der Position eines Arguments einer Funktion f auf der
linken Seite jeder Definitionsgleichung für f ein irrefutibles Pattern, dann nennt man
dieses Argument von f nichtstrikt und die anderen Argumente von f strikt.
Während alle diese Definitionen die syntaktischen Bedingungen (i)-(iii) erfüllen, wird (iii)
von den ersten vier Konstanten bzw. Funktionen des Client/Server-Beispiels (siehe Das
relationale Berechnungsmodell) verletzt!
158
160
Die Bezeichnung δ-, η- oder β-Regel entstammt dem klassischen λ-Kalkül. Hier sind einige
δ-Regeln, die sich aus der Definition von Standardfunktionen ergeben:
x+0 → x
x∗0 → 0
True && x → x
False && x → False
if True then x else y →
if False then x else y →
πi(x1, . . . , xn) → xi
head(x : xs) → x
tail(x : xs) → xs
Reduktionsregeln zur Auswertung von λ-Applikationen
Seien x, y, x1, . . . , xn, xs Variablen, p, e, e3, e1, . . . , en Terme und σ eine Substitution.
x
y
1≤i≤n
Ist die let-Bedingung nicht erfüllt, dann wird die Übersetzung komplizierter. Die Definition von f muss in m + 1 Funktionsdefinitionen zerlegt werden:
λ ∼(x1, . . . , xn).e$(e1, . . . , en)
λ ∼(x1, . . . , xn).e$e3
λ ∼(x : xs).e$e1 : e2
λ ∼(x : xs).e$e3
λ ∼p.e$e3
→
→
→
→
→
λ ∼p.e$e3
→
λx.e=y$e3
λp.e=y$e3
λp.e=y$e3
→
→
→
e[ei/xi | 1 ≤ i ≤ n]
e[πi$e3/xi | 1 ≤ i ≤ n]
e[e1/x, e2/xs]
e[head$e3/x, tail$e3/xs]
eσ
falls e3 &= (e1, . . . , en)
falls e3 &= e1 : e2
falls pσ = e3 und
p &∈ {(x1, . . . , xn), x : xs}
e[λp.x$e3/x | x ∈ var(p)] falls p &≤ e3 und
p &∈ {(x1, . . . , xn), x : xs}
e[e3/x]
eσ
falls pσ = e3
3
y$e
falls p &≤ e3
und e3 reduziert ist
. bindet stärker als =.
161
163
Termination und Konfluenz
f (x) = case x of
p10
_
f2(x) = case x of p20
-> if e1(n1+1) then e1(n1+2) else f2(x)
where p11 = e11; . . . ; p1n1 = e1n1
-> f2(x)
-> if e2(n2+1) then e2(n2+2) else f3(x)
where p21 = e21; . . . ; p2n2 = e2n2
-> f3(x)
_
...
(4)
fm(x) = case x of pm0 | em(nm+1) -> em(nm+2)
where pm1 = em1; . . . ; pmnm = emnm
(4) wird in Reduktionsregeln übersetzt, die λ-Abstraktionen mit Alternativen der Form
λp1.e1= . . . =λpn.en enthalten:
f (x) → λp10.λ ∼p11.(. . . λ ∼p1n1 .(if e1(n1+1) then e1(n1+2) else f2(x))$e1n1 . . . )$e11=
λ_.f2(x)
f2(x) → λp20.λ ∼p21.(. . . λ ∼p2n2 .(if e2(n2+1) then e2(n2+2) else f3(x))$e2n2 . . . )$e21=
λ_.f3(x)
...
δ-Regeln
enm +1
fm(x) →
λpm0.(λ ∼pm1.(. . . λ ∼pmnm .enm+2$emnm . . . )$em1)$x
162
Man beachte, dass bei der Anwendung dieser Regeln nur die freien Vorkommen von Variablen substituiert werden. Es bedarf deshalb ihrer genauen Analyse, um die Bedingung(en)
zu ermitteln, unter denen jede mit ihnen ausgeführte Termreduktion endlich ist und damit
das Regelsystem terminierend genannt wird.
Für die aus (2) oder (2’) gebildeten δ-Regeln und die meisten λ-Reduktionsregeln folgt die
Termination aus Bedingung (iii). Vorsicht geboten ist jedoch bei der ersten β-Regel. Lässt
man ungetypte Terme zu, dann induziert sie die folgende unendliche Termreduktion: Sei
x eine Variable.
λx.(x$x)$λx.(x$x) → λx.(x$x)$λx.(x$x) →
...
Ein Term der Form e$e ist zwar syntaktisch korrekt, aber sein Typ ist nicht inferierbar.
In einem geeigneten Halbverband hätte e$e zwar einen Wert, aber der liegt nicht in einem
Typ endlicher Ordnung. Reduziert man umgekehrt nur Terme, die einen Typ endlicher
Ordnung haben, dann bleiben Termreduktionen auch bei Anwendung der ersten β-Regel
endlich. Jede solche Anwendung verkleinert dann nämlich die Ordnung des Typs des
Teilterms mit maximaler Typordnung!
164
Ein aus δ-, η- oder β-Regeln gebildetes (und nur auf getypte Terme angewendetes) Regelsystem ist nicht nur terminierend, sondern auch konfluent, d.h. alle vom selben Term
ausgehenden Termreduktionen, bei denen in jedem Schritt auf den jeweils ausgewählten
Redex die erste anwendbare Regel angewendet wird, zu Reduktionen fortgesetzt werden
können, die mit derselben Normalform enden, also demselben Term, auf den keine Regel
mehr anwendbar ist.
Man zeigt zunächst, dass die folgendermaßen induktiv definierte Termordnung =⇒ stark
konfluent ist, d.h. für alle Terme t, u, v mit t =⇒ u und t =⇒ v gibt es einen Term w
mit u =⇒ w und v =⇒ w (Beweis!):
• Für alle Terme t gilt t =⇒ t.
• Alle δ-, η- und β-Schritte gehören zu =⇒.
• Für alle n > 0 und alle n-stelligen Funktionskonstanten f (einschließlich $) gilt:
t1 =⇒ u1 ∧ · · · ∧ tn =⇒ un ⇒ f (t1, . . . , tn) =⇒ f (u1, . . . , un).
• Für alle Patterns p gilt: t =⇒ u ⇒ λp.t =⇒ λp.u.
• Für alle Patterns p1, . . . , pn gilt:
t1 =⇒ u1 ∧ · · · ∧ tn =⇒ un ⇒ λp1.t1= . . . =λpn.tn =⇒ λp1.u1= . . . =λpn.un.
µ-Abstraktionen
Weiterhelfen bei der Suche nach vollständigen Reduktionsstrategien tut jedoch die Tatsache, dass Terme, die unendliche Objekte oder nicht-totalisierbare Funktionen enthalten,
mit Hilfe des µ-Operators ausgewertet werden können: Der µ-Abstraktion
µx1 . . . xn.t
bezeichnet die kleinste Lösung der Gleichung
(x1, . . . , xn) = t
(1)
in der Erweiterung des Produktes zum aufwärtsvollständigen Halbverband, in dem der
Term t interpretiert wird.
Diese Erweiterung einer Menge A besteht zunächst in der Auswahl oder Hinzunahme eines
kleinsten Elementes ⊥ und der Bildung einer flachen Halbordnung: Sei a, b ∈ A∪{⊥}.
a ≤ b ⇐⇒def
a = ⊥ ∨ a = b.
Die flache Halbordnung wird dann auf Funktionen in die Menge A ∪ {⊥} fortgesetzt:
Seien f, g : B → A ∪ {⊥}.
f ≤ g ⇐⇒def
∀ a ∈ A : f (a) ≤ g(a).
165
167
Da jede Termreduktion im transitiven Abschluss von =⇒ liegt, folgt aus der starken
Konfluenz von =⇒ die starke Konfluenz des reflexiv-transitiven Abschlusses der Reduktionsrelation. Die aber liefert sofort die Konfluenz des zugrundeliegenden Regelsystems.
Sind alle in t1, . . . , tn auftretenden Funktionen in dieser oder anderer Weise zu stetigen
Funktionen erweitert worden, dann ist auch
Unter der o.g. Voraussetzung, dass in jedem Schritt auf den jeweils ausgewählten Redex
die erste anwendbare Regel angewendet wird, unterscheiden sich Reduktionsstrategien
nur in der jeweils ausgewählten Redexposition und der Festlegung, wann gewisse Regeltypen anzuwenden sind. Ein nicht-terminierendes Regelsystem kann zwar zu unendlichen
Reduktionen eines bestimmten Term t führen. Das bedeutet aber nicht, dass alle Reduktionen von t unendlich sind. Man sucht daher nach vollständigen Strategien, die jeden
Term, der eine Normalform hat, dort hinführen. Konfluenz (s.o.) hilft hier nicht weiter.
Sie stellt die Eindeutigkeit von Normalformen sicher, nicht aber deren Existenz.
166
Φ : 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
*i∈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 *i∈N Φi(⊥).
168
Schema 2 einer Funktionsdefinition
Schema 3 einer Funktionsdefinition
Betrachten wir nun das Schema der nicht-rekursiven Definition einer Funktion f , deren
lokale Definitionen in einer Gleichung der Form (1) zusammengefasst sind.
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.
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]
δ-Regel
Um die Auswertung einer µ-Abstraktion anzustoßen, wenden wir die folgende Reduktionsregel an, deren Korrektheit sich direkt aus der Interpretation von µx1 . . . xn.t als
Lösung von (1) ergibt:
µx1 . . . xn.t
→
t[πi$µx1 . . . xn.t/xi | 1 ≤ i ≤ n]
Expansionsregel
Man sieht sofort, dass die Anwendung dieser Regel unendlich oft wiederholbar ist. Glücklicherweise ist sie aber die einzige in unserem Regelsystem, die zu unendlichen Termreduktionen führen kann. Ob eine Reduktionsstrategie vollständig ist, also jeden Term, der
eine Normalform hat, dort auch hinführt (s.o.), hängt davon ab, wann und wo sie die
Expansionsregel anwendet.
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
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)
169
Beispiele
(eq)
171
Sei F = {f1, . . . , fk } und eqs ∈ DS(F, ∅).
repBy :: (a -> a -> a) -> Btree a -> Btree a
repBy f t = u where (x,u) = rep f t x
repBy min ((L 3:#(L 22:#L 4)):#(L 2:#L 11))
===> ((2#(2#2))#(2#2))
repBy (+) ((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
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.e11= . . . =p1n1 .e1n1 ,
...
λpk1.ek1= . . . =pknk .eknk )
liefert die Gleichung (f1, . . . , fk ) = µ(eqs) eine zu eqs äquivalente Definition von F .
palI s = b where (r,b) = reveqI s r []
sort, sortI :: Ord a => Btree a -> Btree a
sort t = u where (ls,u,_) = represttips 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,_) = represttipsI t (sort ls) []
170
172
Im Fall (b) seien für alle 1 ≤ i ≤ k
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.).
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
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
µ(eqs) =def µf1 . . . fk .( λp11.σ11(e11)= . . . =p1n1 .σ1n1 (e1n1 ),
...
λpk1.σk1(ek1)= . . . =pknk .σknk (eknk ))
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 aufwärtsvollständigen Halbverband A 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 ≤ . . .
fi(pi1) = ei1 where eqsi1,
...
fi(pini ) = eini where eqsini
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
t0 →RS t1 →RS t2 →RS . . .
Der von RS berechnete Wert von t0 in A wird dann definiert durch:
A
tA
0,RS =def *i∈N ti .
173
175
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:
Diese Definition kann auf Funktionen höherer Ordnung erweitert werden: Sei A ein aufwärtsvollständiger Halbverband mit flacher Halbordnung 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
• β- und δ-Regeln werden stets vor der Expansionsregel angewendet.
(A)
• Die Expansionsregel wird immer parallel auf alle bzgl. der Teiltermordnung maximalen
µ-Abstraktionen angewendet.
(B)
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:
Die lazy-evaluation-Strategie
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.
definierte Funktion tA
RS : F T den von RS berechneten Wert von t in A.
j
A
xA
i,F S = πi (*j∈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 Halbverband mit
flacher Halbordnung interpretiert werden, folgt:
Eine Reduktion t0 →RS t1 →RS t2 →RS . . . terminiert ⇐⇒ tA
0,RS &= ⊥.
174
176
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 &= ⊥ und damit
⊥=
&
tA
k
=
A(⊥)
tk
≤
A(⊥)
*i∈Nti
=
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)
tA
0,RS .
Expansion
→
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 = *i∈N ti
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)))
A(⊥)
Ein i ∈ N mit ti
&= ⊥ würde nämlich zu einem Widerspruch führen: Sei j das kleinste
A(⊥)
i mit ti
&= ⊥. Es gäbe eine aus Funktionen von ti gebildete monotone Funktion f
sowie a1, . . . , am ∈ A mit
A(⊥)
f (a1, . . . , am, ⊥, . . . , ⊥) = tj
&= ⊥.
Aus der Monotonie von f und der Flachheit der Halbordnung des Halbverbandes, 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, ⊥, . . . , ⊥) &= ⊥
A(⊥)
im Widerspruch dazu, dass j das kleinste i mit ti
&= ⊥ 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.)
177
179
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)
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
Die Definition von pal liefert die δ-Regel
pal(s) → π2$µ r b.reveq(s, r).
178
(1)
→
• moves up
→
π2$• ↓ reveq([1, 2, 1], π1 ↑)
π2$• ↓ reveq([1, 2, 1], π1 ↑)
• moves down
→
• moves down
→
• 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.
180
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)
split term
→
π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @)
• A λ ∼(r, b).(r ++[1], 1 = head$tail$π1 ↑ &&b)$ B
C reveq([], tail$tail$π1 ↑)
β−Regel
→
• moves down
→
(3)
→
π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @)
• A (π1 B ++ [1], 1 = head$tail$π1 ↑ &&π2 B)
C reveq([], tail$tail$π1 ↑)
π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @)
A (π1 B ++ [1], 1 = head$tail$π1 ↑ &&π2 B)
• C reveq([], tail$tail$π1 ↑)
π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @)
A (•π1 B ++[1], 1 = head$tail$π1 ↑ && • π2 B)
C ([], True)
δ−Regeln
π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @)
A (•[] ++[1], •1 = head$tail$π1 ↑ &&True)
→
δ−Regeln
→
π2$ ↓ (•π1 @ ++[1], 1 = head$π1 ↑ && • π2 @)
A ([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 ↑)
183
181
Hiermit erhalten wir eine terminierende Reduktion von pal[1, 1], die als Graphtransformation so aussieht: Die Pfeile ↑, ↓, @, A, B und C 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)
→
β−Regel
→
split term
→
β−Regel
→
• moves down
→
(2)
→
β−Regel
→
δ−Regel
→
δ−Regel
→
β−Regeln
→
δ−Regel
→
π2$• ↓ reveq([1, 1], π1 ↑)
π2$• ↓ (λ ∼y : s2.(λ ∼(r, b).(r ++[1], 1 = y&&b)$reveq([1], s2))$π1 ↑
π2$• ↓ λ ∼(r, b).(r ++[1], 1 = head$π1 ↑&&b)$reveq([1], tail$π1 ↑)
π2$• ↓ λ ∼(r, b).(r ++[1], 1 = head$π1 ↑ &&b)$ @
A reveq([1], tail$π1 ↑)
π2$• ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @)
A reveq([1], tail$π1 ↑)
π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @)
• A reveq([1], tail$π1 ↑)
π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @)
• A (λ ∼y : s2.(λ ∼(r, b).(r ++[1], 1 = y&&b)$reveq([], s2))$tail$π1 ↑
π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @)
• A λ ∼(r, b).(r ++[1], 1 = head$tail$π1 ↑&&b)$reveq([], tail$tail$π1 ↑)
182
δ−Regel
→
β−Regel
→
δ−Regel
→
•π2$([1, 1], 1 = head$[1, 1]&&1 = head$tail$[1, 1])
1 = •head$[1, 1]&&1 = head$ • tail$[1, 1]
•1 = 1&&1 = head$[1]
•True&&1 = head$[1]
1 = •head$[1]
•1 = 1
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:
184
pal2[1,1]
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))))))))
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)))))
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]))
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)))))
bool(1 = head[1])
bool(1 = 1)
bool(True)
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)))))))
Number of steps: 19
187
185
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)))))
186
!
#
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 Halbverband 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 kaum in einem Halbverband
mit flacher Halbordnung liegen. Der Konstruktor (:) müsste als monotone Funktion
interpretiert werden, was 1 : ⊥ = ⊥ implizieren würde. Dann wäre aber ⊥ die kleinste
Lösung von ones = 1:ones!
Die Erweiterung zum aufwärtsvollständigen Halbverband muss hier also anders laufen.
Ihr liegt nicht die Menge der unendlichen, sondern der endlichen Objekte zugrunde, die
aus denselben Konstruktoren zusammengesetzt sind wie die unendlichen. Kurz gesagt, die
leere Liste wird zum kleinsten Element, L ≤ L3 gilt genau dann, wenn L ein Präfix von
L3 ist, und eine unendliche Liste wird als Supremum ihrer endlichen Präfixe aufgefasst.
188
Es folgt eine Expander2-Version des Client/Server-Beispiels (siehe Das relationale Berechnungsmodell), mit deren Hilfe wir die oben angekündigte Termreduktion
take 3 requests –> ... –> [0,2,6]
durchführen können:
server(request:requests) == mkResponse(request):server(requests) &
Client2(response) == fun(~(response':responses),
response:Client2(mkRequest(response'))(responses)) &
Requests2 == Client2(init)(Responses2) &
Responses2 == server(Requests2) &
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) &= ⊥ für ein i ∈ N,
(*i∈Nti)(w) =def
⊥
sonst.
In diesem Modell lassen sich alle Σ-Bäume mit unendlichem Definitionsbereich als Suprema von (endlichen) Σ-Termen darstellen.
ReqRes == mu client requests responses.
(fun(response,fun(~(response':responses),
response:client(mkRequest(response'))(responses))),
client(init)(responses),
server(requests)) &
init == 0 & mkRequest == (*2) & mkResponse == (+1) &
ReqRes fasst die Definitionen von Client2, Requests2 und Responses2 zu einer µAbstraktion zusammen. Die Terme take(3,Requests2) und take(3,get1(ReqRes))
sind also äquivalent: Sie werden beide (in 53 bzw. 55 Reduktionsschritten) zu [0,2,6]
reduziert.
189
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 Σ =( BS, RS, C) eine Konstruktorsignatur (siehe Das Tai Chi ...), S = BS ∪ RS
und für alle f : s1 × · · · × sn → s, dom(f ) = s1 × · · · × sn und ran(f ) = s.
191
!
#
Verifikation
"
$
Die folgenden drei Methoden dienen dem Beweis von Eigenschaften der kleinsten bzw.
größten Lösung einer Gleichung der Form
(1)
(x1, . . . , xn) = t.
Die S-sortierte Menge CTΣ der Σ-Bäume besteht aus allen partiellen Funktionen t :
N∗ → C derart, dass t genau dann zu ∈ CTΣ,s gehört, wenn für alle w ∈ N∗ Folgendes
gilt:
• (t(,) ∈ F ∧ ran(t(,)) = s).
• t(w) ∈ F ⇒ ∀ 0 ≤ i < length(n) : (t(wi) ∈ F ∧ ran(t(wi)) = si+1,
wobei dom(t(w)) = s1 . . . sn ∈ S n.
Wir setzen voraus, dass es für alle s ∈ RS eine Konstante ⊥s : , → s in C gibt und
definieren damit eine S-sortierte Halbordnung auf CTΣ: Für alle s ∈ S und t, u ∈ CTΣ,s,
!
∀w ∈ N∗ : t(w) &= ⊥ ⇒ t(w) = u(w) falls s ∈ S \ BS,
t ≤ u ⇐⇒def
t=u
sonst.
190
192
Fixpunktinduktion
Coinduktion
ist anwendbar, wenn es einen aufwärtsvollständigen Halbverband gibt, in dem sich (1)
interpretieren lässt und die Funktionen von t monoton bzw. aufwärtsstetig sind. Die
Korrektheit der Fixpunktinduktion folgt im ersten Fall aus dem Fixpunktsatz von Knaster
und Tarski, im zweiten aus dem Fixpunktsatz von Kleene.
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
Fixpunktinduktion ist durch folgende Beweisregel gegeben:
µx1 . . . xn.t ≤ u
⇑
t[πi(u)/xi | 1 ≤ i ≤ n] ≤ u
• für alle s ∈ RS eine Funktion
ds : s →
(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.
s1 × · · · × sn ,
Σ (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.
195
Berechnungsinduktion
ist anwendbar, wenn es einen aufwärtsvollständigen Halbverband gibt, in dem sich (1)
interpretieren lässt und die Funktionen von t aufwärtsstetig 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 ϕ(*i∈Nai) folgen. Beispielsweise
sind Konjunktionen von Gleichungen oder Ungleichungen zulässig.
Berechnungsinduktion ist durch folgende Beweisregel gegeben:
194
c:s1 ×···×sn →s∈C
deren Interpretation in CTΣ einen Σ-Baum t in seine Wurzel und seine Unterbäume
zerlegt:
193
ϕ(µx1 . . . xn.t)
⇑
ϕ(⊥) ∧ ∀x1, . . . , xn : (ϕ(x1, . . . , xn) ⇒ ϕ(t))
%
(3)
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.
196
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 abwärtsvollständigen Halbverband gibt, in dem sich (1)
interpretieren lässt und die Funktionen von t monoton bzw. abwärtsstetig 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.ϕ)(0x)
⇑
∀0x (ψ ⇒ ϕ[πi(λ0x.ψ)/xi | 1 ≤ i ≤ n](0x))
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.
(6)
197
199
ϕ und ψ sind hier n-Tupel prädikatenlogischer Formeln, x1, . . . , xn Prädikatvariablen und
0x ein Tupel von Individuenvariablen. νx1 . . . xn.ϕ wird interpretiert als das n-Tupel der
größten Relationen, das die logische Äquivalenz
Zum Schluss noch die beiden zur relationalen Coinduktion bzw. Berechnungsinduktion
dualen Regeln:
(7)
-x1, . . . , xn.(0x) ⇐⇒ ϕ(0x)
erfüllt, die der Gleichung (1) entspricht.
Substitution, Implikation und andere aussagenlogische Operatoren werden komponentenweise auf Formeltupel fortgesetzt:
-ϕ1, . . . , ϕn.(0x)
=def (ϕ1(0x), . . . , ϕn(0x)),
(ϕ1, . . . , ϕn) ⇒ (ψ1, . . . , ψn) =def (ϕ1 ⇒ ψ1) ∧ · · · ∧ (ϕn ⇒ ψn)
...
Relationale Fixpunktinduktion
(µx1 . . . xn.ϕ)(0x) ⇒ ψ
⇑
∀0x (ϕ[πi(λ0x.ψ)/xi | 1 ≤ i ≤ n](0x) ⇒ ψ)
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.
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:
198
(11)
200
Berechnungscoinduktion
ist anwendbar, wenn es einen abwärtsvollständigen Halbverband gibt, in dem sich (1)
interpretieren lässt und die Funktionen von t abwärtsstetig 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 ϕ(Gi∈Nai) folgen.
Beispielsweise sind Konjunktionen von Gleichungen oder Ungleichungen zulässig.
Berechnungscoinduktion ist durch folgende Beweisregel gegeben:
ϕ(νx1 . . . xn.t)
⇑
ϕ(H) ∧ ∀x1, . . . , xn : (ϕ(x1, . . . , xn) ⇒ ϕ(t))
(12)
Index
A∗, 14
Σ-Baum, 190
λ-Abstraktion, 11
λ-Applikation, 11
(++), 15
(//), 131
(;), 88
(<-), 88
all, 26
any, 26
Applikationsoperator, 11
Array, 130
array, 130
Attribut, 34
aufwärtsstetig, 69
Anwendungen dieser Regel sind mir nicht bekannt.
Bintree, 61
compileE, 81
#
Haskell-Lehrbücher
"
$
foldr, 25
Marco Block, Adrian Neumann, Haskell-Intensivkurs, Springer 2011
getSubterm, 52
Graph, 71
GraphM, 75
guard, 89
Manuel M. T. Chakravarty, Gabriele C. Keller, Einführung in die Programmierung mit Haskell, Pearson
Studium 2004
Halbverband, 69
head, 15
Kees Doets, Jan van Eijck, The Haskell Road to Logic, Maths and Programming, Texts in Computing
Vol. 4, King’s College 2004
Individuenvariable, 13
init, 15
Instanz eines Terms, 105
Instanz eines Typs, 13
iterate, 20
Ix, 130
Richard Bird, Introduction to Functional Programming using Haskell, Prentice Hall 1998
Richard Bird, Pearls of Functional Algorithm Design, Cambridge University Press 2010
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
Fethi Rabhi, Guy Lapalme, Algorithms: A Functional Programming Approach, Addison-Wesley 1999
Simon Thompson, Haskell: The Craft of Functional Programming, Addison-Wesley 1999
Kellermaschine, 81
Kind, 86
Kompositionsoperator, 11
Konstruktor, 33
last, 16
lines, 32
202
drop, 16
elem, 26
Eq, 54
Expr, 36
expr, 122
exprF, 124
fail, 85
filter, 26
Fixpunkt, 69
Fixpunktsatz von Kleene, 69
flache Halbordnung, 167
flip, 12
fold2, 24
foldl, 23
203
201
!
const, 12
CPO, 69
curry, 12
lookup, 22
lookupM, 93
many, 90
map, 19
mapM, 92
Matrix, 76
mkArray, 130
Monad, 85
MonadPlus, 86
monomorph, 13
monoton, 70
mplus, 86
msum, 90
mzero, 86
newtype, 68
notElem, 26
Parser, 118
polymorph, 13
PTrans, 116
204
range, 130
Read, 62
reduce, 78
repeat, 20
replicate, 20
return, 85
root, 50
sat, 89
sequence, 91
Set, 68
Show, 66
showE, 77
some, 90
splitAt, 17
StackCom, 81
Substitution, 105
subterms, 50
Termunifikation, 107
Trans, 109
Typ zweiter Ordnung, 86
Typkonstruktor, 33
Typvariable, 13
uncurry, 12
unlines, 32
unwords, 32
update, 12
updList, 17
Wörter, 14
when, 89
words, 32
zip, 19
zipWith, 19
zipWithM, 92
tail, 15
take, 16
Term, 50
205
Herunterladen