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