1 - Korrektur 2 - Abstrakte Datentypen für arithmetische Ausdrücke Der Datentyp Wir beginnen zunächst mit dem algebraischen Datentyp für Ausdrücke. Hierfür definieren wir einen Konstruktor Number für Zahlen, sowie je einen Konstruktor für die 4 Operationen: data Expr = Const Int | Add Expr Expr | Sub Expr Expr | Mul Expr Expr | Div Expr Expr deriving Show Eine konkreten Ausdruck wie beispielsweise (3 + 4) ∗ ((73 − 37)/6) können wir dann wie folgt definieren: expr :: Expr expr = Mul (Add (Const 3) (Const 4)) (Div (Sub (Const 73) (Const 37)) (Const 6)) Das sieht jedoch relativ kompliziert aus (Scheme!?). Dies können wir lösen, indem wir nicht wie oben Präfixkonstruktoren, sondern stattdessen Infixkonstruktoren verwenden. Damit diese als solche erkannt werden können, müssen Infixkonstruktoren mit einem Doppelpunkt : beginnen und weitere Symbole enthalten. 1 2 3 4 5 6 7 data Exp = Number Int | Exp :+: Exp | Exp :-: Exp | Exp :*: Exp | Exp :/: Exp deriving Show Nun können wir den Ausdruck wie folgt notieren: 8 9 test :: Exp test = (Number 3 :+: Number 4) :*: ((Number 73 :-: Number 37) :/: Number 6) Wir können optional auch noch Präzedenzen (Bindungsstärken) für die Konstruktoren festlegen um Klammern zu sparen: 10 11 infixl 6 :+:, :-: infixl 7 :*:, :/: Dies definiert die 4 Konstruktoren wegen infixl als links-assoziativ, im Gegensatz zu infixr (rechtsassoziativ) und infix (nicht assoziativ), und die multiplikativen Operationen binden stärker. Damit kann der Ausdruck 1 + 2 ∗ 3 als 12 13 seven :: Exp seven = Number 1 :+: Number 2 :*: Number 3 notiert werden. Auswertung Da Exp eine induktiv definierte Datenstruktur ist, versuchen wir zunächst für jeden Fall eine Gleichung anzugeben, und erhalten direkt die folgende Lösung: 1 14 15 16 17 18 19 eval eval eval eval eval eval :: Exp -> Int (Number i) = (e1 :+: e2) = (e1 :-: e2) = (e1 :*: e2) = (e1 :/: e2) = i eval eval eval eval e1 e1 e1 e1 + eval e2 - eval e2 * eval e2 `div` eval e2 Binärbäume Arithmetische Ausdrücke lassen sich gut als Termbäume darstellen, wobei an den Blättern Zahlen und an den Knoten die Operationen stehen. Wenn wir zunächst die 4 Konstruktoren für Operationen durch einen gemeinsamen Fall abdecken und anschließend die Beschriftungen (Zahlen und Operatornamen) durch Typvariablen ersetzen, erhalten wir schlussendlich eine Datenstruktur für Binärbäume mit prinzipiell unterschiedlichen Beschriftungen an den Blättern und den Knoten: 1. Gemeinsame Teile herausfaktorisieren: data Exp = Number Int | Bin Op Exp Exp data Op = Add | Sub | Mul | Div 2. Typen und Konstruktoren umbenennen: data Tree = Leaf Int | Node Op Tree Tree data Op = Add | Sub | Mul | Div 3. Beschriftungen verallgemeinern: data Tree a b = Leaf a | Node b (Tree a b) (Tree a b) data Op = Add | Sub | Mul | Div Somit lassen sich arithmetische Ausdrücke wie folgt darstellen: 20 21 data Tree a b = Leaf a | Node b (Tree a b) (Tree a b) deriving Show 22 23 24 data Op = Add | Sub | Mul | Div deriving Show 25 26 type Exp' = Tree Int Op Damit wir konkrete Ausdrücke einfacher konstruieren können, definieren wir uns die folgenden Hilfs(konstruktor)funktionen, auch Smartkonstruktoren genannt: 27 28 number :: Int -> Exp' number = Leaf 29 30 31 (.+.) :: Exp' -> Exp' -> Exp' (.+.) = Node Add 32 33 34 (.-.) :: Exp' -> Exp' -> Exp' (.-.) = Node Sub 35 36 37 (.*.) :: Exp' -> Exp' -> Exp' (.*.) = Node Mul 38 2 39 40 (./.) :: Exp' -> Exp' -> Exp' (./.) = Node Div Somit könnte unser Ausdruck test von oben wie folgt in der Baumstruktur definiert werden: 41 42 test' :: Exp' test' = (number 3 .+. number 4) .*. ((number 73 .-. number 37) ./. number 6) Auch für diese Funktionen könnte man wie oben noch Präzedenzen festlegen. Auswertung Die Auswertungsfunktion ergibt sich wie oben, allerdings müssen wir noch einem Wert des Typs Op die dazugehörige Funktion als Bedeutung (Semantik) zuordnen. Wir nennen diese Funktion sem: 43 44 45 46 47 sem sem sem sem sem :: Op Add = Sub = Mul = Div = -> Int -> Int -> Int (+) (-) (*) div Nun können wir die Auswertung wie folgt definieren: 48 49 50 eval' :: Exp' -> Int eval' (Leaf n) = n eval' (Node op l r) = sem op (eval' l) (eval' r) Darstellung Üblicherweise möchte man Ausdrücke aber nicht nur ausrechnen, sondern vielleicht auch noch schön formatiert ausgeben. Wir definieren uns hierzu eine Funktion pshow (kurz für “pretty show”): 51 52 53 pshow :: Exp' -> String pshow (Leaf n) = show n pshow (Node op l r) = concat ["(", pshow l, showOp op, pshow r, ")"] 54 55 56 57 58 59 showOp showOp showOp showOp showOp :: Op Add = Sub = Mul = Div = -> String "+" "-" "*" "/" Faltung Es fällt auf, dass die Funktionen eval' und pshow insofern gleichartig vorgehen, als dass für Blätter die jeweilige Beschriftung verarbeitet wird, während bei Knoten die Teilbäume zunächst rekursiv verarbeitet und dann mit der Knotenbeschriftung kombiniert werden. Dies entspricht genau dem Konzept einer Faltung auf Binärbäumen: 60 61 62 foldTree :: (a -> c) -> (b -> c -> c -> c) -> Tree a b -> c foldTree f _ (Leaf x) = f x foldTree f g (Node y l r) = g y (foldTree f g l) (foldTree f g r) Somiz können wir beide Funktionen auch mittels foldTree implementieren: 63 64 eval2 :: Exp' -> Int eval2 = foldTree id sem 65 66 67 pshow2 :: Exp' -> String pshow2 = foldTree show (\op s1 s2 -> concat ["(", s1, showOp op, s2, ")"]) Beliebigstellige Operationen Um Operationen mit beliebiger Stelligkeit darstellen zu können, reicht es aus die beiden Teilbäume eines Knotens durch eine Liste von Teilbäumen zu ersetzen: 3 68 69 data NTree a b = NLeaf a | NNode b [NTree a b] deriving Show Diese Bäume werden in der Literatur übrigens oft als “rose trees”, zu deutsch “Rhododendron-Bäume”, bezeichnet. 3 - Suchbäume Wir beginnen mit dem Datentyp eines Suchbaums: 1 2 data SearchTree = Empty | Branch SearchTree Int SearchTree deriving (Eq, Show) Die insert-Funktion fügt ein Element nur ein wenn es noch nicht vorhanden ist. In dem Fall wird für das Element ein neues Blatt erzeugt und so in den Baum gehängt, dass es rechts von kleineren und links von größeren Elementen steht. 3 4 5 6 7 8 insert :: Int -> SearchTree -> SearchTree insert x Empty = Branch Empty x Empty insert x (Branch l n r) | x == n = Branch l n r | x < n = Branch (insert x l) n r | otherwise = Branch l n (insert x r) Die Funktion isElem nutzt die Ordnung des Baums, um die richtige Beschriftung zu finden: 9 10 11 12 13 14 isElem :: Int -> SearchTree -> Bool isElem _ Empty = False isElem x (Branch l n r) | x == n = True | x < n = isElem x l | otherwise = isElem x r Zum Löschen einer Beschriftung gibt es unterschiedliche Strategien. Knoten mit weniger als zwei nicht-leeren Teilbäumen können einfach gelöscht werden, aber andere Knoten nicht, da dann zwei nicht-leere Teilbäume zu einem einzigen kombiniert werden müssen. Eine einfache Möglichkeit ist, den linken Teilbaum ganz links im rechten einzufügen oder umgekehrt. Dadurch wird die Höhe des Baumes aber unnötig groß. Besser ist es, eine geeignete Beschriftung aus einem der Teilbäume zu löschen und diese an die frei gewordene Stelle zu schreiben. Man kann entweder die größte (rechteste) Beschriftung des linken oder die kleinste (linkeste) des rechten Teilbaumes an diese Stelle schreiben. Für letzteres benötigt man eine Funktion splitMin, die die kleinste Beschriftung eines sortierten Baumes und den Restbaum als Ergebnis liefert. Da Funktionen nur ein Ergebnis liefern können, liefern wir ein Paar aus beiden Teilen: 15 16 17 18 19 splitMin :: SearchTree -> (Int, SearchTree) splitMin Empty = error "splitMin: empty search tree" splitMin (Branch Empty n r) = (n, r) splitMin (Branch l n r) = (m, Branch t n r) where (m, t) = splitMin l Die splitMin Funktion ist partiell, da sie nur für nicht-leere Bäume definiert ist. Sie wird aber auch nur auf solchen aufgerufen. Mit ihrer Hilfe können wir delete implementieren. Da jede Beschriftung höchstens einmal im Baum vorkommt, genügt es, die erste gefundene zu entfernen. 20 21 delete :: Int -> SearchTree -> SearchTree delete _ Empty = Empty 4 22 23 24 25 26 27 28 29 30 31 32 33 34 delete x (Branch Empty n r) | x == n = r | x < n = Branch Empty n | otherwise = Branch Empty n delete x (Branch l n Empty) | x == n = l | x < n = Branch (delete | otherwise = Branch l delete x (Branch l n r) | x == n = Branch l | x < n = Branch (delete | otherwise = Branch l where (m, t) = splitMin r r (delete x r) x l) n Empty n Empty m t x l) n r n (delete x r) 4 - Polymorphe binäre Blatt-Bäume Der folgende Datentyp representiert Bäume mit Beschriftungen an Blättern. 1 2 data Tree a = Leaf a | Tree a :&: Tree a deriving (Eq,Show) Die flatTree-Funktion soll einen Baum von Bäumen zu einem einzigen Baum flach klopfen. Wir könnten sie rekursiv wie folgt definieren: 3 4 5 flatTree' :: Tree (Tree a) -> Tree a flatTree' (Leaf x) = x flatTree' (s :&: t) = flatTree s :&: flatTree t Die interessante Regel ist die zweite, die den Leaf-Konstruktor der äußeren Baumstruktur löscht. Mit ein wenig Vorausschau können wir uns die Implementierung von flatTree erleichtern. Wählen wir beim Typ von extendTree für die Typvariable a den konkreten Typ Tree b, dann ergibt sich genau der Typ von flatTree. Aber nicht nur das, es ergibt sich auch die Implementierung: 6 7 flatTree :: Tree (Tree a) -> Tree a flatTree = extendTree id Auch die mapTree-Funktion können wir entweder direkt oder mit Hilfe einer anderen Funktion implementieren. Hier die direkte Variante: 8 9 10 mapTree' :: (a -> b) -> Tree a -> Tree b mapTree' f (Leaf x) = Leaf (f x) mapTree' f (s :&: t) = mapTree f s :&: mapTree f t Statdessen können wir auch wieder extendTree verwenden, indem wir die übergebene Funktion mit dem Leaf{.haskell}-Konstruktor kombinieren: 11 12 mapTree :: (a -> b) -> Tree a -> Tree b mapTree f = extendTree (Leaf . f) Die Funktion extendTree können wir entweder direkt implementieren oder mit foldTree. Hier die direkte Implementierung: 13 14 15 extendTree' :: (a -> Tree b) -> Tree a -> Tree b extendTree' f (Leaf x) = f x extendTree' f (s :&: t) = extendTree' f s :&: extendTree' f t Und hier die Variante mit foldTree: 16 17 extendTree :: (a -> Tree b) -> Tree a -> Tree b extendTree f = foldTree f (:&:) 5 Wenn wir die Implementierungen von flatTree und mapTree verwenden, die nicht auf extendTree basieren, können wir extendTree auch mit deren Hilfe definieren: 18 19 extendTree'' :: (a -> Tree b) -> Tree a -> Tree b extendTree'' f = flatTree' . mapTree' f Die einzige Funktion, die wir direkt implementieren müssen, da wir sie nicht auf die anderen zurückführen können, ist foldTree. Sie bekommt für jeden Konstruktor des Tree-Datentyps ein Argument um diesen zu ersetzen: 20 21 22 foldTree :: (a -> b) -> (b -> b -> b) -> Tree a -> b foldTree l _ (Leaf x) = l x foldTree l f (s :&: t) = foldTree l f s `f` foldTree l f t 5 - Folds • • • • foldr foldl foldr foldl (:) (*) (-) (-) [] ist die Identitätsfunktion auf Listen. 1 [x1...xn] berechnet ((1*x1)*x2)...*xn, sprich das Produkt der Elemente der Liste. 1 [x1...xn] berechnet x1-(x2-...(xn-1)). 1 [x1...xn] berechnet ((1-x1)-x2)...-xn bzw. 1 - sum [x1...xn]. Die map Funktion erhält die Listenstruktur und bildet nur die Elemente auf neue ab. 9 10 mapr, mapl :: (a -> b) -> [a] -> [b] mapr f = foldr ((:) . f) [] Aber wie kommt man auf Ausdrücke wie (:) . f? Indem man mit einer anonymen Funktion anfängt und diese systematisch umformt: 11 12 13 14 15 \x xs \x \x \x (:) . -> -> -> -> f f x : xs (f x :) (:) (f x) ((:) . f) x Funktionen ohne explizite Argumente nennt man übrigens punktfrei. Auch die map Funktion kann man mit foldl nur umständlich definieren, und das führt zur gleichen Verschlechterung der Laufzeit wie bei append: 16 mapl f = foldl (\xs x -> xs ++ [f x]) [] Die reverse-Funktion hingegen implementiert man am besten mit foldl. Interessanterweise entspricht die Definition mit foldr genau der naiven (ineffizienten) Variante und die Definition mit foldl der effizienten Implementierung: 17 18 19 20 reverser, reversel :: Eq a => [a] -> [a] reverser = foldr (\x xs -> xs ++ [x]) [] -- quadratisch reversel = foldl (\xs x -> x : xs) -- linear -- reversel = foldl (flip (:)) [] Die zweite Definition nutzt das zweite Argument von foldl als Akkumulator, in dem die gegebene Liste umgedreht wird. unzip lässt sich recht leicht mit foldr implementieren, aber schwierig mit foldl: 20 21 22 unzipr, unzipl :: [(a, b)] -> ([a], [b]) unzipr = foldr (\(a,b) (as,bs) -> (a : as, b : bs)) ([],[]) unzipl = foldl (\(as,bs) (a,b) -> (as ++ [a], bs ++ [b])) ([],[]) 6 Die Variante mit foldr hat hier eine lineare Laufzeit bezogen auf die Listenlänge, die Variante mit foldl ist quadratisch. Bei nub verhält es sich ähnlich, wobei wegen des filter beide Implementierungen eine quadratische Laufzeit besitzen. 23 24 25 nubr, nubl :: Eq a => [a] -> [a] nubr ys = foldr (\x xs -> x : filter (x/=) xs) [] ys nubl ys = foldl (\xs x -> filter (x/=) xs ++ [x]) [] ys 7