4 Datentypen Modellieren und Implementieren in Haskell fldit-www.cs.tu-dortmund.de/∼peter/Essen.pdf Webseite zur LV: fldit-www.cs.tu-dortmund.de/fpba.html Peter Padawitz TU Dortmund 8. Februar 2011 1 Schema einer Datentypdefinition 33 2 Arithmetische Ausdrücke 77 3 Boolesche Ausdrücke 38 4 Symbolische Differentiation 40 5 Expr-Interpreter 41 6 Hilbertkurven 42 7 Farbkreise 47 5 Termbäume 50 6 Typklassen 54 1 Mengenoperationen auf Listen 55 2 Damenproblem 56 3 Quicksort 59 4 Mergesort 60 5 Binäre Bäume 61 6 Einlesen 62 7 Ausgeben 66 1 ! 3 # Inhalt " $ 1 Vorbemerkung 7 2 Funktionen 9 3 Listen 14 1 Listenteilung 17 2 Listenintervall 18 3 Listenmischung 18 4 Relationsdarstellung von Funktionen 22 5 Listenfaltung 23 6 Listenerzeugende Funktionen 20 7 Listenlogik 26 8 Listenkomprehension 27 9 Länge eines Linienzuges 28 10 Minimierung eines Linienzuges 28 11 Pascalsches Dreieck 30 12 Strings sind Listen von Zeichen 32 2 33 8 Set, ein Datentyp für Mengen 68 9 Kleinste und größte Fixpunkte 69 10 Reflexiver und transitiver Abschluss 74 7 Rund um den Datentyp Expr 77 1 Arithmetische Ausdrücke ausgeben 77 2 Arithmetische Ausdrücke reduzieren 78 3 Arithmetische Ausdrücke in Assemblerprogramme übersetzen 81 8 Monaden 85 1 do-Notation 88 2 Monaden-Kombinatoren 89 3 Die Identitätsmonade 94 4 Die Maybe-Monade 96 5 Die Listenmonade 99 6 Tiefen- und Breitensuche in Bäumen 103 7 Eine Termmonade 105 9 Transitionsmonaden 108 1 Trans, eine Monade totaler Transitionsfunktionen 4 109 2 Die IO-Monade 109 3 IOstore, eine selbstdefinierte Trans-Instanz 114 4 PTrans, eine Monade partieller Transitionsfunktionen 116 10 Monadische Parser 118 1 Monadische Scanner 119 2 Bintree-Parser 121 3 Expr-Parser 122 4 Auswertender Expr-Parser 124 5 Testumgebung für Expr-Interpreter, -Compiler und -Parser 126 11 Felder 130 1 Ix, die Typklasse für Indexmengen 130 2 Dynamische Programmierung 132 3 Alignments 134 12 Das Tai Chi formaler Modellierung* 140 13 Mathematische Semantik funktionaler Programme* 144 1 Das relationale Berechnungsmodell 145 2 Das funktionale Berechnungsmodell 150 3 Schema 1 einer Funktionsdefinition 154 4 λ-Applikationen 160 5 Termination und Konfluenz 164 6 µ-Abstraktionen 167 7 Schema 2 einer Funktionsdefinition 169 8 Schema 3 einer Funktionsdefinition 171 9 Die lazy-evaluation-Strategie 174 180 14 Unendliche Objekte* 188 15 Verifikation* 192 16 Haskell-Lehrbücher 202 17 Index 6 # Vorbemerkung " $ Die Inhalte der gesternten Kapitel werden in der Bachelor-LV Funktionale Programmierung nicht behandelt. Vielmehr sind sie Gegenstand meiner Wahlveranstaltungen Funktionales und regelbasiertes Programmieren (Master und Diplom) bzw. Einführung in den logisch-algebraischen Systementwurf (Bachelor und Diplom) und Logisch-algebraischer Systementwurf (Master und Diplom). HörerInnen der LV Funktionale Programmierung rate ich dringend, diese Folien zum Vor(!) und Nacharbeiten von Vorlesungen zu verwenden, nicht jedoch zu glauben, dass sie Vorlesungsbesuche ersetzen können. Der Einstieg in die funktionale Programmierung ist für jeden mit anderen Problemen verbunden: C- oder Java-Programmierer sollten ihnen geläufige Begriffe wie Variable, Zuweisung oder Prozedur erstmal komplett vergessen und sich von Beginn an auf das Einüben der i.w. algebraischen Begriffe, die funktionalen Daten- und Programmstrukturen zugrundeliegen, konzentrieren. Erfahrungsgemäß bereiten diese mathematisch geschulten und von Java, etc. weniger verdorbenen HörerInnen weniger Schwierigkeiten. Ihr Einsatz in programmiersprachlichen Lösungen algorithmischer Probleme aus ganz unterschiedlichen Anwendungsbereichen ist aber auch für diese Hörerschaft vorwiegend Neuland. 7 5 10 Auswertung durch Graphreduktion ! Diese Folien bilden daher i.w. eine Sammlung prototypischer Programmbeispiele, auf die, falls sie eingehend studiert und verstanden worden sind, zurückgegriffen werden kann, wenn später ein dem jeweiligen Beispiel ähnliches Problem funktionalsprachlich gelöst werden soll. Natürlich werden wichtige Haskell-Konstrukte auch allgemein definiert. Vollständige formale Definitionen, z.B. in Form von Grammatiken, finden sich hier jedoch nicht. Dazu wie auch zur allgemeinen Motivation für einzelne Sprachkonstrukte sei auf die zunehmende Zahl an Lehrbüchern, Tutorials und Sprachreports verwiesen (siehe Haskell-Lehrbücher und fldit-www.cs.tu-dortmund.de/fpba.html). Alle Hilfsfunktionen und -datentypen, die in den Beispielen verwendet werden, sind hier auch – manchmal in früheren Abschnitten – eingeführt. Wenn das zum Verständnis nicht ausreicht und auftretende Fragen nicht in angemessener Zeit durch Zugriff auf andere o.g. Quellen geklärt werden können, dann stellt die Fragen in der Übung oder auch in der Vorlesung! Interne Links sind an ihrer braunen Färbung erkennbar. Jede Kapitelüberschrift ist mit dem Inhaltsverzeichnis verlinkt. Namen von Haskell-Modulen sind mit den jeweiligen Programmdateien verknüpft. 8 ! # Funktionen " $ Neben • Produkten A1 × . . . × An (Haskell-Notation: (A1,...,An)) und • Summen A1 + · · · + An (werden in Haskell als Datentypen implementiert; s.u.) werden in Haskell auch Funktionen als Objekte betrachtet und u.a. als λ-Abstraktionen \p -> e (mathematisch: λp.e) dargestellt. p ist ein Muster für die möglichen Argumente der Funktion und e ein Ausdruck, der die Funktionswerte beschreibt und i.d.R. Variablen enthält, die in p vorkommen. Eine Funktion f wird mit Hilfe von Gleichungen definiert: f p = e (applikative Definition) ist äquivalent zu f = \p -> e Funktionen, die andere Funktionen als Argumente oder Werte haben, heißen Funktionen höherer Ordnung. Der Typkonstruktor -> ist rechtsassoziativ. Also ist die Deklaration (+) :: Int -> (Int -> Int) äquivalent zu (+) :: Int -> Int -> Int Die Applikation einer Funktion ist linksassoziativ. Also ist der Aufruf ((+) 5) 6 äquivalent zu Der Ausdruck, der die Anwendung einer λ-Abstraktion \p -> e auf ein Argument repräsentiert, heißt λ-Applikation, z.B. (\p -> e)(e’). Die Applikation führt nur dann zu einem Ergebnis, wenn der Ausdruck e’ das Muster p trifft (matcht). Der Applikationsoperator ($) :: (a -> b) -> a -> b f $ a = f a führt die Anwendung einer gegebenen Funktion auf ein gegebenes Argument durch. Sie ist rechtsassoziativ und hat unter allen Operationen die niedrigste Priorität. Daher kann durch Benutzung von $ manch schließende Klammer vermieden werden: f1 $ f2 $ ... $ fn a Demgegenüber ist der Kompositionsoperator (.) :: (b -> c) -> (a -> b) -> a -> c (g . f) a = g (f a) zwar auch rechtsassoziativ, hat aber - nach den Präfixoperationen - die höchste Priorität. (f1 . f2 . ... . fn) a (+) 5 6 f1 (f2 (...(fn a)...))) ! f1 (f2 (...(fn a)...))) ! 9 11 (+) ist als Präfixfunktion definiert, kann aber auch infix verwendet werden. Dann entfallen die runden Klammern: U.a. benutzt man den Kompositionsoperator, um in einer applikativen Definition Argumentvariablen einzusparen: 5 + 6 Eine Funktion eines Typs a -> b -> c, deren Name mit einem Buchstaben beginnt, kann ebenfalls infix verwendet werden, muss dann aber in Hochkommas eingeschlossen werden. Beispiel: mod :: Int -> Int -> Int mod 11 5 ist äquivalent zu 11 `mod` 5 Die Infixnotation wird auch verwendet, um die in einer Funktion eines Typs a -> b -> c enthaltenen Sektionen (Teilfunktionen) des Typs a -> c bzw. b -> c zu benennen. Z.B. sind die Ausdrücke (+ 5) (+) 5 (11 +) mod 5 (`mod` 5) (11 `mod`) Funktionen des Typs Int -> Int. Hier sind alle angegebenen Klammern notwendig! 10 f a b = g (h a) b ist äquivalent zu f a = g $ h a ist äquivalent zu f = g . h f a b = g $ h a b ist äquivalent zu f a = g . h a ist äquivalent zu f = (g.).h Weitere nützliche Funktionsgeneratoren, -transformatoren und kombinatoren: const :: a -> b -> a const a _ = a konstante Funktion update :: Eq a => (a -> b) -> a -> b -> a -> b update f a b a' = if a == a' then b else f a' Funktionsupdate flip :: (a -> b -> c) -> b -> a -> c flip f b a = f a b Vertauschung der Argumente curry :: ((a,b) -> c) -> a -> b -> c curry f a b = f (a,b) Kaskadierung (Currying) uncurry :: (a -> b -> c) -> (a,b) -> c uncurry f (a,b) = f a b Dekaskadierung 12 (***) :: (a -> b) -> (a -> c) -> a -> (b,c) (f *** g) a = (f a,g a) Funktionsprodukt (&&&), (|||) :: (a -> Bool) -> (a -> Bool) -> a -> Bool (f &&& g) a = f a && g a Lifting von && (f ||| g) a = f a || g a Lifting von || Innerhalb von Typen (die immer Mengen bezeichnen!) bezeichnen Wörter, die mit einem Kleinbuchstaben beginnen, Typvariablen, während Typnamen wie Bool und Int immer mit einem Großbuchstaben beginnen. Ein Typ mit Typvariablen wie z.B. a -> b -> c heißt polymorph. Enthält er keine Typvariablen, dann ist er monomorph. Eine Funktion mit poly/monomorphem Typ heißt poly/monomorph. Ein Typ t heißt Instanz eines Typs u, wenn man t durch Ersetzung der Typvariablen aus u erhält. Individuenvariablen können durch Elemente eines Typs ersetzt werden. Man erkennt sie ebenfalls daran, dass sie mit einem Kleinbuchstaben beginnen. Eine besondere Individuenvariable ist der Unterstrich _. Er darf nur auf der linken Seite einer Funktionsdefinition verwendet werden und wird dort für ein Argument benutzt, von dem die jeweils definierte Funktion nicht abhängt. length :: [a] -> [a] length (_:s) = length s+1 length _ = 0 length [3,2,8,4] ! 4 head :: [a] -> a head (a:_) = a head [3,2,8,4] ! 3 tail :: [a] -> [a] tail (_:s) = s tail [3,2,8,4] ! [2,8,4] (++) :: [a] -> [a] -> [a] (a:s)++s' = a:(s++s') _++s = s [3,2,4]++[8,4,5] ! [3,2,8,4,5] (!!) :: [a] -> Int -> a (a:_)!!0 = a (_:s)!!n | n > 0 = s!!(n-1) [3,2,4]!!1 ! 2 init :: [a] -> [a] init [_] = [] init (a:s) = a:init s init [3,2,8,4] ! [3,2,8] 15 13 ! # Listen " $ Sei A eine Menge. Wörter über A sind Ausdrücke der Form a1 . . . an mit a1, . . . , an ∈ A. Sie werden in Haskell als Listen implementiert: a1 . . . an wird zu [a1, . . . , an]. Die Menge der Wörter über A wird in der Mathematik mit A∗ bezeichnet. Der entsprechende Haskell-Typ der Listen mit Elementtyp(variable) a lautet [a]. Haskell führt die extensionale Darstellung [a1, . . . , an] auf ihre Konstruktordarstellung zurück: [a1, . . . , an] ! a1 : (a2 : (. . . (an : []) . . . )) Hierbei sind [] und (:) Konstruktoren genannte Objekte des Typs [a] bzw. a -> [a] -> [a], welche die leere Liste bzw. die Funktion, die ein Element vorn an einen Liste anfügt, bezeichnen. Da sie sich aus dem Typ von (:) ergibt, ist die Klammerung in der Konstruktordastellung überflüssig. Die durch mehrere Gleichungen ausgedrückten Fallunterscheidungen bei den folgenden Definitionen von Funktionen auf Listen ergeben sich aus verschiedenen Mustern der Funktionsargumente: Seien x, y, s Individuenvariablen. Dann ist s ein Muster für alle Listen, [] das Muster für die leere Liste, [x] ein Muster für alle einelementigen Listen, x : s ein Muster für alle nichtleeren Listen, x : y : s ein Muster für alle mindestens zweielementigen Listen, usw. 14 last :: [a] -> a last [a] = a last (_:s) = last s last [3,2,8,4] ! 4 take take take take :: Int -> [a] -> [a] 0 _ = [] n (a:s) | n > 0 = a:take (n-1) s _ [] = [] take 3 [3,2,4,8,4,5] ! [3,2,4] drop drop drop drop :: Int -> [a] -> [a] 0 s = s n (_:s) | n > 0 = drop (n-1) s _ [] = [] drop 4 [3,2,4,8,4,5] ! [4,5] takeWhile :: (a -> Bool) -> [a] -> [a] takeWhile f (a:s) = if f a then a:takeWhile f s else [] takeWhile f _ = [] takeWhile (<4) [3,2,8,4] ! [3,2] dropWhile :: (a -> Bool) -> [a] -> [a] dropWhile f s@(a:s') = if f a then dropWhile f s' else s dropWhile f _ = [] dropWhile (<4) [3,2,8,4] ! [8,4] 16 updList :: [a] -> Int -> a -> [a] updList [3,2,8,4] 2 9 ! [3,2,9,4] updList s i a = take i s++a:drop (i+1) s Beispiel Listenteilung (beim n-ten Element bzw. beim ersten Element, das f nicht erfüllt) splitAt splitAt splitAt splitAt :: Int -> [a] -> ([a],[a]) 0 s = ([],s) _ [] = ([],[]) n (a:s) | n > 0 = (a:s1,s2) where (s1,s2) = splitAt (n-1) s span :: (a -> Bool) -> [a] -> ([a],[a]) span f s@(a:s') = if f a then (a:s1,s2) else ([],s) where (s1,s2) = span f s' span _ [] = ([],[]) map-Funktionen map :: (a -> b) -> [a] -> [b] map f (a:s) = f a:map f s map _ _ = [] map (+1) [3,2,8,4] ! [4,3,9,5] map ($ 7) [(+1),(+2),(*5)] ! [8,9,35] map ($ a) [f1,f2,...,fn] ! [f1 a,f2 a,...,fn a] zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] zipWith f (a:s) (b:s') = f a b:zipWith f s s' zipWith _ _ _ = [] zipWith (+) [3,2,8,4] [8,9,35] ! [11,11,43] zip :: [a] -> [b] -> [(a,b)] zip = zipWith $ \a b -> (a,b) zip [3,2,8,4] [8,9,35] ! [(3,8),(2,9),8,35)] 17 19 Beispiel Listenintervall (Teilliste vom i-ten bis zum j-ten Element) sublist sublist sublist sublist sublist :: [a] -> Int -> Int -> [a] (a:_) 0 0 = (a:s) 0 j | j > 0 = (_:s) i j | i > 0 && j > 0 = _ _ _ = Unendliche Listen erzeugende Funktionen Standardfunktionen: [a] a:sublist s 0 $ j-1 sublist s (i-1) $ j-1 [] Beispiel Mischung zweier Listen Sind s1 und s2 zwei geordnete Listen mit eindeutigen Vorkommen ihrer jeweiligen Elemente, dann ist auch merge(s1, s2) eine solche Liste. merge :: [Int] -> [Int] -> [Int] merge s1@(x:s2) s3@(y:s4) | x < y = x:merge s2 s3 | x > y = y:merge s1 s4 | True = merge s1 s4 merge [] s = s merge s _ = s 18 repeat :: a -> [a] repeat a = a:repeat a repeat 5 replicate :: Int -> a -> [a] replicate n a = take n $ repeat a replicate 4 5 iterate :: (a -> a) -> a -> [a] iterate f a = a:iterate f $ f a iterate (+1) 5 ! take 4 $ iterate (+1) 5 ! 5:5:5:5:... ! ! [5,5,5,5] 5:6:7:8:... [5,6,7,8] Beispiele aus Datei der Lazy.hs): blink :: [Int] blink = 0:1:blink blink nats :: Int -> [Int] nats n = n:map (+1) (nats n) nats 3 20 ! ! 0:1:0:1:... 3:4:5:6:... fibs :: Int -> [Int] fibs = 1:tailfibs where tailfibs = 1:zipWith (+) fibs tailfibs take 11 fibs ! Beschränkte Iterationen (for-Schleifen) entsprechen meistens Listenfaltungen. Faltung einer Liste von links her f f primes :: [Int] primes = sieve $ nats 2 f sieve :: [Int] -> [Int] sieve (p:s) = p:sieve [n | n <- s, n `mod` p /= 0] take 11 prims ! [2,3,5,7,11,13,17,19,23,29,31] hamming :: [Int] hamming = 1:map (*2) hamming `merge` map (*3) hamming `merge` map (*5) hamming take 30 hamming ! f f [1,1,2,3,5,8,13,21,34,55,89] [1,2,3,4,5,6,8,9,10,12,15,16,18,20,24,25,27, 30,32,36,40,45,48,50,54,60,64,72,75,80] a b1 b2 b4 foldl :: (a -> b -> a) -> a -> [b] -> a foldl f a (b:s) = foldl f (f a b) s foldl _ a _ = a b5 a ist Zustand, b ist Eingabe f ist Zustandsüberführung foldl1 :: (a -> a -> a) -> [a] -> a foldl1 f (a:s) = foldl f a s sum and minimum concat = = = = foldl (+) 0 foldl (&&) True foldl1 min foldl (++) [] product or maximum concatMap f 21 Relationsdarstellung von Funktionen b3 = = = = foldl (*) 1 foldl (||) False foldl1 max concat . map f 23 Parallele Faltung zweier Listen von links her Funktionen lassen sich auch als Liste ihrer (Argument,Wert)-Paare implementieren. Eine Funktionsanwendung wird als Listenzugriff mit Hilfe der Standardfunktion lookup implementiert, ein Funktionsupdate als Listenupdate mit updRel: f f f f f type Relation a b = [(a,b)] lookup :: Relation a b -> a -> Maybe b lookup ((a,b):r) c = if a == c then Just b else lookup r c lookup _ _ = Nothing updRel :: Relation a b -> a -> b -> Relation a b updRel ((a,b):r) c d = if a == c then (a,d):r else (a,b):updRel r c d updRel _ a b = [(a,b)] Der weiter unten näher behandelte Datentyp Maybe b mit der Typvariablen b enthält außer den – in den Konstruktor Just eingebetteten – Elementen der a zugeordneten Menge das Element Nothing. Eine Funktion f :: a -> Maybe b ist insofern partiell, als die Gleichung f(a) = Nothing als Nichtdefiniertheit von f an der Stelle a interpretiert wird. 22 a b1 c1 b2 c2 b3 c3 b4 c4 b5 c5 fold2 :: (a -> b -> c -> a) -> a -> [b] -> [c] -> a fold2 f a (b:s) (c:s') = fold2 f (f a b c) s s' fold2 _ a _ _ = a listsToFun :: Eq a => b -> [a] -> [b] -> a -> b listsToFun = fold2 update . const Beginnend mit const b, erzeugt listsToFun b schrittweise aus einer Argumentliste as und einer Werteliste bs die entsprechende Funktion: ! bs!!i falls i = max{k | as!!k = a}, listsToFun b as bs a = b sonst. 24 Jeder Aufruf von map, zipWith, filter oder einer Komposition dieser Funktionen entspricht einer Listenkomprehension: Faltung einer Liste von rechts her f f f f f b1 b2 b3 b4 b5 a map f s = [f a | a <- s] zipWith f s s' = [f a b | (a,b) <- zip s s'] Beachte : zip s s' = & [(a,b) | a <- s, b <- s'] = kartesisches Produkt von s und s' filter f s = [a | a <- s, f a] allgemein: foldr :: (b -> a -> a) -> a -> [b] -> a foldr f a (b:s) = f b $ foldr f a s foldr _ a _ = a Beispiel Horner-Schema zur Berechnung von Polynomwerten Der Wert von b0 + b1 ∗ x + b2 ∗ x2 + · · · + bn−1 ∗ xn−1 + bn ∗ xn ist das Ergebnis der folgenden Faltung: b0 + (b1 + (b2 + · · · + (bn−1 + bn ∗ x) ∗ x . . . ) ∗ x) ∗ x horner :: [Float] -> Float -> Float horner bs x = foldr f (last bs) (init bs) where f b a = b+a*x [e(x1, . . . , xn) | x1 ← s1, . . . , xn ← sn, be(x1, . . . , xn)] :: [a] • x1, . . . , xn sind Variablen, • s1, . . . , sn sind Listen, • e(x1, . . . , xn) ist ein Ausdruck des Typs a, • xi ← si heißt generator und steht für xi ∈ si, • be(x1, . . . , xn) heißt guard und ist ein Boolescher Ausdruck. Mit der Listenkomprehension lassen sich u.a. Relationen (= Teilmengen eines kartesischen Produktes) definieren, sofern die die Komponentenmengen als Listen gegeben sind, z.B. die Menge aller Tripel (a, b, c) ∈ A1 × A2 × A3, die p : A1 × A2 × A3 → Bool erfüllen: [(a,b,c) | a <- a1, b <- a2, c <- a3, p(a,b,c)]. 27 25 Listenlogik Beispiel Länge eines Linienzuges any :: (a -> Bool) -> [a] -> Bool any f = or . map f any (>4) [3,2,8,4] ! True type Point = (Float,Float) type Path = [Point] all :: (a -> Bool) -> [a] -> Bool all f = and . map f all (>2) [3,2,8,4] ! False length :: Path -> Float length ps = sum $ zipWith distance ps $ tail ps elem :: a -> [a] -> Bool elem a = any (a ==) elem 2 [3,2,8,4] ! True distance :: Point -> Point -> Float distance (x1,y1) (x2,y2) = sqrt $ (x2-x1)^2+(y2-y1)^2 notElem :: a -> [a] -> Bool notElem a = all (a /=) notElem 9 [3,2,8,4] ! True Beispiel Minimierung eines Linienzuges filter :: (a -> Bool) -> [a] -> [a] filter f (a:s) = if f a then a:filter f s else filter f s filter f _ = [] filter (<8) [3,2,8,4] ! [3,2,4] 26 minimize :: Path -> Path minimize (p:ps@(q:r:s)) | straight p q r = minimize $ p:r:s | True = p:minimize ps minimize ps = ps 28 straight p q r prüft, ob die Punkte p, q und r auf einer Geraden liegen: straight :: Point -> Point -> Point -> Bool straight (x1,y1) (x2,y2) (x3,y3) = x1 == x2 && x2 == x3 || x1 /= x2 && x2 /= x3 && (y2-y1)/(x2-x1) == (y3-y2)/(x3-x2) Soll der Linienzug geglättet werden, dann ist es ratsam, ihn vorher zu minimieren, da sonst unerwünschte Plateaus in der resultierenden Kurve verbleiben: Die Binomialfunktion hat dieselben Eigenschaften – modulo der Bijektivität zwischen den Typen Int -> [Int] und Int -> Int -> Int von pascal bzw. binom: binom :: Int -> Int -> Int binom n k = product[k+1..n]`div`product[1..n-k] = n! k!(n − k)! Da die Lösung von (1) und (2) in pascal (als Funktionsvariable) eindeutig ist, gilt für alle n ∈ N und k ≤ n: pascal(n)!!k = binom(n)(k). Die obige Darstellung des Pascalschen Dreiecks wurde übrigens mit Expander2 erzeugt durch Auswertung des Ausdrucks shelf 1$one 1:map(pasc)[1..10] und die graphische Interpretation des Ergebnisses, wobei one x = turtle[rect(15,11),text x] pasc n = shelf(x+1)$map(one)$pascal n Zwei geglättete Linienzüge links vor und rechts nach der Minimierung 31 29 Beispiel Pascalsches Dreieck stellt eine Aufzählung der Binomialkoeffizienten dar. Strings sind Listen von Zeichen Strings werden als Listen von Zeichen betrachtet, d.h. die Typen String und [Char] sind identisch: "Hallo" == ['H','a','l','l','o'] = True Alle Listenfunktionen sind daher auch für Strings verwendbar. words :: String -> [String] und unwords :: [String] -> String zerlegen bzw. konkatenieren Strings, wobei Leerzeichen, Zeilenumbrüche ('\n') und Tabulatoren ('\t') als Trennsymbole fungieren. unwords fügt Leerzeichen zwischen die zu konkatenierenden Strings. pascal :: Int -> [Int] pascal 0 = [1] pascal n = zipWith (+) (s++[0]) (0:s) where s = pascal $ n-1 lines :: String -> [String] und unlines :: [String] -> String zerlegen bzw. konkatenieren Strings, wobei nur Zeilenumbrüche als Trennsymbole fungieren. unlines fügt '\n' zwischen die zu konkatenierenden Strings. pascal(n)!!k ist die Anzahl der k-elementigen Teilmengen einer n-elementigen Menge. Eigenschaften: Für alle n ∈ N, pascal(n)!!0 = pascal(n)!!n = 1. (1) Für alle n, k > 0, pascal(n)!!k = pascal(n − 1)!!(k − 1) + pascal(n − 1)!!k. (2) 30 32 ! # Datentypen " Zunächst das allgemeine Schema einer Datentypdefinition: $ data DT a1 ... am = Constructor_1 e_11 ... e_1n_1 | ... | Constructor_k e_k1 ... e_kn_k e_11,...,e_kn_k sind beliebige Typausdrücke, die aus Typkonstanten (z.B. Int), Typvariablen a1,...,am und Typkonstruktoren (Produktbildung, Listenbildung, Datentypen) zusammengesetzt sind. Kommt DT selbst in einem dieser Typausdrücke vor, dann spricht man von einem rekursiven Datentyp. Die Elemente von DT a1 ... am sind alle funktionalen Ausdrücke, die aus den ObjektKonstruktoren Constructor_1,...,Constructor_k und Elementen von Instanzen von a1,...,am zusammengesetzt sind. Als Funktion hat Constructor_i den Typ e_i1 -> ... -> e_in_i -> DT a1 ... am. Die Objektdefinition obj = Constructor_i t1 ... tn_i ist äquivalent zu obj = Constructor_i {attribute_i1 = t1, ..., attribute_in_i = tn_i} Attribute dürfen nicht rekursiv definiert werden. Folglich deutet Haskell das Vorkommen von attribute_ij auf der rechten Seite einer Definitionsgleichung als eine vom gleichnamigen Attribut verschiedene Funktion und sucht nach deren Definition. Diese Tatsache kann man nutzen, um attribute_ij doch rekursiv zu definieren: An die obige Objektdefinition wird einfach die Zeile where attribute_ij = tj angefügt. Derselbe Konstruktor darf nicht zu mehreren Datentypen gehören. Dasselbe Attribut darf nicht zu mehreren Konstruktoren gehören. Die Werte von Attributen eines Objektes können wie folgt verändert werden: obj' = obj {attribute_ij = t, attribute_ik = t', ...} 35 33 Ein Konstruktor ist zwar eine Funktion. Er verändern aber seine Argumente nicht, sondern fasst sie nur zusammen. Den Zugriff auf ein einzelnes Argument erreicht man durch Einführung von Attributen, eines für jede Argumentposition des Konstruktors. In der Definition von DT wird Constructor_i e_i1 ... e_in_i ersetzt durch Constructor_i {attribute_i1 :: e_i1, ..., attribute_in_i :: e_in_i}. Z.B. lassen sich die aus anderen Programmiersprachen bekannten Recordtypen und Objekt-Klassen als Datentypen mit genau einem Konstruktor, aber mehreren Attributen implementieren. Wie ein Konstruktor, so ist auch ein Attribut eine Funktion. Als solche hat attribute_ij den Typ attribute_ij :: DT a1 ... am -> e_ij Attribute sind also invers zu Konstruktoren. Man nennt sie deshalb auch Destruktoren. attribute_ij (Constructor_i t1 ... tn_i) 34 hat den Wert tj. Beispiel Arithmetische Ausdrücke (Haskell-Code in: Expr.hs) data Expr = Con Int | Var String | Sum [Expr] | Prod [Expr] | Expr :- Expr | Int :* Expr | Expr :^ Int oder (mit der Option -fglasgow-exts beim Aufruf des ghc-Interpreters): data Expr where Con Var Sum Prod (:-) (:*) (:^) :: :: :: :: :: :: :: Int -> Expr String -> Expr [Expr] -> Expr [Expr] -> Expr Expr -> Expr -> Expr Int -> Expr -> Expr Expr -> Int -> Expr Z.B. lautet der Ausdruck 5*11+6*12+x*y*z als Objekt vom Typ Expr folgendermaßen: Sum [5 :* Con 11,6 :* Con 12,Prod [Var "x",Var "y",Var "z"]] 36 Beispiel Arithmetische, Boolesche und bedingte Ausdrücke, Paare und Listen von Ausdrücken, ... data Exp a where Con Var Sum Prod (:-) (:*) (:^) True_ False_ Or And Not (:<) (:=) (:<=) If Pair List :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: :: Int -> Exp Int String -> Exp a [Exp Int] -> Exp Int [Exp Int] -> Exp Int Exp Int -> Exp Int -> Exp Int Int -> Exp Int -> Exp Int Exp Int -> Int -> Exp Int Exp Bool Exp Bool [Exp Bool] -> Exp Bool [Exp Bool] -> Exp Bool Exp Bool -> Exp Bool Exp Int -> Exp Int -> Exp Bool Exp Int -> Exp Int -> Exp Bool Exp Int -> Exp Int -> Exp Bool Exp Bool -> Exp a -> Exp a -> Exp a Exp a -> Exp b -> Exp (a,b) [Exp a] -> Exp [a] 39 37 Beispiel Boolesche Ausdrücke data BExpr = True_ | False_ | BVar String | Or [BExpr] | And [BExpr] | Not BExpr | Expr :< Expr | Expr := Expr | Expr :<= Expr oder data BExpr where True_ False_ BVar Or And Not (:<) (:=) (:<=) :: :: :: :: :: :: :: :: :: BExpr BExpr String -> BExpr [BExpr] -> BExpr [BExpr] -> BExpr BExpr -> BExpr Expr -> Expr -> BExpr Expr -> Expr -> BExpr Expr -> Expr -> BExpr 38 Funktionen mit Argumenten eines Datentyps werden in Abhängigkeit vom jeweiligen Muster (pattern) der Argumente definiert: Beispiel Symbolische Differentiation diff diff diff diff diff :: String -> Expr -> Expr x (Con _) = zero x (Var y) = if x == y then one else zero x (Sum es) = Sum $ map (diff x) es x (Prod es) = Sum $ map f [0..length es-1] where f i = Prod $ updList es i $ diff x $ es!!i diff x (e :- e') = diff x e :- diff x e' diff x (i :* e) = i :* diff x e diff x (e :^ i) = i :* Prod [diff x e,e:^(i-1)] zero = Con 0 one = Con 1 40 Beispiel Expr-Interpreter type Store = String -> Int evalE evalE evalE evalE evalE evalE evalE evalE (Belegung der Variablen) :: Expr -> Store -> Int (Con i) _ = i (Var x) st = st x (Sum es) st = sum $ map (flip evalE st) es (Prod es) st = product $ map (flip evalE st) es (e :- e') st = evalE e st - evalE e' st (i :* e) st = i * evalE e st (e :^ i) st = evalE e st ^ i Eine Testumgebung für evalE wird im Abschnitt Monadische Parser vorgestellt. Linienzüge wie die Hilbertkurven lassen sich nicht nur als Punktlisten (s.o.), sondern auch als Listen von Aktionen repräsentieren, die auszuführen sind, um einen Linienzug zu zeichnen. Ein Schritt von einem Punkt zum nächsten erfordert die Drehung um einen Winkel α (Turn α) und die anschließende Vor- bzw. Rückwärtsbewegung um eine Distanz d (Move d). data Action = Turn Float | Move Float Zum Zeichnen einer Hilbertkurve benötigt man nur die folgenden Aktionen: up,down,back,forth :: Action up = Turn (-90); down = Turn 90; back = Move (-1); forth = Move 1 Welche Aktionen auszuführen sind, hängt bei Hilbertkurven von einem Richtungsparameter ab, dessen Werte durch folgenden Datentyp repräsentiert sind: data Direction = North | East | South | West flip,swap :: Direction -> Direction flip dir = case dir of North -> South; East -> West; South -> North West -> East swap dir = case dir of North -> West; East -> South; South -> East West -> North 41 Beispiel Hilbertkurven 43 move :: Direction -> [Action] move dir = case dir of North -> [up,forth,down]; East -> [forth] South -> [down,forth,up]; West -> [back] Unter Verwendung dieser Hilfsfunktionen lässt sich die Aktionsliste für eine Hilbertkurve der Tiefe n aus den Aktionslisten von vier Hilbertkurven der Tiefe n − 1 konstruieren: hilbertActs :: Int -> [Action] hilbertActs n = f n East where f :: Int -> Direction -> [Action] f 0 _ = [] f n dir = g sdir++move dir++g dir++move sdir++ g dir++move (flip dir)++g (flip sdir) where g = f $ n-1; sdir = swap dir Hilbertkurven der Tiefen 1, 2, 3 und 5. Man erkennt die auf gleicher Rekursionstiefe erzeugten Punkte daran, dass sie mit Linien gleicher Farbe verbunden sind. Die Aufrufe move dir, move (swap dir) und move (flip dir) erzeugen die Aktionen zum Zeichnen der Linie, welche die erste mit der zweiten, die zweite mit der dritten bzw. die dritte mit der vierten Teilkurve verbindet. Die Rolle der Anfangspunkte der von hilbert berechneten vier Teilkurven wird hier von move-Befehlen übernommen, die von einer Teilkurve zur nächsten führen. Die Änderungen des Direction-Parameters dir beim Aufruf von g = f (n − 1) bzw. move lassen sich recht gut durch den Vergleich der Hilbertkurven der Tiefen 1, 2 und 3 (s.o.) nachvollziehen. 42 44 Mit Hilfe von foldl (s.o.) kann eine Aktionsliste vom Typ [Action] leicht in eine Punktliste vom Typ Path (s.o.) überführt werden. Wir können aber auch hilbertActs so abwandeln, dass nicht erst am Ende des rekursiven Aufbaus einer Aktionsliste die Transformation in eine Punktliste erfolgt, sondern die Punktliste selbst rekursiv erzeugt wird. Dazu wird move in eine Funktion des Typs Point -> Direction -> Point umgewandelt, die für jede Punktliste ps und jede Himmelsrichtung dir den direkten Nachfolger des letzten Elementes von ps berechnet: move :: Path -> Direction -> Point move ps dir = case dir of North -> (x,y-1); East -> (x+1,y) South -> (x,y+1); West -> (x-1,y) where (x,y) = last ps Beispiel Farbkreise Zur Repräsentation von Farben wird häufig der folgende Datentyp verwendet: data Color = RGB Int Int Int red = RGB 255 0 0; magenta = RGB 255 0 255; blue = RGB 0 255 0 cyan = RGB 0 255 255; green = RGB 0 0 255; yellow = RGB 255 255 0 black = RGB 0 0 0; white = RGB 255 255 255 Zwischen den sechs Grundfarben Rot, Magenta, Blau, Cyan, Grün und Gelb liegen weitere sog. reine oder Hue-Farben. Davon lassen sich mit dem Datentyp Color also insgesamt 1530 darstellen und mit folgender Funktion aufzählen: nextCol nextCol nextCol nextCol nextCol nextCol nextCol :: Color -> Color (RGB 255 0 n) | n (RGB n 0 255) | n (RGB 0 n 255) | n (RGB 0 255 n) | n (RGB n 255 0) | n (RGB 255 n 0) | n < > < > < > 255 0 255 0 255 0 = = = = = = RGB RGB RGB RGB RGB RGB 255 0 (n+1) (n-1) 0 255 0 (n+1) 255 0 255 (n-1) (n+1) 255 0 255 (n-1) 0 Rot bis Magenta Magenta bis Blau Blau bis Cyan Cyan bis Grün Grün bis Gelb Gelb bis Rot 45 47 Der von move aus der k-ten Hilbertkurve (k = 1, 2, 3) der Tiefe n − 1 berechnete Punkt ist der Anfangspunkt der (k + 1)-ten Kurve der Tiefe n − 1: Jedes RGB-Element außerhalb des Definitionsbereiches von nextCol repräsentiert eine aufgehellte bzw. abgedunkelte Hue-Farbe. hilbertPoints :: Int -> Path hilbertPoints n = f n (0,0) East where f 0 p _ = [p] f i p dir = ps1++ps2++ps3++ps4 where g = f $ i-1 ps1 = g p sdir ps2 = g (move ps1 dir) dir ps3 = g (move ps2 sdir) dir ps4 = g (move ps3 $ flip' dir) $ flip' sdir sdir = swap dir nextCol induziert einen Farbkreis, der sich am einfachsten – für jede Startfarbe – als unendliche Liste darstellen lässt: colorCirc :: Color -> [Color] colorCirc = iterate nextCol Damit kann man z.B. jedem Element einer n-elementigen Liste (n ≤ 1530) eine von den Farben seiner jeweiligen Nachbarn soweit wie möglich entfernte Farbe zuordnen: addColor :: [a] -> [(a,Color)] addColor s = zip s $ map f [0..n-1] where f i = colorCirc red!!round (float i*1530/float n) n = length s Den Elementen einer 11-elementigen Liste ordnet addColor die folgenden Farben zu: 46 48 size, height :: size (F _ ts) = size _ = height (F _ ts) height _ Term f a -> Int sum (map size ts)+1 1 = foldl max 0 (map height ts)+1 = 1 positions :: Term f a -> [[Int]] positions (F _ ts) = []:concat (zipWith f ts [0..length ts-1]) where f t i = map (i:) $ positions t positions _ = [[]] t :: Term Int Int t = F 1 [F 2 [F 2 [V 3 ,V (-1)],V (-2)],F 4 [V (-3),V 5]] positions t ! [[],[0],[0,0],[0,0,0],[0,0,1],[0,1],[1],[1,0],[1,1]] Anwendung von addColor auf die Hilbertkurve der Tiefe 5 51 49 ! # Termbäume " $ Wir definieren Bäume mit beliebigem endlichen Knotenausgrad und zwei Blatttypen f und a. f ist auch der Typ der inneren Knoten und wird oft durch einen Typ von Funktionsnamen instanziiert. a wird oft durch einen Typ (substituierbarer) Variablen instanziiert (Haskell-Code in: Examples.hs). data Term f a = F f [Term f a] | V a getSubterm t p liefert den Unterbaum von t, dessen Wurzel an Position p von t steht: getSubterm :: Term f a -> [Int] -> Term f a getSubterm t [] = t getSubterm (F _ ts) p = getSubterm' ts p getSubterm' :: [Term f a] -> [Int] -> Term f a getSubterm' (t:_) (0:p) = getSubterm t p getSubterm' (_:ts) (n:p) = getSubterm' ts $ (n-1):p root :: Term a a -> a root (F a _) = a root (V a) = a subterms :: Term a a -> [Term a a] subterms (F _ ts) = ts subterms t = [] t :: Term Int Int t = F 1 [F 2 [F 2 [V 3 ,V (-1)],V (-2)],F 4 [V (-3),V 5]] subterms t ! [F 2 [F 2 [V 3,V (-1)],V (-2)],F 4 [V (-3),V 5]] 50 52 mapT h t wendet h auf die Markierungen der F-Knoten von t an: Beispiel Mengenoperationen auf Listen mapT :: (f -> g) -> Term f a -> Term g a mapT h (F f ts) = F (h f) $ map (mapT h) ts mapT _ (V a) = V a insert :: Eq a => a -> [a] -> [a] insert a s@(b:s') = if a == b then s else b:insert a s' insert a _ = [a] foldT h t wertet t aus gemäß einer durch h gegebenen Interpretation der Markierungen der F-Knoten von t durch Funktionen des Typs [a] -> a: foldT :: (f -> [a] -> a) -> Term f a -> a foldT h (F f ts) = h f $ map (foldT h) ts foldT _ (V a) = a h :: String -> [Int] -> Int h "+" = sum h "*" = product t :: Term String Int t = F "+" [F "*" $ map V [2..6], V 55] foldT h t ! 775 remove :: Eq a => a -> [a] -> [a] remove = filter . (/=) union :: Eq a => [a] -> [a] -> [a] union = foldl $ flip insert Mengenvereinigung diff :: Eq a => a -> [a] -> [a] diff = foldl $ flip remove Mengendifferenz inter :: Eq a => [a] -> [a] -> [a] inter = filter . flip elem Mengendurchschnitt unionMap :: Eq a => (a -> [b]) -> [a] -> [b] unionMap = (foldl union [] .) . map concatMap für Mengen 55 53 ! # Typklassen " $ Beispiel Damenproblem stellen Bedingungen an die Instanzen einer Typvariablen. Die Bedingungen bestehen in der Existenz bestimmter Funktionen, z.B. class Eq a where (==), (/=) :: a -> a -> Bool (/=) = (not .) . (==) Eine Instanz einer Typklasse besteht aus den Instanzen ihrer Typvariablen sowie Definitionen der in ihr deklarierten Funktionen. instance Eq (Int,Bool) where (x,b) == (y,c) = x == y && b == c queens 5 ! instance Eq a => Eq [a] where s == s' = length s == length s' && and $ zipWith (==) s s' Auch (/=) könnte hier definiert werden. Die Definition von (/=) in der Klasse Eq als Negation von (==) ist nur ein Default! Der Typ jeder Funktion einer Typklasse muss die - in der Regel eine - Typvariable der Typklasse mindestens einmal enthalten. Sonst wäre die Funktion ja gar nicht von (der jeweiligen Instanz) der Typvariable abhängig. 54 56 Zwischenwerte von queens 4 Unterklassen Typklassen können wie Objektklassen in OO-Sprachen andere Typklassen erben. Die jeweiligen Oberklassen werden vor dem Erben vor dem Pfeil => aufgelistet. class Eq a => Ord a where (<), (<=), (>=), (>) :: a -> a -> Bool max, min :: a -> a -> a max x y = if x >= y then x else y min x y = if x <= y then x else y Beispiel Quicksort quicksort :: Ord a => [a] -> [a] quicksort (x:s) = quicksort (filter (<= x) s)++x: quicksort (filter (> x) s) quicksort s = s Quicksort ist ein typischer divide-and-conquer-Algorithmus mit mittlerer Laufzeit O(n ∗ log2(n)), wobei n die Listenlänge ist. Wegen der 2 rekursiven Aufrufe in der Definition von quicksort ist log2(n) die (mittlere) Anzahl der Aufrufe von quicksort. Wegen des einen rekursiven Aufrufs in der Definition der conquer-Operation ++ ist n die Anzahl der Aufrufe von ++. Entsprechendes gilt für Mergesort mit der divide-Operation split anstelle von filter und der conquer-Operation merge anstelle von ++: 59 57 queens :: Int -> [[Int]] queens n = loop [1..n] [] where loop :: [Int] -> [Int] -> [[Int]] freie vergebene Liste aller (Teil-)Lösungen Spalten Spalten loop [] ys = [ys] loop xs ys = concatMap (uncurry loop) [(remove x xs,x:ys) | x <- xs, let noDiag y i = x /= y+i && x /= y-i, x und y liegen nicht auf einer Diagonalen and $ zipWith noDiag ys [1..length ys]] Zeilenindizes loop berechnet die Spaltenpositionen der Damen zeilenweise von unten nach oben. 58 Beispiel Mergesort mergesort :: Ord a => [a] -> [a] mergesort (x:y:s) = merge (mergesort $ x:s1) (mergesort $ y:s2) where (s1,s2) = split s oder (\(s1,s2) -> merge (mergesort $ x:s1) (mergesort $ y:s2)) $ split s mergesort s = s split :: [a] -> split (x:y:s) = oder split s = ([a],[a]) (x:s1,y:s2) where (s1,s2) = split s (\(s1,s2) -> (x:s1,y:s2)) $ split s (s,[]) merge :: Ord a => [a] -> [a] -> [a] merge s1@(x:s2) s3@(y:s4) = if x <= y then x:merge s2 s3 else y:merge s1 s4 merge [] s = s merge s _ = s 60 Beispiel Binäre Bäume data Bintree a = Empty | Fork (Bintree a) a (Bintree a) subtrees :: Bintree a -> [Bintree a] subtrees (Fork left _ right) = [left,right] subtrees _ = [] lex :: ReadS String ist eine Standardfunktion, die ein evtl. aus mehreren Zeichen bestehendes Symbol erkennt, vom Eingabestring abspaltet und das Paar (Symbol,Resteingabe) ausgibt. Den Generator ("","") <- lex t in der obigen Definition von read s bewirkt, dass nur die Paare (x,t) von reads s berücksichtigt werden, bei denen die Resteingabe t :: String aus Leerzeichen, Zeilenumbrüchen und Tabulatoren besteht (siehe Beispiele unten). leaf :: a -> Bintree a leaf = flip (Fork Empty) Empty insertTree :: Ord a => a -> Bintree a -> insertTree a t@(Fork t1 b t2) | a == b = | a < b = | True = insertTree a _ = Bintree a t Fork (insertTree a t1) b t2 Fork t1 b $ insertTree a t2 leaf a Steht deriving Read am Ende der Definition eines Datentyps T, dann werden T-Objekte in genau der Darstellung erkannt, in der sie in Programmen vorkommen. Will man das Eingabeformat von T-Objekten selbst bestimmen, dann muss die T-Instanz von readsPrec definiert werden. instance Eq a => Ord (Bintree a) where Empty <= _ = True _ <= Empty = False Fork t1 a t2 <= Fork t3 b t4 = t1 <= t3 && a == b && t2 <= t4 61 63 Beispiel Binäre Bäume Einlesen (Haskell-Code in: Examples.hs) Vor der Eingabe von Daten eines Typs T wird automatisch die T-Instanz der Funktion read aufgerufen, die zur Typklasse Read a gehört. type ReadS a = String -> [(a,String)] Das Argument einer Funktion vom Typ ReadS a ist der jeweilige Eingabestring s. Ihr Wert ist eine Liste von Paaren, bestehend aus dem als Objekt vom Typ a erkannten Präfix von s und der jeweiligen Resteingabe (= Suffix von s). class Read a where read :: String -> a read s = case [x | (x,t) <- reads s, ("","") <- lex t] of [x] -> x [] -> error "PreludeText.read: no parse" _ -> error "PreludeText.read: ambiguous parse" reads :: ReadS a reads = readsPrec 0 instance Read a => Read (Bintree a) where readsPrec _ = readTree readTree :: Read a => ReadS (Bintree a) readTree s = parses1++parses2++parses3 where parses1 = [(Fork left a right,s6) | (a,s1) <- reads s, ("(",s2) <- lex s1, (left,s3) <- readTree s2, (",",s4) <- lex s3, (right,s5) <- readTree s4, (")",s6) <- lex s5] parses2 = [(Fork left a Empty,s4) | (a,s1) <- reads s, ("(",s2) <- lex s1, (left,s3) <- readTree s2, (")",s4) <- lex s3] parses3 = [(leaf a,s1) | (a,s1) <- reads s] Die Instanzen von reads in der Definition von readTree haben den Typ ReadS a. readsPrec :: Int -> ReadS a 62 64 reads "5(7(3, 8),6( 2) ) " :: [(Bintree Int,String)] ! [(5(7(3,8),6(2))," "),(5,"(7(3, 8),6( 2) ) ")] Diese Ausgabe setzt die u.g. Bintree-Instanz von Show voraus. Will man das Ausgabeformat von T-Objekten selbst bestimmen, dann muss die T-Instanz von show oder showsPrec definiert werden. Beispiel Binäre Bäume read "5(7(3, 8),6( 2) ) " :: Bintree Int ! 5(7(3,8),6(2)) instance Show a => Show (Bintree a) where show = showTree0 bzw. showsPrec _ = showTree reads "5(7(3,8),6(2))hh" :: [(Bintree Int,String)] ! [(5(7(3,8),6(2)),"hh"),(5,"(7(3,8),6(2))hh")] showTree0 showTree0 showTree0 showTree0 read "5(7(3,8),6(2))hh" :: Bintree Int ! Exception: PreludeText.read: no parse :: Show a => Bintree (Fork Empty a Empty) (Fork left a Empty) (Fork left a right) a = = = -> String show a show a++'(':showTree0 left++")" show a++'(':showTree0 left++',': showTree0 right++")" oder effizienter, weil iterativ: showTree :: Show a => Bintree a -> ShowS showTree (Fork Empty a Empty) = shows a showTree (Fork left a Empty) = shows a . ('(':) . showTree left . (')':) showTree (Fork left a right) = shows a . ('(':) . showTree left . (',':) . showTree right . (')':) showTree _ = "" 67 65 Set, ein Datentyp für Mengen Ausgeben (Haskell-Code in: Examples.hs) Vor der Ausgabe von Daten eines Typs T wird automatisch die T-Instanz der Funktion show aufgerufen, die zur Typklasse Show a gehört. ShowS = String -> String Das Argument einer Funktion vom Typ ShowS ist der an die Ausgabe eines Objektes vom Typ a anzufügende String. Ihr Wert ist die dadurch entstehende Gesamtausgabe. class Show a where show :: a -> String show x = shows x "" newtype Set a = Set {list :: [a]} instance Eq a => Eq (Set a) where s == s' = s <= s' && s' <= s instance Eq a => Ord (Set a) where s <= s' = all (`elem` list s') $ list s instance Show a => Show (Set a) where show = show . list Set[1,2,3] <= Set[3,4,2,5,1,99] Set[1,2,3] >= Set[3,4,2,5,1,99] ! ! showsPrec :: Int -> a -> ShowS Steht deriving Show am Ende der Definition eines Datentyps T, dann werden T-Objekte in genau der Darstellung ausgegeben, in der sie in Programmen vorkommen. 66 True False eliminiert Mehrfachvorkommen mkSet :: Eq a => [a] -> Set a mkSet = Set . union [] shows :: a -> ShowS shows = showsPrec 0 newtype kann data ersetzen, wenn der Datentyp genau einen Konstruktor hat 68 Kleinste und größte Fixpunkte (Haskell-Code in: Examples.hs) Da -a. eine Teilmenge von P(N ) ist, implementieren wir sie durch den oben eingeführten Typ Set a: Eine Menge A heißt ω-vollständiger *-Halbverband (ω-complete *-semilattice oder complete partial order, kurz: CPO), falls A eine Halbordnung ≤, ein kleinstes Element ⊥ (bottom) und Suprema *i∈Nai aller aufsteigenden Ketten a1 ≤ a2 ≤ a3 ≤ . . . von A besitzt. Eine Funktion f : A → B zwischen zwei CPOs A und B heißt aufwärtsstetig, falls für alle aufsteigenden Ketten a1 ≤ a2 ≤ . . . von A f (*i∈Nai) = *i∈Nf (ai) gilt. reachables :: Eq a => Graph a -> a -> Set a reachables graph a = lfp f least where f (Set as) = Set $ union as $ unionMap graph as least = Set [a] Hier wird die Funktion graph in mehreren Iterationen auf dieselben Knoten angewendet. Um das zu vermeiden, wählen wir einen anderen CPO, nämlich -a. × P(N ), mit komponentenweiser Mengeninklusion als Halbordnung und ({a}, ∅) als kleinstem Element. a ∈ A heißt Fixpunkt von f : A → A, falls f (a) = a gilt. Die Menge der von a aus erreichbaren Knoten ist die erste Komponente des kleinsten Fixpunkts der Funktion Fixpunktsatz von Kleene f : -a. × P(N ) → -a. × P(N ) " (M, used) 0→ (M ∪ {graph(b) | b ∈ M \ used}, used ∪ M ) Sei f : A → A aufwärtsstetig. Dann ist der kleinste Fixpunkt von f . type Graph a = a -> [a] lfp(f ) =def *i∈N f i(⊥) ! 69 Ist f aufwärtsstetig, dann ist f monoton, d.h. für alle a ∈ A gilt: a ≤ b ⇒ f (a) ≤ f (b). Daraus folgt f i(⊥) ≤ f i+1(⊥) für alle i ∈ N, so dass es, falls A endlich ist, ein i ∈ N gibt mit f i(⊥) = f i+1(⊥). In diesem Fall lässt sich lfp(f ) sehr einfach berechnen: lfp :: Ord a => (a -> a) -> a -> a lfp f a = if fa <= a then a else lfp f fa where fa = f a 71 Wir implementieren sowohl -a. als auch P(N ) durch Set a: reachables :: Eq a => Graph a -> a -> Set a reachables graph a = fst $ lfp f least where f (Set as,Set used) = (Set $ union as $ unionMap graph $ diff as used, Set $ union used as) least = (Set [a],Set []) Nach dem Fixpunktsatz von Kleene liefert lfp f a den kleinsten Fixpunkt von f , falls <= eine Halbordnung auf A, a das kleinste Element von A und f aufwärtsstetig ist. Beispiel Die Knotenmengen eines Graphen graph : N → N ∗, die einen bestimmten Knoten a ∈ N enthalten, bilden den CPO -a. ⊆ P(N ), dessen Halbordnung die Mengeninklusion und dessen kleinstes Element die Menge {a} ist. Die Menge der von a aus erreichbaren Knoten von graph ist der kleinste Fixpunkt der Funktion f : -a. → -a. " M 0→ M ∪ {graph(b) | b ∈ M }. 70 72 Beispiele Abgesehen von der unterschiedlichen Mengenrepräsentation ([a] bzw. Set a) liefern sowohl reachables graph a als auch rClosure (tClosure graph nodes) nodes a die Menge der von a aus erreichbaren Knoten von graph. graph1 graph1 a = case a of 1 -> [2,3]; 3 -> [1,4,6]; 4 -> [1]; 5 -> [3,5] 6 -> [2,4,5]; _ -> [] reachables graph1 1 ! [1,2,3,4,6,5] graph2 a = if a > 0 && a < 7 then [a+1] else [] reachables graph2 1 ! [1,2,3,4,5,6,7] 73 Reflexiver und transitiver Abschluss Der reflexive Abschluss G eines Graphen G erweitert G für jeden Knoten a von G um eine Kante von a nach a: eq rClosure :: Eq a => Graph a -> [a] -> Graph a rClosure graph nodes = fold2 update graph nodes $ map graph' nodes where graph' a = insert a $ graph a Der transitive Abschluss G+ eines Graphen G erweitert G für jeden Weg von a nach b in G um eine Kante von a nach b. Der Floyd-Warshall-Algorithmus berechnet G+, indem er, ausgehend von G, iterativ für jeweils einen Knoten b von G aus dem zuvor berechneten Graphen G3 einen neuen, trans(G3, b), berechnet, der für je zwei Kanten von G3 von a nach b bzw. von b nach c zusätzlich eine Kante von a nach c enthält: tClosure :: Eq a => Graph a -> [a] -> Graph a tClosure graph nodes = foldl trans graph nodes where trans graph b = fold2 update graph nodes $ map graph' nodes where graph' a = if b `elem` sucs then union sucs $ graph b else sucs where sucs = graph a Ist n die Anzahl der Knoten von G, also die Länge der Liste nodes, dann ist O(n3) die Komplexität von tClosure: Das äußere fold durchläuft nodes, das innere ebenfalls, und die Abfrage b ‘elem‘ sucs kann im worst case, wenn sucs alle Knoten von G enthält, dazu führen, dass jeder einzelne Update von G3 aus n Vergleichen mit b besteht. Die Repräsentation von Graphen (= binären Relationen auf einer Menge A) durch Objekte vom Typ Graph a entspricht der Darstellung durch Adjazenzlisten. Die alternative Repräsentation bilden Adjazenzmatrizen. Der entsprechende Funktionstyp ist type GraphM a = a -> a -> Bool Mit dieser Darstellung eines Graphen lassen sich rClosure bzw. tClosure erheblich vereinfachen: rClosureM :: Eq a => GraphM a -> GraphM a rClosureM graph a b = a == b || graph a b 75 tClosureM :: GraphM a -> [a] -> GraphM a tClosureM = foldl $ \graph b a c -> graph a c || graph a b && graph b c Viele Algorithmen – wie z.B. die Berechnung kürzester Wege – lassen sich als Instanzen der Verallgemeinerung von tClosureM zu closure darstellen, wobei Bool durch einen beliebigen Semiring ersetzt wird: class Semiring a where add,mul :: a -> a -> a type Matrix a b = a -> a -> b closure :: Semiring b => Matrix a b -> [a] -> Matrix a b closure = foldl $ \mat b a c -> mat a c `add` (mat a b `mul` mat b c) Eine Menge A mit Addition und Multiplikation ist ein Semiring, wenn A eine Null und eine Eins enthält, so dass für alle a, b, c ∈ A die folgenden Gleichungen gelten: a + (b + c) = (a + b) + c a+b=b+a 0+a=a=a+0 a ∗ (b ∗ c) = (a ∗ b) ∗ c 1∗a=a=a∗1 0∗a=0=a∗0 a ∗ (b + c) = (a ∗ b) + (a ∗ c) (a + b) ∗ c = (a ∗ c) + (b ∗ c) Assoziativität von + Kommutativität von + Neutralität von 0 bzgl. + Assoziativität von ∗ Neutralität von 1 bzgl. ∗ Annihilierung von A durch 0 Distribution von ∗ über + Um ein Ring zu sein, muss A außerdem inverse Elemente bzgl. + besitzen. 74 76 ! # Rund um den Datentyp Expr " $ Arithmetische Ausdrücke ausgeben instance Show Expr where showsPrec _ = showE showE :: Expr -> ShowS showE (Con i) = (show i++) showE (Var x) = (x++) showE (Sum es) = foldShow '+' es showE (Prod es) = foldShow '*' es showE (e :- e') = ('(':) . showE e . ('-':) . showE e' . (')':) showE (n :* e) = ('(':) . (show n++) . ('*':) . showE e . (')':) showE (e :^ n) = ('(':) . showE e . ('^':) . (show n++) . (')':) foldShow :: Char -> [Expr] -> ShowS foldShow op (e:es) = ('(':) . showE e . foldr trans id es . (')':) where trans e h = (op:) . showE e . h foldShow _ _ = id Prod[Con 3,Con 5,x,Con 11] Sum [11 :* (x :^ 3),5 :* (x :^ 2),16 :* x,Con 33] showE ! ! showE (3*5*x*11) ((11*(x^3))+(5*(x^2))+(16*x)+33) reduce (Sum es) = mkSum $ map mkScal dom ++ if c == 0 then [] else [Con c] where (c,dom,g :: Expr -> Int) :: Estate = foldl f (0,[],const 0) $ map reduce es f :: Estate -> Expr -> Estate f st (Con 0) = st f (c,dom,g) (Con i) = (c+i,dom,g) f (c,dom,g) (i:*e) = (c,insert e dom,update g e $ g e+i) f st e = f st (1:*e) mkScal e = reduce $ g e :* e mkSum [] = zero mkSum [e] = e mkSum es = Sum es reduce (Prod es) = mkProd $ map mkExpo dom ++ if c == 1 then [] else [Con c] where (c,dom,g :: Expr -> Int) :: Estate = foldl f (0,[],const 0) $ map reduce es f :: Estate -> Expr -> Estate f st (Con 1) = st f (c,dom,g) (Con i) = (c*i,dom,g) f (c,dom,g) (e:^i) = (c,insert e dom,update g e $ g e+i) f st e = f st (e:^1) mkExpo e = reduce $ e :^ g e mkProd [] = one mkProd [e] = e mkProd es = Prod es reduce e = e 77 79 Arithmetische Ausdrücke reduzieren Die folgende Funktion reduce wendet die Gleichungen 0+e=e e+e=2∗e (m ∗ e) + (n ∗ e) = (m + n) ∗ e m ∗ (n ∗ e) = (m ∗ n) ∗ e 0∗e=0 e ∗ e = e2 em ∗ en = em+n (em)n = em∗n auf einen arithmetischen Ausdruck an. type Estate = (Int, [Expr], Expr -> Int) reduce reduce reduce reduce reduce reduce reduce reduce reduce reduce :: Expr -> Expr (e :- e') (0 :* e) (1 :* e) (i :* Con j) (i :* (j :* e)) (e :^ 0) (e :^ 1) (Con i :^ j) ((e :^ i) :^ j) = = = = = = = = = reduce $ Sum [e,(-1):*e'] zero reduce e Con $ i*j reduce $ (i*j) :* e one reduce e Con $ i^j reduce $ e :^ (i*j) 78 1∗e=e e0 = 1 e1 = e reduce (Sum es) wendet zunächst reduce auf alle Ausdrücke der Liste es an. Die Ergebnisliste res = map reduce es wird dann, ausgehend vom Anfangszustand (0,[],const 0) mit der Zustandsüberführung f zum Endzustand (c,dom,g) gefaltet, der schließlich mit mkScal in eine reduzierte Version von res überführt wird. Bei der Faltung werden gemäß der Gleichung 0 + e = e die Nullen aus res entfernt und alle Konstanten von res sowie alle Skalarfaktoren von Skalarprodukten m ∗ e mit derselben Basis e gemäß der Gleichung summiert. (m ∗ e) + (n ∗ e) = (m + n) ∗ e Im Endzustand (c,dom,g) ist c ist die Summe aller Konstanten von res und dom die Liste aller Basen von Skalarprodukten von res. g :: Expr -> Int ordnet jedem Ausdruck e die Summe der Skalarfaktoren der Skalarprodukte von res mit der Basis e zu. Nur im Fall c &= 0 wird Con c in den reduzierten Summenausdruck eingefügt. reduce (Prod es) arbeitet völlig analog mit Produkten von Potenzen anstelle von Summen von Skalarprodukten. 80 Arithmetische Ausdrücke in Assemblerprogramme übersetzen und in einer Kellermaschine ausführen Die unten definierte Funktion compileE übersetzt Objekte des Datentyps Expr in Assemblerprogramme. executeE führt diese in einer Kellermaschine aus. Die Zielkommandos sind durch folgenden Datentyp gegeben: data StackCom = Push Int | Load String | Sub | Add Int | Mul Int | Pow Die (virtuelle) Zielmaschine besteht einem Keller für ganze Zahlen und und einem Speicher (Menge von Variablenbelegungen) wie beim Interpreter arithmetischer Ausdrücke (s.o.). Genaugenommen beschreibt ein Typ für diese beiden Objekte nicht diese selbst, sondern die Menge ihrer möglichen Zustände: type State = ([Int],Store) type Store = String -> Int Die Ausführung einer Kommandoliste besteht in der Hintereinanderausführung ihrer Elemente: execute :: [StackCom] -> State -> State execute = flip $ foldl $ flip executeCom Die Übersetzung eines arithmetischen Ausdrucks von seiner Baumdarstellung in eine Befehlsliste erfolgt wie die Definition aller Funktionen auf Expr-Daten induktiv: compileE compileE compileE compileE compileE compileE compileE compileE :: Expr -> [StackCom] (Con i) = [Push i] (Var x) = [Load x] (Sum es) = concatMap compileE es++[Add $ length es] (Prod es) = concatMap compileE es++[Mul $ length es] (e :- e') = compileE e++compileE e'++[Sub] (i :* e) = Push i:compileE e++[Mul 2] (e :^ i) = compileE e++[Push i,Pow] Beginnt die Ausführung des Zielcodes eines Ausdrucks e im Zustand (stack, store), dann endet sie im Zustand (a : stack, store), wobei a der Wert von e unter der Variablenbelegung store ist: executeE (compileE e) (stack,store) = (evalE e store:stack,store) 81 83 Die Bedeutung der einzelnen Zielkommandos wird durch einen Interpreter auf State definiert: executeCom executeCom executeCom executeCom executeCom executeCom executeCom :: StackCom -> State -> State (Push a) (stack,store) = (a:stack,store) (Load x) (stack,store) = (store x:stack,store) Sub st = executeOp (foldl1 (-)) 2 st (Add n) st = executeOp sum n st (Mul n) st = executeOp product n st Pow st = executeOp (foldl1 (^)) 2 st Die Ausführung eines arithmetischen Kommandos besteht in der Anwendung der jeweiligen arithmetischen Operation auf die obersten n Kellereinträge, wobei n die Stelligkeit der Operation ist: executeOp :: ([Int] -> Int) -> Int -> State -> State executeOp f n (stack,store) = (f (reverse as):bs,store) where (as,bs) = splitAt n stack Die Ausdrücke 5*11+6*12+x*y*z und 11*x^3+5*x^2+16*x+33 als Expr-Objekte Z.B. übersetzt compileE die obigen Expr-Objekte in folgende Kommandosequenzen: 0: 1: 2: 3: 4: 5: 6: 7: Push 5 Push 11 Mul 2 Push 6 Push 12 Mul 2 Load "x" Load "y" 8: Load "z" 9: Mul 3 10: Add 3 0: 1: 2: 3: 4: 5: 6: 7: Push 11 Load "x" Push 3 Pow Mul 2 Push 5 Load "x" Push 2 8: 9: 10: 11: 12: 13: 14: Pow Mul 2 Push 16 Load "x" Mul 2 Push 33 Add 4 Eine Testumgebung für execute und compileE wird im Abschnitt Monadische Parser vorgestellt. 82 84 ! # Monaden " $ class Monad m where (>>=) :: m a -> (a -> m b) -> m b sequentielle Komposition (>>) :: m a -> m b -> m b sequentielle Komposition ohne Wertübergabe return :: a -> m a Einbettung in monadischen Typ fail :: String -> m a Wert im Fall eines Matchfehlers m >> m' = m >>= const m' (m >>= f) >>= g = m >>= \a -> f a >>= g m >>= return = m return a >>= f = f a (1) Die roten Gleichungen sind kein Teil der Klassendefinition. Sie beschreiben Bedingungen an Instanzen von Monad, die aber nicht überprüft werden (können). Die Verwendung von Funktionen oder Instanzen der folgenden Unterklasse von Monad erfordert den Import des Standardmoduls Monad.hs, also die Zeile Die Typisierung von Typen durch Kinds erlaubt es, in Typklassen nicht nur Funktionen, sondern auch Typen zu deklarieren, z.B.: class TK a where type T a :: * f :: [a] -> T a instance TK Int where type T Int = Bool f = null Wie die Typen der Funktionen einer Typklasse, so müssen auch deren Typen die Typvariable der Typklasse mindestens einmal enthalten! Einfacher ist es, anstelle der Typdeklaration die Typklasse um eine Typvariable t zu erweitern. Die Abhängigkeit von t von a lässt sich dann als functional dependency a -> t ausdrücken: class TK a t | a -> t where f :: [a] -> t instance TK Int Bool where f = null a -> t verbietet unterschiedliche Instanzen von t bei gleicher Instanz von a. Typdeklarationen und Typklassen mit mehreren Typvariablen erfordern beim Aufruf des ghc-Interpreters die Option -fglasgow-exts. import Monad am Anfang des Programms, das ihn verwendet. 85 class Monad m => MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a 87 scheiternde Berechnung heißt zero in hugs parallele Komposition heißt (++) in hugs mzero >>= f m >>= \a -> mzero mzero `mplus` m m `mplus` mzero = = = = mzero mzero m m (2) Alle bisher deklarierten Typen sind Typen erster Ordnung und haben den Kind ∗. Kinds sind Typen von Typen. Alle Kinds sind aus ∗ und → gebildet. Die Typvariable m (s.o.) und die Ausdrücke Bintree, Term (siehe Datentypen), Graph und Matrix a (siehe Typklassen) sind Typen zweiter Ordnung und haben daher den Kind ∗ → ∗. Sie bezeichnen also Funktionen, die Typen erster Ordnung auf Typen erster Ordnung abbilden. Matrix bildet Typen erster Ordnung auf Typen zweiter Ordnung ab und hat deshalb den Kind ∗ → ∗ → ∗. Die do-Notation verwendet Zuweisung und Sequentialisierung als Funktionen folgender Typen: (<-) :: Monad m => a -> m a -> m () (;) :: Monad m => m a -> m b -> m b Ein Ausdruck der Form do a <- m; m' oder ist semantisch äquivalent zu: m >>= \a -> m' Wird die “Ausgabe” a von m nicht benötigt, dann genügt m anstelle der Zuweisung a <- m. Außerdem schreibt man do a <- m; b <- m'; m'' do a <- m; (do b <- m'; m'') anstelle von Aus Gleichung (1) (s.o.) folgt: do a <- m; return a 86 do a <- m m' = m 88 Monaden-Kombinatoren sequence :: Monad m => [m a] -> m [a] sequence (m:ms) = do a <- m; as <- sequence ms; return $ a:as sequence _ = return [] heißt accumulate in hugs when :: Monad m => Bool -> m () -> m () when b m = if b then m else return () guard :: MonadPlus m => Bool -> m () guard b = if b then return () else mzero (1) (2) ⇒ ⇒ (do guard True; m1; ...; mn) = (do m1; ...; mn) (do guard False; m1; ...; mn) = mzero sat :: m a -> (a -> Bool) -> m a sat m f = do a <- m; guard $ f a; return a sequence ms führt die monadischen Objekte der Liste ms hintereinander aus. Wie bei some m und many m werden die dabei erzeugten Ausgaben vom Typ a aufgesammelt. Im Gegensatz zu some m und many m ist die Ausführung von sequence ms beendet, wenn ms leer ist und nicht, wenn eine Wiederholung von m scheitert. sequence_ :: Monad m => [m a] -> m () sequence_ = foldr (>>) $ return () heißt sequence in hugs sequence_ ms arbeitet wie sequence ms, vergisst aber die dabei erzeugten Ausgaben vom Typ a. sat m f (m satisfies f) bildet jede Berechnung m, deren Ergebnis die Bedingung f verletzt, auf die scheiternde Berechnung mzero ab. 89 91 some, many :: MonadPlus m => m a -> m [a] some m = do a <- m; as <- many m; return $ a:as many m = some m `mplus` return [] some m und many m wiederholen m, bis m scheitert, d.h. das Ergebnis mzero liefert. Das Ergebnis von some m und many m ist die Liste der bei den Wiederholungen von m erzeugten Ausgaben vom Typ a. some m scheitert, falls schon die erste Ausführung von m scheitert. many m hingegen gibt in diesem Fall die leere Liste von Ausgaben zurück. msum :: MonadPlus m => [m a] -> m a msum = foldr mplus mzero heißt concat in hugs msum wendet mplus auf eine Liste monadischer Objekte an. 90 Die folgenden Funktionen führen mit map bzw. zipWith erzeugten Listen monadischer Objekte aus: mapM :: Monad m => (a -> m b) -> [a] -> m [b] mapM f s = sequence $ map f s mapM_ :: Monad m => (a -> m b) -> [a] -> m () mapM_ = sequence_ $ map f s zipWithM :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m [c] zipWithM = sequence $ zipWith f s s' zipWithM_ :: Monad m => (a -> b -> m c) -> [a] -> [b] -> m () zipWithM_ = sequence_ $ zipWith f s s' 92 lookupM :: (Eq a,MonadPlus m) => a -> [(a,b)] -> m b lookupM a ((a',b):s) = if a == a' then return b `mplus` lookupM a s else lookupM a s lookupM _ _ = mzero Die Maybe-Instanz von lookupM entspricht der Funktion lookup (siehe Listen): lookupM s a liefert die zweite Komponente des ersten Paares von s, dessen erste Komponente mit a übereinstimmt. Ist m jedoch durch die Listenmonade instanziiert, dann liefert lookupM s a liefert die Liste der zweiten Komponente aller Paare von s, deren erste Komponenten mit a übereinstimmen. Beispiel Die Funktion sumTree summiert die (ganzzahligen) Knoteneinträge eines binären Baums auf: sumTree :: Bintree Int -> Int sumTree (Fork left a right) = a+sumTree left+sumTree right sumTree _ = 0 Eine monadische Version von sumTree lautet wie folgt: sumTree = run . f where f :: Bintree Int -> Id Int f (Fork left a right) = do b <- f left c <- f right return $ a+b+c f _ = return 0 93 Die Identitätsmonade 95 Die Maybe-Monade newtype Id a = Id {run :: a} data Maybe a = Just a | Nothing instance Monad Id where Id a >>= f = f a return = Id instance Monad Maybe where Just a >>= f = f a _ >>= _ = Nothing return = Just erfüllt (1) fail _ = mzero Einen Datentyp mit genau einer Typvariablen a zur Monade zu machen, zielt darauf ab, über die Komposition >>= festzulegen, wie Werte (jeder Instanz) des Typs a berechnet werden sollen. So gesehen entspricht der Zugriff auf diese Werte einer Prozedurausführung und wird deshalb oft mit run bezeichnet. Tatsächlich liefert die Formulierung einer a-wertigen als (Id a)-wertige Funktion praktisch eine Prozedur, wie sie auch in einer imperativen Sprache aussehen würde. 94 instance MonadPlus Maybe where mzero = Nothing erfüllt (2) Nothing `mplus` m = m m `mplus` _ = m 96 Verwendung der Maybe-Monade Die Listenmonade Rechnen mit partiellen Funktionen Eine partielle Funktion f : A → B wird in Haskell durch f :: A -> Maybe B implementiert. Das Ergebnis der Komposition zweier partieller Funktionen f und g wird durch f ‘comp‘ g implementiert: comp f g :: (a -> Maybe b) -> (b -> Maybe c) -> a -> Maybe c comp f g = do b <- f a; g b instance Monad [ ] where (>>=) = flip concatMap return a = [a] fail _ = mzero instance MonadPlus [ ] where mzero = [] mplus = (++) erfüllt (1) erfüllt (2) Nach Definition von >>= in der Maybe-Monade ist (f ‘comp‘ g) a genau dann definiert (hat also einen Wert der Form Just c), wenn f (a) und f (g(a) definiert sind. c ist dann gleich f (g(a): do b <- f a; g b ist äquivalent zu: f a >>= \b -> g b ist äquivalent zu: case f a of Just b -> g b _ -> Nothing 97 99 Nach Definition von mzero bzw. mplus in der Maybe-Monade gilt z.B.: case f a of Just b | p b -> g b _ -> Nothing ist äquivalent zu: case f a of Nothing -> g a b -> b ist äquivalent zu: do b <- f a guard $ p b g b f a `mplus` g a Verwendung der Listenmonade Rechnen mit nichtdeterministischen Funktionen Eine nichtdeterministische Funktion f : A → P(B) kann in Haskell durch f :: A -> [B] implementiert werden. Das Ergebnis der Komposition zweier nichtdeterministischer Funktionen f und g wird durch f ‘comp‘ g implementiert: comp f g :: (a -> [b]) -> (b -> [c]) -> a -> [c] comp f g = do b <- f a; g b ist äquivalent zu: f a >>= \b -> g b Nach Definition von >>= in der Listenmonade gilt: Beispiel Die folgende Variante split2 von filter wendet zwei Boolesche Funktionen f und g auf die Elemente einer Liste s und ist genau dann definiert, wenn für jedes Listenelement x f (x) oder g(x) gilt. Im definierten Fall liefert split2 das Listenpaar, bestehend aus f ilter(f )(s) und f ilter(g)(s): split2 :: (a -> Bool) -> (a -> Bool) -> [a] -> Maybe ([a],[a]) split2 f g (x:s) = do (s1,s2) <- split2 f g s if f x then Just (x:s1,s2) else do guard $ g x; Just (s1,x:s2) split2 _ _ _ = Just ([],[]) 98 (f `comp` g) a ist äquivalent zu: concat [g b | b <- f a] Die Listeninstanz der Monadenfunktion sequence :: [m a] -> m [a] (s.o.) liefert das - als Liste von Listen dargestellte - kartesische Produkt seiner Argumentlisten as1, . . . , asn: sequence[as1, . . . , asn] = [[a1, . . . , an] | ai ∈ asi, 1 ≤ i ≤ n] = as1 × · · · × asn. Unter Verwendung der Listenkomprehension ist die Listeninstanz von sequence demnach folgendermaßen definiert: 100 sequence :: [[a]] -> [[a]] sequence (as:bss) = [a:bs | a <- as, bs <- sequence bss] sequence _ = [[]] Beispiel sequence $ replicate(3)[1..4] ! [[1..4],[1..4],[1..4]] ! [[1,1,1],[1,1,2],[1,1,3],[1,1,4],[1,2,1],[1,2,2],[1,2,3],[1,2,4], [1,3,1],[1,3,2],[1,3,3],[1,3,4],[1,4,1],[1,4,2],[1,4,3],[1,4,4], [2,1,1],[2,1,2],[2,1,3],[2,1,4],[2,2,1],[2,2,2],[2,2,3],[2,2,4], [2,3,1],[2,3,2],[2,3,3],[2,3,4],[2,4,1],[2,4,2],[2,4,3],[2,4,4], [3,1,1],[3,1,2],[3,1,3],[3,1,4],[3,2,1],[3,2,2],[3,2,3],[3,2,4], [3,3,1],[3,3,2],[3,3,3],[3,3,4],[3,4,1],[3,4,2],[3,4,3],[3,4,4], [4,1,1],[4,1,2],[4,1,3],[4,1,4],[4,2,1],[4,2,2],[4,2,3],[4,2,4], [4,3,1],[4,3,2],[4,3,3],[4,3,4],[4,4,1],[4,4,2],[4,4,3],[4,4,4]] Nach Definition von mzero bzw. mplus in der Listenmonade gilt: do b <- f a; guard (p b); g b ist äquivalent zu: concat [g b | b <- f a, p b] do a <- s; let b = f a; guard (p b); [h b] ist äquivalent zu: [h b | a <- s, let b = f a, p b] Beispiel Tiefen- und Breitensuche in Bäumen (Haskell-Code in: Examples.hs) Je nach Instanziierung von m liefern searchDF h t und searchBF h t einen oder mehrere Knoteneinträge von t, welche die Bedingung h erfüllen (siehe Termbäume): searchDF, searchBF :: MonadPlus m => (a -> Bool) -> Term a a -> m a searchDF h t = msum $ checkRoot h t:map (searchDF h) (subterms t) searchBF h t = visit [t] where visit ts = do guard $ not $ null ts msum $ map (checkRoot h) ts ++ [visit $ concatMap subterms ts] checkRoot h t = do guard $ h a; return a where a = root t t :: Term Int Int t = F 1 [F 2 [F 2 [V 3 ,V (-1)],V (-2)],F 4 [V (-3),V 5]] searchDF searchDF searchBF searchBF (< (< (< (< 0) 0) 0) 0) t t t t :: :: :: :: Maybe Int ! [Int] ! Maybe Int ! [Int] ! Just (-1) [-1,-2,-3] Just (-2) [-2,-3,-1] 103 101 Für binäre Bäume sehen die Suchfunktionen fast genauso aus: Beispiel Damenproblem queens :: Int -> [[Int]] queens n = loop [1..n] [] where loop :: [Int] -> [Int] -> [[Int]] loop [] ys = [ys] loop xs ys = do x <- xs let noDiag y i = x /= y+i && x /= y-i guard $ and $ zipWith noDiag ys [1..length ys] loop (remove x xs) $ x:ys searchBDF, searchBBF :: MonadPlus m => (a -> Bool) -> Bintree a -> m a searchBDF h t = msum $ checkBRoot h t:map (searchBDF h) (subtrees t) searchBBF h t = visit [t] where visit ts = do guard $ not $ null ts msum $ map checkBRoot ts ++ [visit $ concatMap subtrees ts] checkBRoot h (Fork _ a _) = do guard $ h a; return a checkBRoot h _ = mzero t :: Bintree Int t = read "5(4(3,8(9)),6(2))" searchBDF searchBDF searchBBF searchBBF 102 (> (> (> (> 5) 5) 5) 5) t t t t :: :: :: :: Maybe Int ! [Int] ! Maybe Int ! [Int] ! Just 8 [8,9,6] Just 6 [6,8,9] 104 Termunifikation Eine Termmonade (siehe Termbäume) instance Monad Term f where F f ts >>= h = F f $ map (>>= h) ts V a >>= h = h a return = V erfüllt (1) t >>= h wendet die Substitution h :: a -> Term f a auf jede Variable V x von t an, d.h. ersetzt dort V x durch den Baum h x. Das Ergebnis von t >>= h heißt h-Instanz von t. unify t t’ bildet die Bäume t und t’, falls möglich, auf eine Substitution f :: a -> Term f a ab, so dass die Instanzen t >>= f und t' >>= f von t bzw. t’ miteinander übereinstimmen: unify :: (Eq f,Eq a) => Term unify (V a) (V b) | a == b = | True = unify (V a) t = f a -> Term f a -> Maybe (a -> Term f a) Just $ V Just $ update V a $ V b do guard $ checkV (/= a) t Just $ update V a t = unify (V a) t = do guard $ a == b unifyall ts us unify t (V a) unify (F f ts) (F g us) unifyall :: Eq a => [Term f a] -> [Term f a] -> Maybe (a -> Term a) unifyall [] [] = Just V unifyall (t:ts) (u:us) = do f <- unify t u let g = map (>>= f) h <- unifyall (g ts) $ g us Just $ (>>= h) . f unifyall _ _ = Nothing 107 105 t :: Term String String t = F "+" [F "*" [c "5",V "x"],V "y",c "11"] where c = flip F [] h :: String -> Term String String h x = case x of "x" -> F "/" [F "-" [c "6"],c "9",V "z"] "y" -> F "-" [c "7",F "*" [c "8",c "0"]] _ -> V x where c = flip F [] t >>= h ! F "+" [F "*" [F "5" [], F "/" [F "-" [F "6" []],F "9" [],V "z"]], F "-" [F "7" [],F "*" [F "8" [],F "0" []]], F "11" []] ! # Transitionsmonaden " $ total functions IO a IOstore a state is system state state is Store Trans state a state -> (a,state) PTrans state a state -> Maybe (a,state) state is String Parser a partial functions 106 108 Trans, eine Monade totaler Transitionsfunktionen newtype Trans state a = T {run :: state -> (a,state)} instance Monad (Trans state) where T trans >>= f = T $ \st -> let (a,st') = trans st trans' = run (f a) in trans' st' return a = T $ \st -> (a,st) Hier komponiert der bind-Operator >>= die Zustandstransformationen trans und trans’ sequentiell. Dabei erhält trans’ die von trans erzeugte Ausgabe a als Eingabe. Die IO-Monade kann man sich vorstellen als Instanz von des Datentyps Trans, wobei die Menge möglicher Systemzustände die Typvariable state substituiert. Verwenden lässt sie sich nur indirekt über Standardfunktionen wie readFile, writeFile, putStr, getLine, usw., wie in den folgenden Beispielen. 109 Ein/Ausgabe von Funktionsargumenten bzw. -werten test :: (Read a,Show b) => (a -> b) -> IO () test f = do str <- readFile "source" writeFile "target" $ show $ f $ read str readFile :: String -> IO String readFile "source" liest den Inhalt der Datei source und gibt ihn als String zurück. read :: Read a => String -> a read übersetzt einen String in ein Objekt vom Typ a. show :: Show b => b -> String show übersetzt ein Objekt vom Typ b in einen String. Ein/Ausgabe-Schleifen loop :: IO () loop = do putStrLn "Hello!" putStrLn "Enter an integer x!" str <- getLine let x = read str lokale Definition, gilt in allen darauffolgenden Kommandos. Nicht mit einer lokalen Definition let a = e in e' verwechseln, die nur in e' gilt! if x < 5 then do putStr "x < 5" else do putStrLn $ "x = "++show x loop then und else müssen bzgl. der Spalte ihres ersten Zeichen hinter if stehen! putStr :: String -> IO () putStr str schreibt str ins Konsolenfenster. putStrLn :: String -> IO () putStrLn str schreibt str ins Konsolenfenster und geht in die nächste Zeile. getLine :: IO String getLine liest den eingebenen String und geht in die nächste Zeile. 111 Datei lesen mit Fehlerbehandlung und Inhaltsübergabe an Folgeprozedur readFileAndDo :: String -> (String -> IO ()) -> IO () readFileAndDo file continue = do str <- readFile file `catch` const (return "") if null str then putStrLn $ file++" does not exist" else continue str test :: (Read a,Show b) => (a -> b) -> IO () test f = readFileAndDo "source" $ writeFile "target" . show . f . read catch hat den Typ IO a -> (IOError -> IO a) -> IO a. Der Aufruf catch m f fängt einen bei der Ausführung von m auftretenden IO-Fehler err ab, indem er f auf err anwendet. writeFile :: String -> String -> IO () writeFile "target" schreibt einen String in die Datei target. 110 112 Noch ein IO-Beispiel Monadische Version von trace Die Funktionen des Graphikprogramms Painter.hs rufen die folgende Schleife auf, die bei jedem Durchlauf ein Objekt vom Typ a unter Berücksichtigung jeweils eingegebener Skalierungsfaktoren in SVG-Code übersetzt und diesen in die Datei file.svg schreibt: readFileAndDraw :: Read a => String -> (Float -> Float -> a -> (String,Pos)) -> IO () readFileAndDraw file draw = readFileAndDo file $ scale . read where scale a = do putStrLn "Enter a horizontal and a vertical scaling factor!" str <- getLine let strs = words str when (length strs == 2) $ do let [hor,ver] = map read strs (code,size) = draw hor ver a writeFile (file++".svg") $ svg code size scale a 113 IOstore, eine selbstdefinierte Trans-Instanz (Haskell-Code in: Expr.hs) data DefUse = Def String Int | Use String type Store = String -> Int Eine Aufgabe aus der Datenflussanalyse: Ein straight-line-Programm wird auf die Liste der Definitions- und Verwendungsstellen seiner Variablen reduziert. Ausgehend von einem Startzustand st filtert trace die Verwendungsstellen aus der Liste heraus und ersetzt sie durch Paare, bestehend aus der jeweils benutzten Variable und ihrem an der jeweiligen Verwendungsstelle gültigen Wert. trace :: [DefUse] -> [(String,Int)] trace s = f s $ const 0 where f :: [DefUse] -> Store -> [(String,Int)] f (Def x a:s) st = f s $ update st x a f (Use x:s) st = (x,st x): f s st f _ _ = [] trace [Def x 1,Use x,Def y 2,Use y,Def x 3,Use x,Use y] ! [(x,1),(y,2),(x,3),(y,2)] trace macht alle Zustandsänderungen sichtbar. 114 type IOstore = Trans Store updM :: String -> Int -> IOstore () updM x a = T $ \st -> ((),update st x a) getM :: String -> IOstore Int getM x = T $ \st -> (st x,st) traceM :: [DefUse] -> [(String,Int)] traceM s = fst $ run (f s) $ const 0 where f :: [DefUse] -> IOstore [(String,Int)] f (Def x a:s) = do updM x a; f s f (Use x:s) = do a <- getM x; s <- f s; return $ (x,a):s f _ = return [] traceM versteckt die Zustandsänderungen: Die Kapselung in IOStore macht Zustände (vom Typ Store) implizit zu Werten einer globalen Variable, die – wie in imperativen Programmen üblich – weder Parameter noch Wert der Funktionen, die sie benutzen, ist. run (f s) ist die der “Prozedur” f s entsprechende Zustandstransformation. 115 PTrans, eine Monade partieller Transitionsfunktionen Sie unterscheidet sich von Trans dadurch, das deren Wertetyp (a,state) in die MaybeMonade eingebettet wird: newtype PTrans state a = PT {runP :: state -> Maybe (a,state)} instance Monad (PTrans state) where PT trans >>= f = PT $ \st -> do (a,st) <- trans st runP (f a) st return a = PT $ \st -> return (a,st) fail _ = mzero instance MonadPlus (PTrans state) where mzero = PT $ const Nothing PT trans `mplus` PT trans' = PT $ \st -> trans st `mplus` trans' st PTrans state liftet die Operationen >>=, mplus, return, fail und mzero von der Maybe-Monade zum Funktionenraum state -> Maybe (a,state) und zwar so, dass die Gültigkeit der Gleichungen (1) und (2) von Maybe auf PTrans state übertragen werden. 116 Das Ergebnis von prog = (PT trans ‘mplus‘ PT trans’) erlaubt Backtracking: Monadische Scanner Liefert trans ein von Nothing verschiedenes Ergebnis, dann wird dieses auch von prog zurückgegeben. Scheitert trans jedoch, so liefert prog das Ergebnis von trans’. getChr liest das erste Zeichen eines Strings und gibt es als Ergebnis aus: Demnach lassen sich mit PTrans deterministische Automaten implementieren: Die Zustandsmenge ist eine Instanz von state, Übergangs- und Ausgabefunktion sind durch fst . runP bzw. snd . runP gegeben. getChr :: Parser Char getChr = PT $ \str -> do c:str <- return str; return (c,str) Ein Matchfehler bei der Zuweisung führt zum Wert von fail, also const Nothing. char chr und string str erwarten das Zeichen chr bzw. den String str: char :: Char -> Parser Char char chr = sat getChr (== chr) string :: String -> Parser String string = mapM char token p erlaubt vor und hinter des von p erkannten Strings Leerzeichen, Zeilenumbrüche oder Tabulatoren: Parser a -> Parser a token p = do space; a <- p; space; return a where space = many $ sat getChr (`elem` " \t\n") 119 117 ! # Monadische Parser " $ Ein Parser liest eine Zeichenfolge (auch Wort genannt) von links nach rechts, übersetzt das jeweils gelesene Teilwort in ein Objekt eines Typs a und gibt daneben das jeweilige Restwort aus. Betrachtet man ihn als Automaten, dann bilden die Eingabewörter seine Zustände und die Objekte vom Typ a seine Ausgaben. Demzufolge kann er als Objekt der Instanz type Parser = PTrans String der Monade partieller Transitionen implementiert werden. Ein vollständiger Parser wird aufgebaut aus den o.g. Monadenkonstanten bzw. -kombinatoren wie return, mzero, >>=, mplus, mapM, sat und many sowie den folgenden, die typische Scannerfunktionen realisieren. Die folgenden Programme stehen in Expr.hs. Es folgen vier Scanner, die Elemente von Basistypen (Wahrheitswerte, natürliche Zahlen, ganze Zahlen bzw. Identifier) erkennen und in entsprechende Haskell-Typen übersetzen. bool :: Parser Bool bool = msum [do token $ string "True"; return True, do token $ string "False"; return False] nat,int :: Parser Int nat = do ds <- some $ sat getChr (`elem` ['0'..'9']); return $ read ds int = msum [nat, do char '-'; n <- nat; return $ -n] identifier :: Parser String identifier = do first <- sat getChr (`elem` ['a'..'z']++['A'..'Z']) rest <- many $ sat getChr (`notElem` "();=!>+-*^ \t\n") return $ first:rest Das zweite Argument des Applikationsoperators $ endet beim ersten darauffolgenden Semikolon, es sei denn, $ steht direkt vor einem do. Die Kommas trennen die Elemente der Argumentliste von msum. 118 120 Bintree-Parser tchar = token . char bintree :: Parser a -> Parser (Bintree a) bintree p = do a <- p msum [do tchar '(' left <- bintree msum [do tchar ',' right <- bintree tchar ')' return $ Fork left a right, do tchar ')' return $ Fork left a Empty], do return $ leaf a] Z.B. übersetzt runP (bintree int) str den String str, falls möglich, in einen Baum vom Typ Bintree Int. moreSummands, moreFactors, power :: Expr -> Parser Expr moreSummands a = msum [do tchar '-' b <- summand moreSummands $ a :- b, do as <- some $ do tchar '+'; summand moreSummands $ Sum $ a:as, return a] moreFactors a = msum [do as <- some $ do tchar '*'; factor moreFactors $ Prod $ a:as, return a] power a = msum [do tchar '^' i <- token int return $ a :^ i, return a] Die Unterscheidung zwischen Parsern für Ausdrücke, Summanden bzw. Faktoren dient nicht nur der Berücksichtigung von Operatorprioritäten (+ und - vor * und ^), sondern auch der Vermeidung linksrekursiver Aufrufe des Parsers: Zwischen je zwei aufeinanderfolgenden Aufrufen muss mindestens ein Zeichen gelesen werden, damit der zweite Aufruf ein kürzeres Argument hat als der erste und so die Termination des Parsers gesichert ist. 123 121 Expr-Parser Auswertender Expr-Parser runP expr str übersetzt den String str, falls möglich, in ein Objekt des Typs Expr: Strings für Ausdrücke mit genau einer Variablen (x), lassen sich nach demselben Schema in die dem jeweiligen Ausdruck entsprechende einstellige Funktion vom Typ Int -> Int übersetzen: expr, summand, expr = do a summand = do a factor = msum factor :: Parser Expr <- summand; moreSummands a <- factor; moreFactors a [do x <- token identifier power $ Var x, do i <- token int msum [power $ Con i, scalar i], do tchar '('; a <- expr; tchar ')' power a] scalar :: Int -> Parser Expr scalar i = msum [do tchar '*'; a <- summand return $ i :* a, return $ Con i] 122 exprF, summandF, factorF :: Parser (Int -> Int) exprF = do a <- summandF; moreSummandsF a summandF = do a <- factorF; moreFactorsF a factorF = msum [do x <- token identifier guard $ x == "x" powerF id, do i <- token int msum [powerF $ const i, scalarF i], do tchar '('; a <- expr; tchar ')'; powerF a] scalarF :: Int -> Parser (Int -> Int) scalarF i = msum [do tchar '*'; a <- summandF return $ \x -> i*a(x), return $ const i] 124 moreSummandsF, moreFactorsF, powerF :: (Int -> Int) -> Parser (Int -> Int) moreSummandsF a = msum [do tchar '-' b <- summandF moreSummandsF $ \x -> a(x)-b(x), do as <- some $ do tchar '+'; summandF moreSummands $ \x -> sum $ map ($x) $ a:as, return a] moreFactorsF a = msum [do as <- some $ do tchar '*'; factorF moreFactorsF $ \x -> product $ map ($x) $ a:as, return a] powerF a = msum [do tchar '^' i <- token int return $ \x -> a(x)^i, return a] 125 Zusammmenhang zwischen den beiden Expr-Parsern und dem Expr-Interpreter (siehe Datentypen) ∀ str ∈ String : runP (exprF )(str) = Just(f, str) ! runP (expr)(str) = Just(e, str) ∧ =⇒ ∃ e ∈ Expr : ∀ i ∈ Int : evalE(e)(const(i)) = f (i). Testumgebung für Expr-Interpreter, -Compiler und -Parser (Haskell-Code in: Expr.hs) compile :: String -> Int -> IO compile file n = readFileAndDo where h str = case n of 0 -> 1 -> 2 -> 3 -> () file h act str act str act str act str where g siehe Transitionsmonaden expr showExp expr showRedExp expr loop1 expr g exp = do let code = compileE exp showCode code loop2 code 4 -> act str exprF loop3 126 compile file n erwartet Quellcode für ein Expr-Objekt in der Datei file, parsiert es im Fall n < 4 mit expr und im Fall n = 4 mit exprF. Im Fall n = 0 wird das von expr berechnete Expr-Objekt exp in die Datei exp.svg gezeichnet. Im Fall n = 1 wird exp vorher reduziert. Im Fall n = 2 wird die Schleife loop1 gestartet, die in jedem Durchlauf eine Variablenbelegung store einliest und evalE auf exp und sore anwendet. Im Fall n = 3 wird exp mit compileE in Zielcode übersetzt und dann die Schleife loop2 betreten, die in jedem Durchlauf eine Variablenbelegung store einliest und mit execute den Zielcode von exp auf store ausführt. Im Fall n = 4 wird die Schleife loop3 gestartet, die in jedem Durchlauf eine Variablenbelegung store einliest und die von exprF berechnete Funktion f vom Typ Int → Int auf store(x) anwendet. Hilfsfunktionen von compile act :: String -> Parser a -> (a act str parser loop = case runP Just Just -> -> IO ()) -> IO () parser str of (a,"") -> loop a (a,rest) do putStrLn $ "unparsed suffix: "++rest loop a _ -> putStrLn "syntax error" 127 showExp :: Expr -> IO () showExp exp = do writeFile "exp" $ show exp drawTerm "exp" showRedExp :: Expr -> IO () showRedExp exp = do writeFile "redexp" $ show $ reduce exp drawTerm "redexp" loop1 :: Expr -> IO () loop1 exp = do (store,b) <- input let result = evalE exp store when b $ do putStrLn $ "result = "++show result loop1 exp loop2 :: [StackCom] -> IO () loop2 code = do (store,b) <- input let (result:_,_) = execute code ([],store) when b $ do putStrLn $ "result = "++show result loop2 code 128 loop3 :: (Int -> Int) -> IO () loop3 f = do (store,b) <- input when b $ do putStrLn $ "result = "++show (f $ store "x") loop3 f Zugriffsoperator für Felder: input :: IO (Store,Bool) input = do putStrLn "Enter variables!"; str <- getLine let vars = words str putStrLn "Enter values!"; str <- getLine return (listsToFun 0 vars $ map read $ words str, nonempty str) Update-Operator für Felder: (!) :: Ix a => Array a b -> a -> b Funktionsapplikation wird zum Feldzugriff: Für alle i ∈ [a, b], f (i) = mkArray(f )!i. (//) :: Ix a => Array a b -> [(a,b)] -> Array a b Für alle Felder arr mit Indexmenge A und Wertemenge B, s = [(a1, b1), . . . (an, bn)] ∈ (A × B)∗ and a ∈ A gilt also: ! bi falls a = ai für ein 1 ≤ i ≤ n, (arr//s)!a = arr!a sonst. showCode :: [StackCom] -> IO () showCode cs = writeFile "code" $ fold2 f "" [0..length cs-1] cs where f str n c = str++'\n':replicate (5-length lab) ' ' ++lab++": "++show c where lab = show n a1, . . . , an sind genau die Positionen des Feldes arr, an denen es sich von arr//s unterscheidet. 129 ! 131 # Felder " $ Ix, die Typklasse für Indexmengen class Ord a => Ix a where range :: (a,a) -> [a] index :: (a,a) -> a -> Int inRange :: (a,a) -> a -> Bool instance Ix Int where range (a,b) = [a..b] index (a,b) c = c-a inRange (a,b) c = a <= c && c <= b rangeSize :: (a,a) -> Int rangeSize (a,b) = b-a+1 rangeSize (a,b) = index (a,b) b+1 Dynamische Programmierung verbindet die rekursive Implementierung einer oder mehrerer Funktionen mit Memoization, das ist die Speicherung der Ergebnisse rekursiver Aufrufe in einer Tabelle (die üblicherweise als Feld implementiert wird), so dass diese nur einmal berechnet werden müssen, während weitere Vorkommen desselben rekursiven Aufrufs durch Tabellenzugriffe ersetzt werden können. Exponentieller Zeitaufwand wird auf diese Weise oft auf linearen heruntergedrückt Die Standardfunktion array bildet eineListe von (Index,Wert)-Paare auf ein Feld ab: array :: Ix a => (a,a) -> [(a,b)] -> Array a b mkArray (a,b) wandelt die Einschränkung einer Funktion f : A → B auf das Intervall [a, b] ⊆ A in ein Feld um: mkArray :: Ix a => (a,a) -> (a -> b) -> Array a b mkArray (a,b) f = array (a,b) [(x,f x) | x <- range (a,b)] 130 132 Beispiel Fibonacci-Zahlen Tripeldarstellung von Alignments fib 0 = 1 fib 1 = 1 fib n = fib (n-1) + fib (n-2) Wegen der binärbaumartigen Rekursion in der Definition von fib benötigt fib(n) 2n Rechenschritte. Ein äquivalentes dynamisches Programm lautet wie folgt: fibA = mkArray (0,1000) fib where fib 0 = 1 fib 1 = 1 fib n = fibA!(n-1) + fibA!(n-2) fibA!n benötigt nur O(n) Rechenschritte. Der Aufruf führt zur Anlage des Feldes fibA, in das nach Definition von mkArray (s.o) hintereinander die Werte der Funktion fib von fib(0) bis fib(n) eingetragen werden. Für alle i > 1 errechnet sich fib(i) aus Funktionswerten an Stellen j < i. Diese stehen aber bereits in fibA, wenn der i-te Eintrag vorgenommen wird. Folglich sind alle rekursiven Aufrufe in der ursprünglichen Definition von fib als Zugriffe auf bereits belegte Positionen von fibA implementierbar. 133 Beispiel Alignments (Haskell-Code in: Align.hs) Zwei Listen xs und ys des Typs [String] sollen in die Menge alis(xs, ys) der Alignments von xs und ys übersetzt werden. Vorausgesetzt wird eine Boolesche Funktion compl, die für je zwei Strings x und y angibt, ob x und y komplementär zueinander sind und deshalb aneinander “andocken” können. Das ist auch möglich, wenn x und y übereinstimmen. Sei A = String 7 {Nothing} und h : A∗ → String ∗ die Funktion, die aus einem Wort über A alle Vorkommen von Nothing streicht. Dann ist für alle xs, ys ∈ String ∗ die Menge der Alignments von xs und ys wie folgt definiert: "|xs|+|ys| alis(xs, ys) =def n=max(|xs|,|ys|) {[(a1, b1, c1), . . . , (an, bn, cn)] ∈ (A × A × Color)n | h(a1 . . . an) = xs ∧ h(b1 . . . bn) = ys ∧ ∀ 1 ≤ i ≤ n : (ci = red ∧ compl(ai, bi)) ∨ (ci = green ∧ ai = bi &= Nothing) ∨ (ci = white ∧ ((ai = Nothing ∧ bi &= Nothing) ∨ (bi = Nothing ∧ ai &= Nothing))} Alignments lassen sich demnach durch folgenden Datentyp implementieren: type Alignment = [(Maybe String,Maybe String,RGB)] Nur Alignments mit maximalem Matchcount und darunter diejenigen mit maximalen zusammenhängenden Matches sollen berechnet werden. 135 matchcount(s) zählt die Vorkommen von red oder green im Alignment s: matchcount :: Alignment -> Int matchcount (_,_,cs) = length $ filter (/= white) cs maxmatch(s) liefert die Länge der maximalen zusammenhängenden Teillisten von s mit ausschließlich grünen oder roten Farbkomponenten: maxmatch :: Alignment -> Int maxmatch s = max m n where (_,m,n) = foldl f (False,0,0) s f (b,m,n) (_,_,c) = if c == white then (False,0,max m n) else (True,if b then m+1 else 1,n) Zwei Alignments von a c t a c t g c t und a g a t a g maxima(f )(s) ist die Teilliste aller a ∈ s mit maximalem Wert f (a): maxima :: Ord b => (a -> b) -> [a] -> [a] maxima f s = [a | a <- s, f a == maximum (map f s)] Ein Alignment von a d f a a a a a a und a a a a a a d f a 134 136 selectedAlis xs ys compl berechnet alle gewünschten Alignments von xs und ys: selectedAlisR :: [String] -> [String] -> (String -> String -> Bool) -> [Alignment] selectedAlisR xs ys compl = maxima maxmatch $ align (0,0) where lg1 = length xs; lg2 = length ys align = maxima matchcount . f where f (i,j) = if i == lg1 then if j == lg2 then [[]] else appendy else if j == lg2 then appendx else equal++match++appendx++appendy where x = xs!!i; y = ys!!j; alis = align (i+1,j+1) equal = [(Just x,Just y,green):s | s <- alis, x == y] match = [(Just x,Just y,red):s | s <- alis, compl x y || compl y x] appendx = map ((Just x,Nothing,white):) $ align (i+1,j) appendy = map ((Nothing,Just y,white):) $ align (i,j+1) Testumgebung zur Alignment-Erzeugung (Haskell-Code in: Align.hs) drawAlis "file" lädt zwei untereinander geschriebene Stringlisten aus der Datei file und schreibt ihre Alignments in die Datei "fileAlign", wo sie drawLGraph liest und zeichnet (siehe Painter.hs). drawAlis :: String -> Bool -> IO () drawAlis file b = readFileAndDo file f where f str = do writeFile alignFile $ show $ concat $ zipWithIndices $ alis b drawLGraph alignFile where alignFile = file++"Align" (str1,str2) = break (== '\n') str (xs,ys) = (words str1,words str2) compl x y = (x == "a" && y == "t") || (x == "c" && y == "g") genetischer Code alis True = selectedAlis xs ys compl alis _ = selectedAlisR xs ys compl zipWithIndices s = zipWith mkPaths [0..length s-1] s mkPaths :: Int -> Alignment -> [CLPath] mkPaths j s = hor 0:hor 1:zipWithIndices ver s3 where (s1,s2,s3) = unzip3 s hor k = ([p 0 k,p (length s1-1) k],["",""],blue) ver i c = ([p i 0,p i 1],[str s1,str s2],c) where str s = case s!!i of Just a -> a; _ -> "" p i k = (float i,float $ j+j+k) 137 Für zwei Positionen i und j von xs bzw. ys liefert align!(i, j) die Alignments von drop(i)(xs) bzw. drop(j)(ys) mit maximalem Matchcount. Wegen der tertiärbaumartigen Rekursion in der Definition von align benötigt align(0, 0) O(3lgxs+lgys) Rechenschritte. Das nach dem Schema von fibA (s.o.) aus selectedAlisR gebidete äquivalentes dynamische Programm lautet wie folgt: selectedAlis :: [String] -> [String] -> (String -> String -> Bool) -> [Alignment] selectedAlis xs ys compl = maxima maxmatch $ align!(0,0) where lg1 = length xs; lg2 = length ys align = mkArray ((0,0),(lg1,lg2)) $ maxima matchcount . f where f (i,j) = ... s.o. ... where x = xs!!i; y = ys!!j; alis = align!(i+1,j+1) equal = ... s.o. ... match = ... s.o. ... appendx = map ((Just x,Nothing,white):) $ align!(i+1,j) appendy = map ((Nothing,Just y,white):) $ align!(i,j+1) 138 139 ! # Das Tai Chi formaler Modellierung " konstruktorbasiert Signaturen F(A) Algebra " ! A ! initiale Algebra Ini nat destruktorbasiert !' " ! A F(Ini') G(Fin') ! fold F(Ini) !' fold' Ini !'-konsistent <=> fold' mono Ini' $ Ini !'-erreichbar <=> fold' epi unfold' Fin !'-observabel <=> unfold' mono Fin !'-vollständig <=> unfold' epi Quotient Ini/~ ~ ist !-Kongruenz G(A) unfold ! Fin' Coalgebra Fin ! finale Coalgebra G(Fin) inc inv Unterstruktur ist !-Invariante 140 # Injektionen ιi : Asi → i∈I Asi , i ∈ I, a 0→ (a, i) # bilden die Konstruktoren einer Summe i∈I Asi . $ Projektionen πi : i∈I Asi → Asi , i ∈ I, (a1, . . . , an) 0→ ai $ bilden die Destruktoren eines Produktes i∈I Asi . Allgemein besteht eine Konstruktorsignatur Σ =( BS, RS, C) aus einer Menge BS von Basissorten, einer Menge RS rekursiver Sorten und einer Menge C von Konstruktoren f : s1 × · · · × sn → s mit s1, . . . , sn ∈ BS ∪ RS und s ∈ RS und Konstanten c : 1 → s mit s ∈ BS. Dual dazu besteht eine Destruktorsignatur Σ =( BS, CS, D) aus einer Menge BS von Basissorten, einer Menge CS corekursiver Sorten und einer Menge D von Destruktoren f : s → s1 + · · · + sn mit s ∈ CS, s ∈ CS und s1, . . . , sn ∈ BS ∪ CS. Implementierung der Beispiele Endliche Bäume data Tree entry = Join entry (Treelist entry) data Treelist entry = Nil | Cons (Tree entry) (Treelist entry) Endliche und unendliche Bäume data Tree entry = Tree {root :: entry, subtrees :: Treelist entry} data Treelist entry = Treelist {split :: Maybe (Tree entry, Treelist entry)} Der Typkonstruktor Maybe dient hier der Implementierung des Typs 1 + pair. Die beiden Projektionen fst und snd eines binären Produkttyps sind Standardfunktionen von Haskell. Eine Σ-Algebra bzw. Σ-Coalgebra interpretiert jede Sorte von Σ als eine Menge und jeden Konstruktor bzw. Destruktor von Σ als eine Funktion des jeweiligen in Σ deklarierten Typs. 143 141 ! Beispiele 1 steht für die Basissorte, die immer als einelementige Menge interpretiert wird. Die Konstruktoren join : entry × treelist → tree nil : 1 → treelist cons : tree × treelist → treelist definieren den Typ der endlichen Bäume mit Knoteneinträgen der Sorte entry. Die Destruktoren root : tree → entry subtrees : tree → treelist split : treelist → 1 + pair fst : pair → tree snd : pair → treelist Mathematische Semantik funktionaler Programme " # $ Jeder Aufruf eines Haskell-Programms ist ein Ausdruck, der aus Standard- und selbstdefinierten Funktionen zusammengesetzt ist. Demzufolge besteht die Ausführung von Haskell-Programmen in der Auswertung funktionaler Ausdrücke. Da sowohl Konstanten als auch Funktionen rekursiv definiert werden können, kann es passieren, dass die Auswertung eines Ausdrucks – genauso wie die Ausführung eines imperativen Programms – nicht terminiert. Das kann und soll grundsätzlich auch nicht verhindert werden. Z.B. muss die Funktion, die den Interpreter einer Programmiersprache mit Schleifenkonstrukten darstellt, auch für den Fall einer unendlichen Zahl von Schleifendurchläufen definiert werden. Folglich kann ihr Auftreten in einem Ausdruck dazu führen, dass die Auswertung des Ausdrucks nicht terminiert. definieren den Typ der endlichen oder unendlichen (!) Bäume mit Knoteneinträgen der Sorte entry. Es kann auch passieren, dass sich ein und derselbe Ausdruck mit einer Strategie in endlicher Zeit auswerten lässt, mit einer anderen jedoch nicht. Ist das Ergebnis der Auswertung selbst eine Funktion, dann kann es sogar sein, dass beide Strategien in endlicher Zeit ein (funktionales) Ergebnis liefern, die beiden Ergebnisse jedoch verschiedene Funktionen darstellen: eine hat evtl. einen größeren Definitionsbereich als die andere. 142 144 Um diese Unterschiede präzise fassen und u.a. Auswertungsstrategien miteinander vergleichen zu können, modelliert man Funktionen oder Relationen als Elemente von Halbverbänden, genauer gesagt, als nur dort existierende Lösungen von Gleichungen. Wir setzen: Ein Haskell-Programm besteht i.w. aus solchen Gleichungen. Ist seine Bedeutung durch eine Lösung der Gleichung gegeben, dann muss der Lösungsvorgang in den Auswertungsprozess integriert werden, weil – wie oben gesagt – das Ausführen funktionaler Programme im Auswerten von Ausdrücken besteht. Demgegenüber ist das Lösen von Gleichungen und anderen prädikatenlogischen Formeln in der logischen oder relationalen Programmierung das eigentliche Berechnungsziel: Die Ausführung eines logisches Programms besteht in der Suche nach Belegungen der Variablen einer Formel durch Werte, welche die Formel gültig machen. Gelöst werden soll die Gleichung s = take 3 requests in der Variablen s. Von ihrer Bedeutung her sind die im Laufe der Transformation eingeführten Variablen init = 0; mkRequest = (*2); mkResponse = (+1) x0,...,x5,s0,...,s5 existenzquantifiziert. Jeder Pfeil –> beschreibt dann eine logische Äquivalenz. Jede Zwischenformel ist ein Goal, d.i. eine Konjunktion von Gleichungen. Jede Gleichung der Form x = t, wobei x eine Variable und t ein variablenfreier Term (funktionaler Ausdruck) ist, repräsentiert eine Lösung von x. Im relationalen Berechnungsmodell ist Programmausführung = Formeltransformation: Das relationale Berechnungsmodell Da rekursive Gleichungen Formeln sind, ist das schrittweise Lösen von Formeln durch Anwendung von Transformationsregeln wie Unifikation, Resolution und Narrowing zwar ein mögliches, aber doch recht ineffizientes Berechnungsmodell, weil sein allgemeiner prädikatenlogischer Ansatz die Besonderheit rekursiver Gleichungen nicht berücksichtigt und die Anwendung der genannten Regeln platz- und zeitaufwändig ist, u.a. wegen der bei vielen Regelanwendungen notwendigen Einführung neuer Variablen. 145 Beispiel client/server --> --> --> --> init client requests responses 147 server --> --> --> --> --> --> --> requests :: [a] requests = client init responses responses :: [b] responses = server requests --> client :: a -> [b] -> [a] client init responses = init:client (mkRequest response) responses' where response:responses' = responses server :: [a] -> [b] server (request:requests) = mkResponse request:server requests 146 --> --> --> --> s = take 3 requests s = take 3 (client 0 responses) s = take 3 (0:client (x0*2) s0) && x0:s0 = responses s = 0:take 2 (client (x0*2) s0) && x0:s0 = responses s = 0:take 2 (x0*2:client (x1*2) s1) && x1:s1 = s0 && x0:s0 = responses s = 0:x0*2:take 1 (client (x1*2) s1) && x1:s1 = s0 && x0:s0 = responses s = 0:x0*2:take 1 (x1*2:client (x2*2) s2) && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = responses s = 0:x0*2:x1*2:take 0 (client (x2*2) s2) && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = responses s = 0:x0*2:x1*2:[] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = responses s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = responses s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = server requests s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = server (client 0 responses) s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = server (0:client (x3*2) s3) && x3:s3 = responses s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = mkResponse 0:server (client (x3*2) s3) && x3:s3 = responses s = [0,x0*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 && x0:s0 = 1:server (client (x3*2) s3) && x3:s3 = responses s = [0,1*2,x1*2] && x2:s2 = s1 && x1:s1 = s0 && s0 = server (client (x3*2) s3) && x3:s3 = responses s = [0,2,x1*2] && x2:s2 = s1 && 148 x1:s1 = server (client (x3*2) s3) && x3:s3 = responses --> s = [0,2,x1*2] && x2:s2 = s1 && x1:s1 = server (x3*2:client (x4*2) s4) && x4:s4 = s3 && x3:s3 = responses --> s = [0,2,x1*2] && x2:s2 = s1 && x1:s1 = x3*2+1:server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = responses --> s = [0,2,(x3*2+1)*2] && x2:s2 = s1 && s1 = server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = responses --> s = [0,2,x3*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = responses --> s = [0,2,x3*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = server requests --> s = [0,2,x3*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = server (client 0 responses) --> s = [0,2,x3*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = server (0:client (x5*2) s5) && x5:s5 = s4 --> s = [0,2,x3*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 && x3:s3 = 1:server (client (x5*2) s5) && x5:s5 = s4 --> s = [0,2,1*4+2] && x2:s2 = server (client (x4*2) s4) && x4:s4 = s3 && s3 = server (client (x5*2) s5) && x5:s5 = s4 --> s = [0,2,6] && x2:s2 = server (client (x4*2) s4) && x4:s4 = server (client (x5*2) s5) && x5:s5 = s4 Funktionskonstanten und λ-Abstraktionen sind die einzigen Konstruktoren funktionaler Objekte. Da sie alle nullstellig sind, bilden sie zusammen mit einzelnen Variablen die einzigen Patterns für funktionale Objekte. Sei t ein Term. var(t) bezeichnet die Menge der freien Variablen von t. Eine Substitution σ bildet Variablen auf Terme ab. dom(σ) bezeichnet die Menge aller Variablen x, die tatsächlich substituiert werden, für die also σ(x) &= x gilt. σ[t/x] (“σ bis auf: t für x”) bezeichnet die Substitution, die σ an der Stelle x verändert: x bekommt den Wert t. ! t falls x = t σ[t/x](y) =def σ(y) sonst σ wird wie folgt zur Funktion σ ∗ fortgesetzt, die Terme auf Terme abbildet: σ ∗(x) = σ(x) für alle Variablen x, σ ∗(f (t1, . . . , tn)) = f (σ ∗(t1), . . . , σ ∗(tn)) für alle Funktionskonstanten f und Terme f (t1, . . . , tn), σ ∗(λp.e) = λρ∗(p).τ ∗(ρ∗(e)) für alle λ-Abstraktionen λp.e. Hierbei sind die Substititionen τ und ρ wie folgt definiert: τ =def σ[x/x | x ∈ var(p)] bzw. ρ(x) =def ! x3 falls x ∈ var(p) ∩ var(τ (var(e) ∩ dom(τ ))) x sonst 151 149 Das funktionale Berechnungsmodell Dieses ist auf die Lösung rekursiver Gleichungen der Form (x1, . . . , xn) = t (1) zugeschnitten, wobei x1, . . . , xn verschiedene Variablen sind und t ein Term ist, der zu einem n-Tupel von Konstanten oder Funktionen beliebiger Ordnung auswertbar ist. An die Stelle der obigen Formelsequenz tritt eine Termreduktion: Das Renaming ρ der Variablen von var(p) ∩ var(τ (var(e) ∩ dom(τ ))) verhindert die Bindung in λp.τ ∗(e) ihrer freien Vorkommen in τ (dom(τ )). Die gleichen Variablen müssen umbenannt werden, wenn eine quantifizierte Formel instanziiert wird (siehe LV Logik für Informatiker). x3 muss eine frische Variable sein, die weder in p noch in τ (var(e)) vorkommt. Wie man leicht nachrechnen kann, gilt für je zwei Substitutionen σ und τ die Gleichung (τ ∗ ◦ σ)∗ = τ ∗ ◦ σ ∗. Die Semantik des Pfeils –> ist hier nicht logische Äquivalenz, sondern die Gleichheit der Werte der Terme links bzw. rechts vom Pfeil. σ ∗(t) wird Instanz von t genannt. Ein Term u matcht t, falls u eine Instanz von t ist. Die Matching-Relation ist reflexiv, transitiv und – bis auf das Renaming von Variablen – antisymmetrisch. Ihre Umkehrung ≤ heißt Subsumptionsordnung: Notationen und Begriffe zu Termreduktionen Anstelle von σ (t) schreibt man auch: take 3 requests –> ... –> [0,2,6] Funktionen tauchen in Termen als Funktionskonstanten oder λ-Abstraktionen λp.e auf (siehe Funktionen). Hierbei ist e ein beliebiger Term und p ein Pattern, also ein Term, der aus Konstruktoren und Variablen besteht, von denen jede höchstens einmal in p vorkommt. 150 ∗ u ≤ t ⇐⇒def t ist eine Instanz von u. t[σ(x)/x | x ∈ var(t)], in Worten: t mit σ(x) für x, wobei x die Variablen von t durchläuft. 152 Jeder Term t2 einer Termreduktion entsteht aus seinem Vorgänger t1 durch Anwendung einer Reduktionsregel b v1 → v2 auf einen Teilterm u1 von t1, falls die – als Boolescher Ausdruck formulierte – Bedingung b gilt. Dazu muss es eine Substitution σ mit σ ∗(v1) = u1 geben. Falls σ ∗(b) = True gilt, wird der Redex u1 durch das Redukt u2 = σ ∗(v2) ersetzt. Darüberhinaus gibt es einen Kontextterm c (mit einer Variablen x), den der Reduktionsschritt nicht verändert: c[u1/x] = t1 und c[u2/x] = t2. Der gesamte Reduktionsschritt kann demnach wie folgt zerlegt werden: t1 = c[σ ∗(v1)/x] → c[σ ∗(v2)/x] = t2. Um aus Regel und Redex ein eindeutiges Redukt zu erhalten, müssen alle Variablen von b und v2 bereits in v1 vorkommen. Die Gleichungen einer Funktionsdefinition können also erst dann in Termreduktionen angewendet werden, wenn sie keine lokalen Definitionen enthalten. Wir betrachten mehrere Schemata einer Funktionsdefinition und transformieren diese in semantisch äquivalente Reduktionsregeln. 153 Die übliche (wechselseitig rekursive) Definition einer Menge F totaler (oder zumindest totalisierbarer) Funktionen besteht aus Gleichungen folgender Form: (2) Im Fall, dass alle Variablen des Booleschen Ausdrucks en+1 in p0 vorkommen (let-Bedingung), ist das folgende Schema zu (2) äquivalent: f (p0) | en+1 = let p1 = e1 ... pn = en in en+2 • sind p0, . . . , pn Patterns, • e1, . . . , en+2 beliebige Terme, • alle Variablen von p0, . . . , pn paarweise verschieden. (i) • kommt für alle 1 ≤ i ≤ n + 1 jede Variable von ei in einem der Terme p0, . . . , pi−1 vor. (ii) Die binäre Relation >F auf F sei wie folgt definiert: ! es gibt eine Gleichung der Form (2) so, f >F g ⇐⇒def dass g in einem der Terme e1, . . . , en+2 vorkommt. Um sicherzustellen, dass die u.g. aus (2) bzw. (2’) gebildeten Reduktionsregeln im Laufe einer Termreduktion nur endlich oft angewendet werden können, soll es eine wohlfundierte Termordnung < geben, so dass für alle Gleichungen (2) bzw. (2’) der Definition von F und alle Teilterme g(p) einer der Terme e1, . . . , en+2 gilt. (iii) g >+ F f ⇒ p0 < p 155 Schema 1 einer Funktionsdefinition f (p0) | en+1 = en+2 where p1 = e1 ... pn = en Dabei Kurz gesagt: Entweder wird f in der Definition von g nicht benutzt oder die Argumente der Aufrufe von g in der Definition von f sind kleiner als das Argument von f auf der linken Seite der jeweiligen Definitionsgleichung. Dann lässt sich nämlich aus den Relationen >F und < induktiv eine transitive wohlfundierte Termordnung > mit folgenden Eigenschaften konstruieren: • Für jeden Reduktionsschritt t → t3 gilt t > t3. • Für alle Terme t und echten Teilterme u von t gilt t > u. (23) 154 156 Beispiele für Schema 1 mergesort :: [a] -> [a] siehe Typklassen data Btree a = L a | Btree a :# Btree a rep :: (a -> a -> a) -> Btree a -> a -> (a,Btree a) rep _ (L x) y = (x,L y) rep f (t1:#t2) x = (f y z,u1:#u2) where (y,u1) = rep f t1 x (z,u2) = rep f t2 x reveq :: Eq a => [a] -> [a] -> ([a],Bool) reveq (x:s1) (y:s2) = (r++[x],x==y && b) where (r,b) = reveq s1 s2 reveq _ _ = ([],True) reveqI :: Eq a => [a] -> [a] -> [a] -> ([a],Bool) reveqI (x:s1) (y:s2) acc = (r,x==y&&b) where (r,b) = reveqI s1 s2 (x:acc) reveqI _ _ acc = (acc,True) Um Gleichungen der Form (2) in äquivalente ohne lokale Definitionen zu übersetzen, müssen wir die gesamte Definition von f betrachten: f (p10) | e1(n1+1) = e1(n1+2) where p11 = e11; . . . ; p1n1 = e1n1 ... (3) f (pm0) | em(nm+1) = em(nm+2) where pm1 = em1; . . . ; pmnm = emnm bzw., falls die let-Bedingung erfüllt ist (s.o), f (p10) | e1(n1+1) = let p11 = e11; . . . ; p1n1 = e1n1 in e1(n1+2) ... (33) f (pm0) | em(nm+1) = let pm1 = em1; . . . ; pmnm = emnm in em(nm+2) Ist die let-Bedingung erfüllt, dann werden die Gleichungen von (3) bzw. (3’) zu bedingten Reduktionsregeln: f (p10) ... f (pm0) e1(n +1) 1 → em(nm +1) → represttipsI :: Btree a -> [a] -> [a] -> ([a],Btree a,[a]) represttipsI (L x) (y:s) acc = (x:acc,L y,s) represttipsI (t1:#t2) s acc = (ls2,u1:#u2,s2) where (ls1,u1,s1) = represttipsI t1 s acc (ls2,u2,s2) = represttipsI t2 s1 ls1 represttipsI t s acc = (leaves(t)++acc,t[take(n,s)/leaves(t)],drop(n,s)) where n = length(leaves(t)) δ-Regeln let pm1 = em1; . . . ; pnm+2 = enm+2 in emnm 159 157 represttips :: Btree a -> [a] -> ([a],Btree a,[a]) represttips (L x) (y:s) = ([x],L y,s) represttips (t1:#t2) s = (ls1++ls2,u1:#u2,s2) where (ls1,u1,s1) = represttips t1 s (ls2,u2,s2) = represttips t2 s1 represttips t s = (leaves(t),t[take(n,s)/leaves(t)],drop(n,s)) where n = length(leaves(t)) let p11 = e11; . . . ; pn1+2 = en1+2 in e1n1 λ-Applikationen Die let-Ausdrücke werden mit folgender Regel in λ-Applikationen übersetzt: let p1 = e1; . . . ; pn = en in en+1 → λ ∼p1.(. . . (λ ∼pn.en$en+1) . . . )$e1 δ-Regel Das Symbol ∼ kennzeichnet ein irrefutibles Pattern. Das Matching irrefutibler Patterns wird so lange wie möglich hinausgezögert, d.h. eine Applikation der Form (λ ∼ p.e)(t) kann reduziert werden, auch wenn t von p (noch) nicht gematcht wird (s.u.). In Haskell kann ∼ jedem Pattern vorangestellt werden. Semantisch unterscheidet sich ∼p natürlich nur dann von p, wenn p keine Variable ist. Deshalb nennen wir auch Variablen irrefutible Patterns. Steht an der Position eines Arguments einer Funktion f auf der linken Seite jeder Definitionsgleichung für f ein irrefutibles Pattern, dann nennt man dieses Argument von f nichtstrikt und die anderen Argumente von f strikt. Während alle diese Definitionen die syntaktischen Bedingungen (i)-(iii) erfüllen, wird (iii) von den ersten vier Konstanten bzw. Funktionen des Client/Server-Beispiels (siehe Das relationale Berechnungsmodell) verletzt! 158 160 Die Bezeichnung δ-, η- oder β-Regel entstammt dem klassischen λ-Kalkül. Hier sind einige δ-Regeln, die sich aus der Definition von Standardfunktionen ergeben: x+0 → x x∗0 → 0 True && x → x False && x → False if True then x else y → if False then x else y → πi(x1, . . . , xn) → xi head(x : xs) → x tail(x : xs) → xs Reduktionsregeln zur Auswertung von λ-Applikationen Seien x, y, x1, . . . , xn, xs Variablen, p, e, e3, e1, . . . , en Terme und σ eine Substitution. x y 1≤i≤n Ist die let-Bedingung nicht erfüllt, dann wird die Übersetzung komplizierter. Die Definition von f muss in m + 1 Funktionsdefinitionen zerlegt werden: λ ∼(x1, . . . , xn).e$(e1, . . . , en) λ ∼(x1, . . . , xn).e$e3 λ ∼(x : xs).e$e1 : e2 λ ∼(x : xs).e$e3 λ ∼p.e$e3 → → → → → λ ∼p.e$e3 → λx.e=y$e3 λp.e=y$e3 λp.e=y$e3 → → → e[ei/xi | 1 ≤ i ≤ n] e[πi$e3/xi | 1 ≤ i ≤ n] e[e1/x, e2/xs] e[head$e3/x, tail$e3/xs] eσ falls e3 &= (e1, . . . , en) falls e3 &= e1 : e2 falls pσ = e3 und p &∈ {(x1, . . . , xn), x : xs} e[λp.x$e3/x | x ∈ var(p)] falls p &≤ e3 und p &∈ {(x1, . . . , xn), x : xs} e[e3/x] eσ falls pσ = e3 3 y$e falls p &≤ e3 und e3 reduziert ist . bindet stärker als =. 161 163 Termination und Konfluenz f (x) = case x of p10 _ f2(x) = case x of p20 -> if e1(n1+1) then e1(n1+2) else f2(x) where p11 = e11; . . . ; p1n1 = e1n1 -> f2(x) -> if e2(n2+1) then e2(n2+2) else f3(x) where p21 = e21; . . . ; p2n2 = e2n2 -> f3(x) _ ... (4) fm(x) = case x of pm0 | em(nm+1) -> em(nm+2) where pm1 = em1; . . . ; pmnm = emnm (4) wird in Reduktionsregeln übersetzt, die λ-Abstraktionen mit Alternativen der Form λp1.e1= . . . =λpn.en enthalten: f (x) → λp10.λ ∼p11.(. . . λ ∼p1n1 .(if e1(n1+1) then e1(n1+2) else f2(x))$e1n1 . . . )$e11= λ_.f2(x) f2(x) → λp20.λ ∼p21.(. . . λ ∼p2n2 .(if e2(n2+1) then e2(n2+2) else f3(x))$e2n2 . . . )$e21= λ_.f3(x) ... δ-Regeln enm +1 fm(x) → λpm0.(λ ∼pm1.(. . . λ ∼pmnm .enm+2$emnm . . . )$em1)$x 162 Man beachte, dass bei der Anwendung dieser Regeln nur die freien Vorkommen von Variablen substituiert werden. Es bedarf deshalb ihrer genauen Analyse, um die Bedingung(en) zu ermitteln, unter denen jede mit ihnen ausgeführte Termreduktion endlich ist und damit das Regelsystem terminierend genannt wird. Für die aus (2) oder (2’) gebildeten δ-Regeln und die meisten λ-Reduktionsregeln folgt die Termination aus Bedingung (iii). Vorsicht geboten ist jedoch bei der ersten β-Regel. Lässt man ungetypte Terme zu, dann induziert sie die folgende unendliche Termreduktion: Sei x eine Variable. λx.(x$x)$λx.(x$x) → λx.(x$x)$λx.(x$x) → ... Ein Term der Form e$e ist zwar syntaktisch korrekt, aber sein Typ ist nicht inferierbar. In einem geeigneten Halbverband hätte e$e zwar einen Wert, aber der liegt nicht in einem Typ endlicher Ordnung. Reduziert man umgekehrt nur Terme, die einen Typ endlicher Ordnung haben, dann bleiben Termreduktionen auch bei Anwendung der ersten β-Regel endlich. Jede solche Anwendung verkleinert dann nämlich die Ordnung des Typs des Teilterms mit maximaler Typordnung! 164 Ein aus δ-, η- oder β-Regeln gebildetes (und nur auf getypte Terme angewendetes) Regelsystem ist nicht nur terminierend, sondern auch konfluent, d.h. alle vom selben Term ausgehenden Termreduktionen, bei denen in jedem Schritt auf den jeweils ausgewählten Redex die erste anwendbare Regel angewendet wird, zu Reduktionen fortgesetzt werden können, die mit derselben Normalform enden, also demselben Term, auf den keine Regel mehr anwendbar ist. Man zeigt zunächst, dass die folgendermaßen induktiv definierte Termordnung =⇒ stark konfluent ist, d.h. für alle Terme t, u, v mit t =⇒ u und t =⇒ v gibt es einen Term w mit u =⇒ w und v =⇒ w (Beweis!): • Für alle Terme t gilt t =⇒ t. • Alle δ-, η- und β-Schritte gehören zu =⇒. • Für alle n > 0 und alle n-stelligen Funktionskonstanten f (einschließlich $) gilt: t1 =⇒ u1 ∧ · · · ∧ tn =⇒ un ⇒ f (t1, . . . , tn) =⇒ f (u1, . . . , un). • Für alle Patterns p gilt: t =⇒ u ⇒ λp.t =⇒ λp.u. • Für alle Patterns p1, . . . , pn gilt: t1 =⇒ u1 ∧ · · · ∧ tn =⇒ un ⇒ λp1.t1= . . . =λpn.tn =⇒ λp1.u1= . . . =λpn.un. µ-Abstraktionen Weiterhelfen bei der Suche nach vollständigen Reduktionsstrategien tut jedoch die Tatsache, dass Terme, die unendliche Objekte oder nicht-totalisierbare Funktionen enthalten, mit Hilfe des µ-Operators ausgewertet werden können: Der µ-Abstraktion µx1 . . . xn.t bezeichnet die kleinste Lösung der Gleichung (x1, . . . , xn) = t (1) in der Erweiterung des Produktes zum aufwärtsvollständigen Halbverband, in dem der Term t interpretiert wird. Diese Erweiterung einer Menge A besteht zunächst in der Auswahl oder Hinzunahme eines kleinsten Elementes ⊥ und der Bildung einer flachen Halbordnung: Sei a, b ∈ A∪{⊥}. a ≤ b ⇐⇒def a = ⊥ ∨ a = b. Die flache Halbordnung wird dann auf Funktionen in die Menge A ∪ {⊥} fortgesetzt: Seien f, g : B → A ∪ {⊥}. f ≤ g ⇐⇒def ∀ a ∈ A : f (a) ≤ g(a). 165 167 Da jede Termreduktion im transitiven Abschluss von =⇒ liegt, folgt aus der starken Konfluenz von =⇒ die starke Konfluenz des reflexiv-transitiven Abschlusses der Reduktionsrelation. Die aber liefert sofort die Konfluenz des zugrundeliegenden Regelsystems. Sind alle in t1, . . . , tn auftretenden Funktionen in dieser oder anderer Weise zu stetigen Funktionen erweitert worden, dann ist auch Unter der o.g. Voraussetzung, dass in jedem Schritt auf den jeweils ausgewählten Redex die erste anwendbare Regel angewendet wird, unterscheiden sich Reduktionsstrategien nur in der jeweils ausgewählten Redexposition und der Festlegung, wann gewisse Regeltypen anzuwenden sind. Ein nicht-terminierendes Regelsystem kann zwar zu unendlichen Reduktionen eines bestimmten Term t führen. Das bedeutet aber nicht, dass alle Reduktionen von t unendlich sind. Man sucht daher nach vollständigen Strategien, die jeden Term, der eine Normalform hat, dort hinführen. Konfluenz (s.o.) hilft hier nicht weiter. Sie stellt die Eindeutigkeit von Normalformen sicher, nicht aber deren Existenz. 166 Φ : A1 × . . . × An → A1 × . . . × An Φ(a1, . . . , an) =def t[ai/xi | 1 ≤ i ≤ n]A1×...×An stetig und wir können den Fixpunktsatz von Kleene anwenden, nach dem *i∈NΦi(⊥) die kleinste Lösung von (x1, . . . , xn) = t in A1 × . . . × An ist. Daraus ergibt sich die Interpretation einer µ-Abstraktion: (µx1 . . . xn.t)A1×...×An =def *i∈N Φi(⊥). 168 Schema 2 einer Funktionsdefinition Schema 3 einer Funktionsdefinition Betrachten wir nun das Schema der nicht-rekursiven Definition einer Funktion f , deren lokale Definitionen in einer Gleichung der Form (1) zusammengefasst sind. Bevor wir auf vollständige Strategien eingehen, definieren wir induktiv das allgemeine Schema der rekursiven Definition DS(F, globals) einer Menge F funktionaler oder nichtfunktionaler Objekte mit lokalen Definitionen, die selbst diesem Schema genügen. Seien x1, . . . , xm, z1, . . . , zn paarweise verschiedene Variablen und e, t beliebige Terme, in denen f nicht vorkommt und deren freie Variablen zur Menge {x1, . . . , xm, z1, . . . , zn} gehören. f (x1, . . . , xm) = e where (z1, . . . , zn) = t Aus (7) und (8) ergibt sich die folgende Reduktionsregel zur Auswertung von f : f (x1, . . . , xm) → e[πi$µz1 . . . zn.t/zi | 1 ≤ i ≤ n] δ-Regel Um die Auswertung einer µ-Abstraktion anzustoßen, wenden wir die folgende Reduktionsregel an, deren Korrektheit sich direkt aus der Interpretation von µx1 . . . xn.t als Lösung von (1) ergibt: µx1 . . . xn.t → t[πi$µx1 . . . xn.t/xi | 1 ≤ i ≤ n] Expansionsregel Man sieht sofort, dass die Anwendung dieser Regel unendlich oft wiederholbar ist. Glücklicherweise ist sie aber die einzige in unserem Regelsystem, die zu unendlichen Termreduktionen führen kann. Ob eine Reduktionsstrategie vollständig ist, also jeden Term, der eine Normalform hat, dort auch hinführt (s.o.), hängt davon ab, wann und wo sie die Expansionsregel anwendet. globals bezeichnet die Menge der globalen (funktionalen oder nichtfunktionalen) Objekte die in DS(F, globals) vorkommen. • Sei eqs eine Definition von F , die aus Gleichungen der Form f (p) = e mit f ∈ F besteht, wobei p ein Pattern ist und alle in e verwendeten Funktionen Standardfunktionen sind oder zur Menge globals ∪ F gehören. Dann gilt eqs ∈ DS(F, globals). (a) • Sei δ eine Definition von F , die aus Gleichungen der Form f (p) = e where eqs mit f ∈ F besteht, wobei p ein Pattern ist und es Mengen Geq und globalseq von Funktionen gibt mit eqs ∈ DS(Geq , globalseq ∪ F ). Dann gilt eqs ∈ DS(F, ∪eq∈δ globalseq ). (b) 169 Beispiele (eq) 171 Sei F = {f1, . . . , fk } und eqs ∈ DS(F, ∅). repBy :: (a -> a -> a) -> Btree a -> Btree a repBy f t = u where (x,u) = rep f t x repBy min ((L 3:#(L 22:#L 4)):#(L 2:#L 11)) ===> ((2#(2#2))#(2#2)) repBy (+) ((L 3:#(L 22:#L 4)):#(L 2:#L 11)) ===> ((42#(42#42))#(42#42)) pal, palI :: Eq a => [a] -> Bool pal s = b where (r,b) = reveq s r Im Fall (a) kann jedes f ∈ F durch eine einzige µ-Abstraktion dargestellt werden: Für alle 1 ≤ i ≤ k seien fi(pi1) = ei1, . . . , fi(pini ) = eini die Gleichungen für fi innerhalb von eqs. Mit µ(eqs) =def µf1 . . . fk .( λp11.e11= . . . =p1n1 .e1n1 , ... λpk1.ek1= . . . =pknk .eknk ) liefert die Gleichung (f1, . . . , fk ) = µ(eqs) eine zu eqs äquivalente Definition von F . palI s = b where (r,b) = reveqI s r [] sort, sortI :: Ord a => Btree a -> Btree a sort t = u where (ls,u,_) = represttips t (sort ls) sort ((L 3:#(L 22:#L 4)):#((L 3:#(L 22:#L 4)):#(L 2:#L 11))) ===> ((2#(3#3))#((4#(4#11))#(22#22))) sortI t = u where (ls,u,_) = represttipsI t (sort ls) [] 170 172 Im Fall (b) seien für alle 1 ≤ i ≤ k Ein Pfad (der Baumdarstellung von) t ist strikt, wenn jeder Pfadknoten die Wurzel eines Teilterms von t ist, der zur Herleitung einer Normalform von t reduziert werden muss, m.a.W.: jeder Pfadknoten ist ein striktes Argument der Funktion im jeweiligen Vorgängerknoten (s.o.). die Gleichungen für fi innerhalb von eqs. Für alle 1 ≤ i ≤ k und 1 ≤ j ≤ ni gibt es Mengen Gij und globalsij von Funktionen mit eqsij ∈ DS(Gij , globalsij ∪ F ). Die Substitution σij ersetze jede Funktion g ∈ Gij in eij durch ihre äquivalente µ-Abstraktion πg (µ(eqsij )). Mit Sei RS eine Reduktionsstrategie mit (A). Da β- und δ-Regeln niemals unendlich oft hintereinander angewendet werden können, lässt sich jede gemäß RS durchgeführte Termreduktion eindeutig als Folge µ(eqs) =def µf1 . . . fk .( λp11.σ11(e11)= . . . =p1n1 .σ1n1 (e1n1 ), ... λpk1.σk1(ek1)= . . . =pknk .σknk (eknk )) von Termen repräsentieren derart, dass für alle i ∈ N ti+1 durch parallele Anwendungen der Expansionsregel aus ti hervorgeht. Wertet man alle Terme in einem aufwärtsvollständigen Halbverband A aus, der die Funktionssymbole in den Termen durch monotone Funktionen interpretiert, dann wird aus der obigen Termreduktion eine Kette von Werten in A: A A tA 0 ≤ t1 ≤ t2 ≤ . . . fi(pi1) = ei1 where eqsi1, ... fi(pini ) = eini where eqsini liefert die Gleichung (f1, . . . , fk ) = µ(eqs) eine zu eqs äquivalente Definition von F . Aus ihr ergibt sich die folgende Reduktionsregel zur Auswertung von fi, 1 ≤ i ≤ k: fi → πi$µ(eqs) δ-Regel t0 →RS t1 →RS t2 →RS . . . Der von RS berechnete Wert von t0 in A wird dann definiert durch: A tA 0,RS =def *i∈N ti . 173 175 Nach einem auf getypte λ- und µ-Abstraktionen übertragenen Resultat von Jean Vuillemin ist die folgende parallel-outermost, call-by-need oder lazy evaluation (verzögerte Auswertung) genannte Reduktionsstrategie vollständig: Diese Definition kann auf Funktionen höherer Ordnung erweitert werden: Sei A ein aufwärtsvollständiger Halbverband mit flacher Halbordnung und t0 ein Term eines Typs F T = A1 \ {⊥} → . . . → Ak \ {⊥} → A. Dann nennen wir die für alle 1 ≤ i ≤ k und ai ∈ Ai durch A tA RS (a1 ) . . . (ak ) =def (t(a1 ) . . . (ak ))RS • β- und δ-Regeln werden stets vor der Expansionsregel angewendet. (A) • Die Expansionsregel wird immer parallel auf alle bzgl. der Teiltermordnung maximalen µ-Abstraktionen angewendet. (B) Offenbar stimmt der von der full-substitution-Strategie (F S) berechnete Wert von xi, 1 ≤ i ≤ n, mit der (i-ten Projektion der) kleinsten Lösung von (1) in A überein: Die lazy-evaluation-Strategie Der Beweis basiert auf der Beobachtung, dass die Konstruktion der kleinsten Lösung von (x1, . . . , xn) = t nach dem Fixpunktsatz von Kleene selbst eine Reduktionsstrategie wiederspiegelt, die full-substitution genannt wird. Diese wendet die Expansionsregel im Unterschied zu (B) parallel auf alle µ-Abstraktionen an. Da schon die parallele Expansion aller maximalen µ-Abstraktionen viel Platz verbraucht, wird sie in der Regel nicht durchgeführt. Stattdessen wird nur die erste auf einem strikten Pfad gelegene µ-Abstraktion expandiert. Enthält dieser eine kommutative Operation, dann gibt es möglicherweise mehrere solche Pfade, so dass die Strategie unvollständig wird. definierte Funktion tA RS : F T den von RS berechneten Wert von t in A. j A xA i,F S = πi (*j∈N Φ (⊥)) = πi (µx1 . . . xn .t) . Das impliziert u.a., dass die kleinste Lösung von (1) niemals kleiner als der von RS berechnete Wert von (x1, . . . , xn) ist: A A A A (xA 1,RS , . . . , xn,RS ) ≤ (x1,F S , . . . , xn,F S ) = (µx1 . . . xn .t) . RS ist also genau dann vollständig, wenn der von RS berechnete Wert von (x1, . . . , xn) mit der kleinsten Lösung von (x1, . . . , xn) = t übereinstimmt. Aus der o.g. Voraussetzung, dass die Terme einer Reduktion in einem Halbverband mit flacher Halbordnung interpretiert werden, folgt: Eine Reduktion t0 →RS t1 →RS t2 →RS . . . terminiert ⇐⇒ tA 0,RS &= ⊥. 174 176 Zunächst einmal terminiert die Reduktion genau dann, wenn es k ∈ N gibt, so dass tk keine der Variablen von x1, . . . , xn enthält. Wie t, so ist dann auch tk bottomfrei. Also gilt tA k &= ⊥ und damit ⊥= & tA k = A(⊥) tk ≤ A(⊥) *i∈Nti = Parallel-outermost-Reduktionen von pal terminieren nicht, weil die Expansionsschritte keine Redexe für die obige Definition von reveq liefern: (1) pal[1, 2, 1] → π2$µ r b.reveq([1, 2, 1], r) tA 0,RS . Expansion → Enthält für alle i ∈ N ti Variablen von {x1, . . . , xn}, dann gilt für alle i ∈ N tA i (⊥) = ⊥ und damit A(⊥) tA = ⊥. 0,RS = *i∈N ti Expansion → π2$reveq([1, 2, 1], π1$µ r b.reveq([1, 2, 1], r)) π2$reveq([1, 2, 1], π1$reveq([1, 2, 1], π1$µ r b.reveq([1, 2, 1], r))) A(⊥) Ein i ∈ N mit ti &= ⊥ würde nämlich zu einem Widerspruch führen: Sei j das kleinste A(⊥) i mit ti &= ⊥. Es gäbe eine aus Funktionen von ti gebildete monotone Funktion f sowie a1, . . . , am ∈ A mit A(⊥) f (a1, . . . , am, ⊥, . . . , ⊥) = tj &= ⊥. Aus der Monotonie von f und der Flachheit der Halbordnung des Halbverbandes, in dem tj interpretiert wird, würde folgen, dass es k < j und b1, . . . , br ∈ A gibt mit A(⊥) tk = f (a1, . . . , am, b1, . . . , br ) = f (a1, . . . , am, ⊥, . . . , ⊥) &= ⊥ A(⊥) im Widerspruch dazu, dass j das kleinste i mit ti &= ⊥ ist. (Ein ähnliches Argument wird verwendet, um zu zeigen, dass parallel-outermost vollständig ist; siehe Zohar Manna, Mathematical Theory of Computation, Theorem 5-4.) 177 179 Eine Reduktionsstrategie bevorzugt Anwendungen von δ-Regeln, um danach µ-Abstraktionen eliminieren zu können. Dazu müssen vorher Expansionsschritte die Redexe dieser Regeln erzeugen. Tun sie das nicht, dann kann die Reduktion nicht terminieren, da jedes Expansionsredukt einen neuen Expansionsredex enthält. Neben diesem sollte es also auch einen neuen δ- (oder β-) Redex enthalten. Diese Bedingung ist z.B. in der obigen Definition von pal verletzt, sofern dort die obige Definition von reveq verwendet wird: pal :: Eq a => [a] -> Bool pal s = b where (r,b) = reveq s r reveq :: Eq a => [a] -> [a] -> ([a],Bool) reveq (x:s1) (y:s2) = (r++[x],x==y && b) where (r,b) = reveq s1 s2 reveq _ _ = ([],True) Auswertung durch Graphreduktion Manche Compiler funktionaler Sprachen implementieren µ-Abstraktionen durch Graphen: µx1, . . . , xn.t wird zunächst als Baum dargestellt. Dann werden alle identischen Teilbäume von t zu jeweils einem verschmolzen (collapsing). Schließlich wird für alle 1 ≤ i ≤ n die Markierung xi in πi umbenannt und von dem mit πi markierten Knoten eine Kante zur Wurzel von t gezogen. Expansionsschritte verändern den Graphen nicht, sondern die Position eines Zeigers • auf die Wurzel des nächsten Redex. Jedes Fortschreiten des Zeigers auf einer Rückwärtskante implementiert einen Expansionsschritt. Die obige Reduktion von pal[1, 2, 1] entspricht folgender Graphtransformation: (1) •pal[1, 2, 1] → π2$• ↓ reveq([1, 2, 1], π1 ↑) • moves up Die Definition von pal liefert die δ-Regel pal(s) → π2$µ r b.reveq(s, r). 178 (1) → • moves up → π2$• ↓ reveq([1, 2, 1], π1 ↑) π2$• ↓ reveq([1, 2, 1], π1 ↑) • moves down → • moves down → • moves down → π2$ ↓ reveq([1, 2, 1], • π1 ↑) π2$ ↓ reveq([1, 2, 1], • π1 ↑) ... Die Pfeile ↑ und ↓ zeigen auf die Quelle bzw. das Ziel der einen Rückkante in diesem Beispiel. 180 Wie muss die Definition von reveq repariert werden, damit die Auswertung von pal[1, 2, 1] terminiert? Trifft der Zeiger • auf den Ausdruck reveq([1, 2, 1], π1 ↑), dann muss auf diesen wenigstens ein Reduktionsschritt anwendbar sein, damit er modifiziert und damit der Zyklus, den der Zeiger durchläuft, durchbrochen wird. Man erreicht das mit der folgenden Definition von reveq, deren Anwendbarkeit im Gegensatz zur obigen Definition kein pattern matching des zweiten Arguments verlangt: reveq :: Eq a => [a] -> [a] -> ([a],Bool) reveq (x:s1) s = (r++[x],x==y && b) where y:s2 = s (r,b) = reveq s1 s2 reveq _ _ = ([],True) Diese Definition von reveq folgt Schema 1, so dass bei ihrer Überführung in Reduktionsregeln die lokalen Definitionen wie folgt entfernt werden können: reveq(x : s1, s) → λ ∼y : s2.(λ ∼(r, b).(r ++[x], x = y&&b)$reveq(s1, s2))$s (2) reveq([], s) → ([], True) (3) split term → π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @) • A λ ∼(r, b).(r ++[1], 1 = head$tail$π1 ↑ &&b)$ B C reveq([], tail$tail$π1 ↑) β−Regel → • moves down → (3) → π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @) • A (π1 B ++ [1], 1 = head$tail$π1 ↑ &&π2 B) C reveq([], tail$tail$π1 ↑) π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @) A (π1 B ++ [1], 1 = head$tail$π1 ↑ &&π2 B) • C reveq([], tail$tail$π1 ↑) π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @) A (•π1 B ++[1], 1 = head$tail$π1 ↑ && • π2 B) C ([], True) δ−Regeln π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @) A (•[] ++[1], •1 = head$tail$π1 ↑ &&True) → δ−Regeln → π2$ ↓ (•π1 @ ++[1], 1 = head$π1 ↑ && • π2 @) A ([1], 1 = head$tail$π1 ↑) δ−Regeln → π2$ ↓ (•[1] ++[1], 1 = head$ • π1 ↑&&1 = head$tail$ • π1 ↑) δ−Regeln → π2$ ↓ ([1, 1], 1 = head$ • π1 ↑&&1 = head$tail$ • π1 ↑) 183 181 Hiermit erhalten wir eine terminierende Reduktion von pal[1, 1], die als Graphtransformation so aussieht: Die Pfeile ↑, ↓, @, A, B und C zeigen auf die Quelle bzw. das Ziel von drei verschiedenen Kanten. Redexe sind rot, die zugehörigen Redukte grün gefärbt. •pal[1, 1] (1) → (2) → β−Regel → split term → β−Regel → • moves down → (2) → β−Regel → δ−Regel → δ−Regel → β−Regeln → δ−Regel → π2$• ↓ reveq([1, 1], π1 ↑) π2$• ↓ (λ ∼y : s2.(λ ∼(r, b).(r ++[1], 1 = y&&b)$reveq([1], s2))$π1 ↑ π2$• ↓ λ ∼(r, b).(r ++[1], 1 = head$π1 ↑&&b)$reveq([1], tail$π1 ↑) π2$• ↓ λ ∼(r, b).(r ++[1], 1 = head$π1 ↑ &&b)$ @ A reveq([1], tail$π1 ↑) π2$• ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @) A reveq([1], tail$π1 ↑) π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @) • A reveq([1], tail$π1 ↑) π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @) • A (λ ∼y : s2.(λ ∼(r, b).(r ++[1], 1 = y&&b)$reveq([], s2))$tail$π1 ↑ π2$ ↓ (π1 @ ++ [1], 1 = head$π1 ↑ &&π2 @) • A λ ∼(r, b).(r ++[1], 1 = head$tail$π1 ↑&&b)$reveq([], tail$tail$π1 ↑) 182 δ−Regel → β−Regel → δ−Regel → •π2$([1, 1], 1 = head$[1, 1]&&1 = head$tail$[1, 1]) 1 = •head$[1, 1]&&1 = head$ • tail$[1, 1] •1 = 1&&1 = head$[1] •True&&1 = head$[1] 1 = •head$[1] •1 = 1 True In Expander2 sieht die aus den obigen Regeln (1)-(3) bestehende Definition von pal und reveq folgendermaßen aus: pal2(s) == get1(mu r b.reveq2(s)(r)) & reveq2[] == fun(~[],([],bool(True))) & reveq2(x:s1) == fun(~(y:s2),fun((r,b),(r++[x],bool(x=y & Bool(b)))) (reveq2(s1)(s2))) & Die darauf basierende Reduktion von pal2[1,1] enthält zwar z.T. größere Terme als die obige Graphreduktion von pal[1,1]. Dafür entfällt aber die dort erforderliche Zeigerverwaltung: 184 pal2[1,1] bool(1 = head(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r)))))) & 1 = head(tail(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r)))))))) get1(mu r b.(reveq2[1,1](r))) get1(mu r b.(fun(~(y:s2), fun((r,b),(r++[1],bool(1 = y & Bool(b)))) (reveq2[1](s2))) (r))) get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b)))) (reveq2[1](tail(r))))) bool(1 = head[1,1] & 1 = head(tail(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r)))))))) bool(1 = 1 & 1 = head(tail(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r)))))))) bool(1 = head(tail(get0(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r)))))))) bool(1 = head(tail[1,1])) get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b)))) (fun(~(y:s2), fun((r,b),(r++[1],bool(1 = y & Bool(b)))) (reveq2[](s2))) (tail(r))))) bool(1 = head[1]) bool(1 = 1) bool(True) get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b)))) (fun((r0,b),(r0++[1],bool(1 = head(tail(r)) & Bool(b)))) (reveq2[](tail(tail(r))))))) Number of steps: 19 187 185 get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b)))) (fun((r0,b),(r0++[1],bool(1 = head(tail(r)) & Bool(b)))) (fun(~[],([],bool(True))) (tail(tail(r))))))) get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b)))) (fun((r0,b),(r0++[1],bool(1 = head(tail(r)) & Bool(b)))) ([],bool(True))))) get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b)))) ([]++[1],bool(1 = head(tail(r)) & Bool(bool(True)))))) get1(mu r b.(fun((r0,b),(r0++[1],bool(1 = head(r) & Bool(b)))) ([1],bool(1 = head(tail(r)))))) get1(mu r b.(([1]++[1],bool(1 = head(r) & Bool(bool(1 = head(tail(r)))))))) get1(mu r b.([1,1],bool(1 = head(r) & 1 = head(tail(r))))) 186 ! # Unendliche Objekte " $ Auch im Fall, dass einige Datenbereiche aus unendlichen Objekten bestehen (wie im Client/Server-Beispiel (siehe Das relationale Berechnungsmodell), können die obigen Ergebnisse verwendet werden. Allerdings macht es i.d.R. keinen Sinn, solche Datenbereiche in der oben beschriebenen Weise zu einem Halbverband zu vervollständigen. Nimmt man z.B. die Gleichung ones = 1:ones, deren kleinste Lösung die unendliche Liste von Einsen repräsentieren soll, dann kann diese Lösung kaum in einem Halbverband mit flacher Halbordnung liegen. Der Konstruktor (:) müsste als monotone Funktion interpretiert werden, was 1 : ⊥ = ⊥ implizieren würde. Dann wäre aber ⊥ die kleinste Lösung von ones = 1:ones! Die Erweiterung zum aufwärtsvollständigen Halbverband muss hier also anders laufen. Ihr liegt nicht die Menge der unendlichen, sondern der endlichen Objekte zugrunde, die aus denselben Konstruktoren zusammengesetzt sind wie die unendlichen. Kurz gesagt, die leere Liste wird zum kleinsten Element, L ≤ L3 gilt genau dann, wenn L ein Präfix von L3 ist, und eine unendliche Liste wird als Supremum ihrer endlichen Präfixe aufgefasst. 188 Es folgt eine Expander2-Version des Client/Server-Beispiels (siehe Das relationale Berechnungsmodell), mit deren Hilfe wir die oben angekündigte Termreduktion take 3 requests –> ... –> [0,2,6] durchführen können: server(request:requests) == mkResponse(request):server(requests) & Client2(response) == fun(~(response':responses), response:Client2(mkRequest(response'))(responses)) & Requests2 == Client2(init)(Responses2) & Responses2 == server(Requests2) & Bezüglich dieser Halbordnung ist der Σ-Baum Ωs mit ! ⊥s falls w = , Ωs(w) =def undefiniert sonst das kleinste Element von CTΣ,s. Außerdem hat jede Kette t1 ≤ t2 ≤ t3 ≤ . . . von Σ-Bäumen ein Supremum: Für alle w ∈ N∗, ! ti(w) falls ti(w) &= ⊥ für ein i ∈ N, (*i∈Nti)(w) =def ⊥ sonst. In diesem Modell lassen sich alle Σ-Bäume mit unendlichem Definitionsbereich als Suprema von (endlichen) Σ-Termen darstellen. ReqRes == mu client requests responses. (fun(response,fun(~(response':responses), response:client(mkRequest(response'))(responses))), client(init)(responses), server(requests)) & init == 0 & mkRequest == (*2) & mkResponse == (+1) & ReqRes fasst die Definitionen von Client2, Requests2 und Responses2 zu einer µAbstraktion zusammen. Die Terme take(3,Requests2) und take(3,get1(ReqRes)) sind also äquivalent: Sie werden beide (in 53 bzw. 55 Reduktionsschritten) zu [0,2,6] reduziert. 189 Die Semantik unendlicher Listen als Suprema endlicher Approximationen kann auf unendliche Objekte eines beliebigen (konstruktorbasierten) Datentyps fortgesetzt werden. Auch diese Objekte lassen sich partiell ordnen, wenn man sie als partielle Funktionen definiert: Sei Σ =( BS, RS, C) eine Konstruktorsignatur (siehe Das Tai Chi ...), S = BS ∪ RS und für alle f : s1 × · · · × sn → s, dom(f ) = s1 × · · · × sn und ran(f ) = s. 191 ! # Verifikation " $ Die folgenden drei Methoden dienen dem Beweis von Eigenschaften der kleinsten bzw. größten Lösung einer Gleichung der Form (1) (x1, . . . , xn) = t. Die S-sortierte Menge CTΣ der Σ-Bäume besteht aus allen partiellen Funktionen t : N∗ → C derart, dass t genau dann zu ∈ CTΣ,s gehört, wenn für alle w ∈ N∗ Folgendes gilt: • (t(,) ∈ F ∧ ran(t(,)) = s). • t(w) ∈ F ⇒ ∀ 0 ≤ i < length(n) : (t(wi) ∈ F ∧ ran(t(wi)) = si+1, wobei dom(t(w)) = s1 . . . sn ∈ S n. Wir setzen voraus, dass es für alle s ∈ RS eine Konstante ⊥s : , → s in C gibt und definieren damit eine S-sortierte Halbordnung auf CTΣ: Für alle s ∈ S und t, u ∈ CTΣ,s, ! ∀w ∈ N∗ : t(w) &= ⊥ ⇒ t(w) = u(w) falls s ∈ S \ BS, t ≤ u ⇐⇒def t=u sonst. 190 192 Fixpunktinduktion Coinduktion ist anwendbar, wenn es einen aufwärtsvollständigen Halbverband gibt, in dem sich (1) interpretieren lässt und die Funktionen von t monoton bzw. aufwärtsstetig sind. Die Korrektheit der Fixpunktinduktion folgt im ersten Fall aus dem Fixpunktsatz von Knaster und Tarski, im zweiten aus dem Fixpunktsatz von Kleene. ist anwendbar, wenn sich Gleichung (1) in einer finalen Coalgebra lösen lässt (siehe Das Tai Chi ...). Die Trägermengen dieser Coalgebra stimmen mit denen von CTΣ überein (siehe Unendliche Objekte). Ihre Destruktoren sind Fixpunktinduktion ist durch folgende Beweisregel gegeben: µx1 . . . xn.t ≤ u ⇑ t[πi(u)/xi | 1 ≤ i ≤ n] ≤ u • für alle s ∈ RS eine Funktion ds : s → (2) Der Pfeil deutet die Schlußrichtung in einem Beweis an, in dem die Regel angewendet wird. Hier impliziert demnach als der Sukzedent der Regel ihren Antezedenten. Der Fixpunktsatz von Knaster und Tarski besagt, dass die kleinste Lösung von (1) dem kleinsten t-abgeschlossenen Objekt entspricht. Ein Objekt heißt t-abgeschlossen, wenn es die Konklusion von (2) erfüllt. Zur Anwendung der Fixpunktinduktion muss das Beweisziel die Form der Prämisse von (2) haben. s1 × · · · × sn , Σ (t) = dCT def (t(,) : s1 × · · · × sn → s, (λw.t(0w), . . . , λw.t((n − 1)w))), s • für alle n > 1, s1, . . . , sn ∈ S und 1 ≤ i ≤ n, eine Funktion πi : s1 × · · · × sn → si, deren Interpretation in CTΣ ein Baumtupel auf seine i-te Komponenete projiziert: CTΣ πi (t1, . . . , tn) = ti. Z.B. ist CTΣ im Fall der Listensignatur Σ =( {entry}, {list}, E ∪ {[], (:)}) isomorph zur Menge der endlichen und unendlichen Wörter über E. 195 Berechnungsinduktion ist anwendbar, wenn es einen aufwärtsvollständigen Halbverband gibt, in dem sich (1) interpretieren lässt und die Funktionen von t aufwärtsstetig sind. Die Korrektheit der Berechnungsinduktion folgt aus dem Fixpunktsatz von Kleene und erfordert die Zulässigkeit des Beweisziels ϕ, d.h. für alle aufsteigenden Ketten a0 ≤ a1 ≤ a2 ≤ . . . muss aus der Gültigkeit von ϕ(ai) für alle i ∈ N die Gültigkeit von ϕ(*i∈Nai) folgen. Beispielsweise sind Konjunktionen von Gleichungen oder Ungleichungen zulässig. Berechnungsinduktion ist durch folgende Beweisregel gegeben: 194 c:s1 ×···×sn →s∈C deren Interpretation in CTΣ einen Σ-Baum t in seine Wurzel und seine Unterbäume zerlegt: 193 ϕ(µx1 . . . xn.t) ⇑ ϕ(⊥) ∧ ∀x1, . . . , xn : (ϕ(x1, . . . , xn) ⇒ ϕ(t)) % (3) Aus der Finalität von CTΣ folgt u.a., dass für alle s ∈ S zwei Σ-Bäume t und u der Sorte s genau dann gleich sind, wenn sie bzgl. der oben definierten Destruktoren verhaltensäquivalent sind. D.h. (t, u) liegt in der größten binären Relation ∼ von CTΣ, welche die Implikation x ∼ y ⇒ ds(x) ∼ ds(y) (4) erfüllt. Ein coinduktiver Beweis von t ∼ u besteht darin, eine binäre Relation ≈ zu finden, die das Paar (t, u) enthält und (4) erfüllt. Man geht aus von ≈ = {(t, u)}, wendet (4) von links nach rechts auf die Paare von ≈ an und erhält damit Instanzen der rechten Seite von (4), die zu ≈ hinzugenommen werden. Auf die neuen Paare von ≈ wird wieder (4) angewendet, usw. Das Verfahren terminiert, sobald alle durch Anwendungen von (4) auf ≈ erzeugten Paare bereits im Äquivalenzabschluss von ≈ liegen. Dann gilt (4) für ≈ und wir schließen t ∼ u daraus, dass ∼ die größte Relation ist, die (4) erfüllt. 196 Dieses Verfahren basiert auf der zur Fixpunktinduktion dualen Regel: u ≤ νx1 . . . xn.t ⇑ u ≤ t[πi(u)/xi | 1 ≤ i ≤ n] (5) (5) ist anwendbar, wenn es einen abwärtsvollständigen Halbverband gibt, in dem sich (1) interpretieren lässt und die Funktionen von t monoton bzw. abwärtsstetig sind. Die Korrektheit der Coinduktion folgt im ersten Fall aus dem Fixpunktsatz von Knaster und Tarski, im zweiten aus dem Fixpunktsatz von Kleene. Die im oben skizzierten coinduktiven Beweis verwendete Variante von (5) basiert auf dem Potenzmengenverband der durch prädikatenlogische Formeln gegebenen Relationen auf einer – ggf. mehrsortigen – Menge A. Die Halbordnung ≤ entspricht dort der Mengeninklusion, das kleinste Element ist die leere Menge, das größte die Menge A. Damit wird (5) zur Beweisregel für Implikationen: Relationale Coinduktion ψ ⇒ (νx1 . . . xn.ϕ)(0x) ⇑ ∀0x (ψ ⇒ ϕ[πi(λ0x.ψ)/xi | 1 ≤ i ≤ n](0x)) x ≈s y ⇒ x ∼s y ⇑ ∀ x, y : (x ≈s y ⇒ ds(x) ≈ran(ds) ds(y)) (9) M.a.W.: Alle Paare von ≈s sind s-äquivalent, wenn ≈s den Sukzedenten von (9) erfüllt, welcher der Bedingung entspricht. Da die größte Lösung von (8) eine Äquivalenzrelation ist, also mit ihrem Äquivalenzabschluss übereinstimmt, bleibt Regel (9) korrekt, wenn ihr Sukzedent zu ∀(x, y) (x ≈s y ⇒ ds(x) ≈eq ran(ds ) ds (y)) (10) abgeschwächt wird. Deshalb können wir die oben beschriebene schrittweise Konstruktion von ≈s bereits dann beenden, wenn sich der Äquivalenzabschluss von ≈s nicht mehr verändert. Alle wichtigen Induktions- und Coinduktionsregeln sowie zahlreiche Beispiele ihrer Anwendung finden sich in Algebraic Model Checking and more sowie Expander2: Program Verification between Interaction and Automation. (6) 197 199 ϕ und ψ sind hier n-Tupel prädikatenlogischer Formeln, x1, . . . , xn Prädikatvariablen und 0x ein Tupel von Individuenvariablen. νx1 . . . xn.ϕ wird interpretiert als das n-Tupel der größten Relationen, das die logische Äquivalenz Zum Schluss noch die beiden zur relationalen Coinduktion bzw. Berechnungsinduktion dualen Regeln: (7) -x1, . . . , xn.(0x) ⇐⇒ ϕ(0x) erfüllt, die der Gleichung (1) entspricht. Substitution, Implikation und andere aussagenlogische Operatoren werden komponentenweise auf Formeltupel fortgesetzt: -ϕ1, . . . , ϕn.(0x) =def (ϕ1(0x), . . . , ϕn(0x)), (ϕ1, . . . , ϕn) ⇒ (ψ1, . . . , ψn) =def (ϕ1 ⇒ ψ1) ∧ · · · ∧ (ϕn ⇒ ψn) ... Relationale Fixpunktinduktion (µx1 . . . xn.ϕ)(0x) ⇒ ψ ⇑ ∀0x (ϕ[πi(λ0x.ψ)/xi | 1 ≤ i ≤ n](0x) ⇒ ψ) Mit dieser Regel beweist man u.a. Eigenschaften einer Funktion f , die durch ein rekursives, ggf. bedingtes, Gleichungssystem, also z.B. ein Haskell-Programm, definiert ist. ϕ bezeichnet dann die Ein/Ausgabe-Relation von f , hat also die Form f (x) = y, während ψ den erwarteten – nicht notwendig funktionalen – Zusammenhang zwischen den Argumenten und Werten von f beschreibt. Die oben definierte s-Verhaltensäquivalenz ∼s auf CTΣ,s ist durch die Formel ν ≈s .λ(x, y).ds(x) ≈ran(ds) ds(y) als größte Lösung der Instanz x ≈s y ⇐⇒ ds(x) ≈ran(ds) ds(y) (8) von (7) definiert. Die entsprechende Instanz der Coinduktionsregel (6) lautet demnach wie folgt: 198 (11) 200 Berechnungscoinduktion ist anwendbar, wenn es einen abwärtsvollständigen Halbverband gibt, in dem sich (1) interpretieren lässt und die Funktionen von t abwärtsstetig sind. Die Korrektheit der Berechnungscoinduktion folgt aus dem Fixpunktsatz von Kleene und erfordert die Zulässigkeit des Beweisziels ϕ, d.h. für alle absteigenden Ketten a0 ≥ a1 ≥ a2 ≥ . . . muss aus der Gültigkeit von ϕ(ai) für alle i ∈ N die Gültigkeit von ϕ(Gi∈Nai) folgen. Beispielsweise sind Konjunktionen von Gleichungen oder Ungleichungen zulässig. Berechnungscoinduktion ist durch folgende Beweisregel gegeben: ϕ(νx1 . . . xn.t) ⇑ ϕ(H) ∧ ∀x1, . . . , xn : (ϕ(x1, . . . , xn) ⇒ ϕ(t)) (12) Index A∗, 14 Σ-Baum, 190 λ-Abstraktion, 11 λ-Applikation, 11 (++), 15 (//), 131 (;), 88 (<-), 88 all, 26 any, 26 Applikationsoperator, 11 Array, 130 array, 130 Attribut, 34 aufwärtsstetig, 69 Anwendungen dieser Regel sind mir nicht bekannt. Bintree, 61 compileE, 81 # Haskell-Lehrbücher " $ foldr, 25 Marco Block, Adrian Neumann, Haskell-Intensivkurs, Springer 2011 getSubterm, 52 Graph, 71 GraphM, 75 guard, 89 Manuel M. T. Chakravarty, Gabriele C. Keller, Einführung in die Programmierung mit Haskell, Pearson Studium 2004 Halbverband, 69 head, 15 Kees Doets, Jan van Eijck, The Haskell Road to Logic, Maths and Programming, Texts in Computing Vol. 4, King’s College 2004 Individuenvariable, 13 init, 15 Instanz eines Terms, 105 Instanz eines Typs, 13 iterate, 20 Ix, 130 Richard Bird, Introduction to Functional Programming using Haskell, Prentice Hall 1998 Richard Bird, Pearls of Functional Algorithm Design, Cambridge University Press 2010 Paul Hudak, The Haskell School of Expression: Learning Functional Programming through Multimedia, Cambridge University Press 2000 Paul Hudak, John Peterson, Joseph Fasel, A Gentle Introduction to Haskell, Yale and Los Alamos 2000 Graham Hutton, Programming in Haskell, Cambridge University Press 2007 Peter Pepper, Petra Hofstedt, Funktionale Programmierung: Sprachdesign und Programmiertechnik, Springer 2006 Fethi Rabhi, Guy Lapalme, Algorithms: A Functional Programming Approach, Addison-Wesley 1999 Simon Thompson, Haskell: The Craft of Functional Programming, Addison-Wesley 1999 Kellermaschine, 81 Kind, 86 Kompositionsoperator, 11 Konstruktor, 33 last, 16 lines, 32 202 drop, 16 elem, 26 Eq, 54 Expr, 36 expr, 122 exprF, 124 fail, 85 filter, 26 Fixpunkt, 69 Fixpunktsatz von Kleene, 69 flache Halbordnung, 167 flip, 12 fold2, 24 foldl, 23 203 201 ! const, 12 CPO, 69 curry, 12 lookup, 22 lookupM, 93 many, 90 map, 19 mapM, 92 Matrix, 76 mkArray, 130 Monad, 85 MonadPlus, 86 monomorph, 13 monoton, 70 mplus, 86 msum, 90 mzero, 86 newtype, 68 notElem, 26 Parser, 118 polymorph, 13 PTrans, 116 204 range, 130 Read, 62 reduce, 78 repeat, 20 replicate, 20 return, 85 root, 50 sat, 89 sequence, 91 Set, 68 Show, 66 showE, 77 some, 90 splitAt, 17 StackCom, 81 Substitution, 105 subterms, 50 Termunifikation, 107 Trans, 109 Typ zweiter Ordnung, 86 Typkonstruktor, 33 Typvariable, 13 uncurry, 12 unlines, 32 unwords, 32 update, 12 updList, 17 Wörter, 14 when, 89 words, 32 zip, 19 zipWith, 19 zipWithM, 92 tail, 15 take, 16 Term, 50 205