9 Algebraische Datentypen Dieses Kapitel erweitert Haskells Typsystem, das neben Basistypen (Integer, Float, Char, Bool, . . . ) und Typkonstruktoren ([ · ] und ( · )) auch algebraische Datentypen kennt. I Ganz analog zum Typkonstruktor [ · ], der die beiden Konstruktorfunktionen (:) und [] einführte, um Werte des Typs [α] zu konstruieren, kann der Programmierer neue Konstruktoren definieren, um Werte eines neuen algebraischen Datentyps zu erzeugen. I Wie bei Listen und Tupeln möglich, können Werte dieser neuen Typen dann mittels Pattern Matching wieder analysiert (dekonstruiert) werden. In der Tat ist der eingebaute Typkonstruktor [α] selbst ein algebraischer Datentyp (s. unten). 9.1 Deklaration eines algebraischen Datentyps Mittels einer data-Deklaration wird ein neuer algebraischer Datentyp spezifizert durch: O 1 den Namen T des Typkonstruktors (Identifier beginnend mit Zeichen ∈ {A . . . Z}) und seine Typparameter αj , O 2 die Namen Ki der Konstrukturfunktionen (Identifier beginnend mit Zeichen ∈ {A . . . Z}) und der Typen βik , die diese als Parameter erwarten. © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 122 Syntax einer data-Deklaration (n ≥ 0, m ≥ 1, ni ≥ 0, die βik sind entweder Typbezeichner oder βik = αj ): data T α1 α2 . . . αn = | ... | K1 K2 β11 . . . β1n1 β21 . . . β2n2 Km βm1 . . . βmnm Dieses data-Statement deklariert einen Typkonstruktor T und m Konstruktorfunktionen: Ki :: βi1 -> . . . -> βini -> T α1 α2 . . . αn Sonderfälle: I n = 0, ni = 0: data T = K1 | K2 | · · · |Km T ist damit ein reiner Aufzählungstyp wie aus vielen Programmiersprachen bekannt (etwa in C: enum). I m = 1: data T α1 . . . αn = K1 β11 . . . β1n1 T verhält sich damit ähnlich wie der Tupelkonstruktor und wird auch Produkttyp genannt. In der Typtheorie oft als β11 × β12 × · · · × β1n1 notiert. In seiner allgemeinsten Form führt die data-Deklaration also Alternativen (Typtheorie: Summe) von Produkttypen ein, bezeichnet als sum-of-product types. © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 123 Beispiel 9.1 Der benutzerdefinierte Aufzählungstyp data Weekday = Mon | Tue | Wed | Thu | Fri | Sat | Sun definiert den Typkonstruktor Weekday und die Konstruktorfunktionen Mon . . . Sun mit Mon :: Weekday . . . . Funktionen über diesem Typ werden mittels Pattern Matching realisiert: weekend weekend weekend weekend :: Weekday -> Bool Sat = True Sun = True _ = False Der vordefinierte Typ Bool ist prinzipiell ein Aufzählungstyp: data Bool = False | True Dies gilt theoretisch ebenso für die anderen Basisdatentypen in Haskells Typsystem: data Int = -2^29 | ... | -1 | 0 | 1 | ... | 2^29 - 1 data Char = ’a’ | ’b’ | ... | ’A’ | ... | ’1’ | ... © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen -- Pseudo-Code! 124 p q Bei der Arbeit mit diesen neuen Typen reagiert der Haskell-Interpreter/Compiler merkwürdig: > ::::: Mon ERROR: Cannot find "show" function for: *** expression : Mon *** of type : Weekday > ::::: Tue::::: ==:::::: Fri ERROR: Weekday is not an instance of class "Eq" O 1 O 2 O 1 Das Haskell-System hat keine Methode show für die Ausgabe von Werten des Typs Weekday mitgeteilt bekommen. Intuition: Name des Konstruktors Ki benutzen. O 2 Gleichheit auf den Elementen des Typs ist nicht definiert worden. Intuition: nur Werte die durch denselben Konstruktor Ki mit identischen Parametern erzeugt wurden, sind gleich. Haskell kann diese Intuitionen automatisch zur Verfügung stellen, wenn die data-Deklaration durch den Zusatz deriving (Show, Eq) gefolgt wird. Der neue Typ T wird damit automatisch Instanz der Typklasse Show aller druckbaren Typen und Instanz der Typklasse Eq aller Typen mit Gleichheit (==). (Der deriving-Mechanismus ist genereller und wird in Kapitel 10 näher besprochen.) x © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen y 125 Algebraische Datentypen erlauben die Erweiterung eines Typs um einen speziellen Wert, der eingesetzt werden kann, wenn Berechnungen kein sinnvolles oder ein unbekanntes Ergebnis besitzen. Beispiel 9.2 Erweitere den Typ Integer um einen “Fehlerwert” Nothing: data MaybeInt = I Integer | Nothing deriving (Show, Eq) safediv :: Integer -> Integer -> MaybeInt safediv _ 0 = Nothing safediv x y = I (x ‘div‘ y) Der folgende neue Typkonstruktor Maybe α kann jeden Typ α um das Element Nothing erweitern. Der Typkonstruktor ist polymorph (wie etwa auch [α]): data Maybe a = Just a | Nothing deriving (Show, Eq) safediv :: Integer -> Integer -> Maybe Integer safediv _ 0 = Nothing safediv x y = Just (x ‘div‘ y) © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 126 Unions sind ebenfalls durch algebraische Datentypen darstellbar (vgl. mit C’s union oder PASCALs varianten Records). Beispiel 9.3 Der Typkonstruktor Either α β konstruiert einen Union-Typ mit den zwei Diskrimanten Left und Right. getLeft filtert die mit Left markierten Elemente aus einer Liste des Union-Typs: data Either a b = Left a | Right b deriving (Show, Eq) getLeft :: [Either a b] -> [a] getLeft = foldr (\x xs -> case x of Left e -> e:xs _ -> xs) [] > ::: :t:::::::::: [Left:::::::: ’x’,:::::::::: Right:::::::::: True, ::::::::: Right:::::::::::: False, :::::::: Left:::::::: ’y’] [Left ’x’,Right True,Right False,Left ’y’] :: [Either Char Bool] > :::::::::::: getLeft ::::::::: [Left:::::::: ’x’,:::::::::: Right:::::::::: True, ::::::::: Right:::::::::::: False,:::::::: Left:::::::: ’y’] "xy" :: [Char] Frage: Welches Ergebnis liefert der Aufruf getLeft [Left ’x’, Right True, Right ’z’, Left ’y’]? © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 127 9.2 Rekursive algebraische Typen Die interessantesten Konstruktionen lassen sich durch rekursive Typ-Deklarationen erzielen. Damit lassen sich vor allem diverse Arten von Bäumen als neue Typen realisieren. Der rekursive Typ BinTree α definiert den Typ der binären Bäume über einem beliebigen Typ α: data BinTree a = | Empty Node (BinTree a) a (BinTree a) deriving (Eq, Show) Der Konstruktor Empty steht damit für den leeren (Unter-)Baum, während Node einen Knoten mit linkem Nachfolger, Knotenbeschriftung des Typs α und rechtem Nachfolger repräsentiert. Die Konstruktion eines Binärbaums mit Integer-Knotenlabels ist dann einfach: atree :: BinTree Integer atree = Node (Node Empty 1 Empty) 2 (Node (Node (Node Empty 3 Empty) 4 Empty) 6 (Node Empty 7 Empty)) © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 128 atree repräsentiert den folgenden binären Baum (ε bezeichnet leere Unterbäume): jj jjjj j j j jjjj jjjj j j j j ??? ?? ?? ? 1 ε ε 2 TTTTTTTT TTTT TTTT TTTT T o OOOO o o o OOO o o OOO oo o OOO o o O ooo ?? ? ?? ??? ?? ?? ?? ?? ? ??? ?? ?? 6 4 ε 3 ε 7 ε ε ε Um die Notation weiter zu vereinfachen, setzen wir eine Funktion zur Konstruktion von Blättern, leaf, ein: leaf :: a -> BinTree a leaf x = Node Empty x Empty Damit notieren wir atree’ mit atree’ == atree (Gleichheit sinnvoll aufgrund deriving (Eq, ...)) kürzer als atree’ :: BinTree Integer atree’ = Node (leaf 1) 2 (Node (Node (leaf 3) 4 Empty) 6 (leaf 7)) © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 129 p q Der eingebaute Typkonstruktor für Listen [ · ] ist, ganz ähnlich wie BinTree, ein rekursiver algebraischer Datentyp. Seine Definition lautet data [a] = | [] a : [a] Entgegen der bisherigen Vereinbarungen wird hier der Konstruktor K 2 = (:) in Infix-Notation gebraucht. Auch für nutzerdefinierte Konstruktorfunktionen steht dieses Feature zur Verfügung: Ein Konstruktorname der Form : [!#$&*+/<=>?@\^|~:.] ∗ (ein ’:’ gefolgt von einer beliebigen Folge von Symbolen, vgl. Abschnitt 6.2.1) kann auch infix angewandt werden. Beispiel 9.4 Mittels Infix-Konstruktoren läßt sich bspw. der hier neu definierte Typ rationaler Zahlen sehr natürlich im Programmtext darstellen: data Frac = Integer :/ Integer deriving Show > :: 2::::: :/ ::: 3 2 :/ 3 :: Frac Frage: Wieso wird hier nicht auch die Gleichheit mittels deriving (Show, Eq) abgeleitet? x © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen y 130 9.3 Bäume Wir werden im folgenden einige Algorithmen auf Bäumen näher betrachten und dabei die Anwendung algebraischer Datentypen weiter vertiefen. 9.3.1 Größe und Höhe eines Baumes Bei der Analyse von Algorithmen auf Bäumen hängt die Laufzeit oft von der Größe (Anzahl der Knoten) und Höhe (Länge des längsten Pfades von der Wurzel zu einem Blatt) eines Baumes ab. Wir definieren hier die entsprechenden Funktionen size und depth für unsere vorher deklarierten BinTrees. size, depth :: BinTree a -> Integer size Empty = 0 size (Node l a r) = size l + 1 + size r depth Empty = 0 depth (Node l a r) = 1 + depth l ‘max‘ depth r Beide Funktionen orientieren sich an der rekursiven Struktur des Typs BinTree und sehen je einen Fall für seine Konstruktoren vor (vgl. Kapitel 8 zur Listenverarbeitung). © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 131 Beweise über Algorithmen auf algebraischen Datentypen verlaufen ganz analog zu Beweisen von Aussagen über Listen (vgl. Abschnitt 8.3). 1 Für den Typ BinTree α lautet das Schema für Induktionsbeweise (jeder BinTree hat entweder Form O 2 ): oder O O 1 Induktionsverankerung: leerer Baum Empty, O 2 Induktionsschritt: von ` und r zu Node ` a r Beispiel 9.5 Zwischen der Größe und Tiefe eines Binärbaums t besteht der folgende Zusammenhang („ein Baum der Tiefe n enthält mindestens n und höchstens 2n − 1 Knoten“): depth t ≤ size t ≤ 2 ↑ depth t − 1 Wir verwenden Induktion über die Struktur von t zum Beweis des zweiten ‘≤’: Induktionsverankerung Empty: size Empty = = 0 2 ↑ depth Empty − 1 (size.1) (depth.1, Arithmetik) Falls wir uns an dieser Stelle nicht auf die Begründung Arithmetik verlassen wollen, besteht die Möglichkeit, arithmetische Operationen durch Haskell-Äquivalente zu ersetzen (bspw. ↑ durch das durch uns definierte power). © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 132 Induktionsschritt Node ` a r: = ≤ ≤ = = = size (Node ` a r) size ` + 1 + size r (2 ↑ depth ` − 1) + 1 + (2 ↑ depth r − 1) 2 × ((2 ↑ depth `) ‘max‘(2 ↑ depth r)) − 1 2 × 2 ↑ (depth ` ‘max‘ depth r) − 1 2 ↑ (1 + depth ` ‘max‘ depth r) − 1 2 ↑ depth (Node ` a r) − 1 (size.2) (Hypothese) (a ≤ a ‘max‘ b) (a ≤ b ⇒ 2 ↑ a ≤ 2 ↑ b) (Arithmetik) (depth.2) Beispiel 9.5 9.3.2 Linkester Knoten eines Binärbaumes Ein weiteres kleines Problem auf Binärbäumen besteht in der Ermittlung der Knotenmarkierung des Knotens „links außen“. Wir schreiben dazu die Funktion leftmost. leftmost kann nicht immer ein sinnvolles Ergbenis liefern: ein leerer Baum (Empty) hat keinen linkesten Knoten. Unsere Implementation von leftmost setzt daher daher den algebraischen Typkonstruktor Maybe ein, um dieses Problem evtl. signalisieren zu können. © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 133 Damit haben wir also leftmost :: Bintree α -> Maybe α leftmost :: leftmost Empty = leftmost (Node Empty a r) = leftmost (Node l a r) = BinTree α -> Maybe α Nothing Just a leftmost l Eine alternative Formulierung wäre leftmost’, die zuerst rekursiv in den Baum absteigt, einen evtl. linkesten Knoten (Just b) nach oben propagiert bzw. den aktuell linkesten Knoten zurückgibt (Just a), wenn der linke Teilbaum leer sein sollte: leftmost’ :: BinTree α -> Maybe α leftmost’ Empty = Nothing leftmost’ (Node l a r) = case leftmost’ l of Nothing -> Just a Just b -> Just b Frage: Welcher Variante würdet ihr den Vorzug geben? © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 134 Die folgende Variante des Problems ermittelt uns das Element links außen, gibt aber gleichzeitig auch den Baum zurück, der bei Entfernung des „Linksaußen“ entsteht: splitleftmost’ :: BinTree α -> Maybe (α, BinTree α) splitleftmost’ Empty = Nothing splitleftmost’ (Node l a r) = case splitleftmost’ l of Nothing -> Just (a, r) Just (a’,l’) -> Just (a’, Node l’ a r) Übung: splitleftmost’ orientiert sich an dem Rekursionsschema für leftmost’ und nicht an dem für leftmost. Die ganze Arbeit beim rekursiven Abstieg in den Baum zu leisten ist schwieriger. Wie könnte eine endrekursive Variante splitleftmost implementiert werden? Nicht ganz einfach. 9.3.3 Linearisierung von Bäumen Dieser Abschnitt befaßt sich mit der Überführung von Bäumen in Listen von Knotenmarkierungen. Wir werden sowohl Tiefendurchläufe als auch Breitendurchläufe ansprechen. © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 135 O 1 Tiefendurchlauf Tiefendurchläufe folgen der rekursiven Struktur unserer Binärbäume und sind vergleichsweise simpel zu implementieren. Je nachdem, ob man die Markierung eines Knotens (a) vor, (b) zwischen oder (c) nach der Linearisierung seiner Teilbäume ausgibt, erhält man verschiedene Tiefendurchläufe: (b) Inorder : inorder inorder Empty inorder (Node ` a r) :: = = BinTree α -> [α] [] inorder ` ++ [a] ++ inorder r Die entscheidenden Gleichungen von (a) Preorder und (c) Postorder lauten preorder (Node ` a r) postorder (Node ` a r) = = [a] ++ preorder ` ++ preorder r postorder ` ++ postorder r ++ [a] Beispiel: inorder atree _ [1,2,3,4,6,7]. Die Effizienz von inorder wird durch die Laufzeit der Listenkonkatenation ++ bestimmt, die linear im ersten Argument ist (siehe Abschnitt 8.2). Der worst-case für inorder ist somit ein linksentarteter Baum. © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 136 Die Funktion leftist erzeugt einen linksentarteten Baum aus einer Liste von vorgegebenen Knotenmarkierungen: leftist :: [α] -> BinTree α leftist [] = Empty leftist (x:xs) = Node (leftist xs) x Empty Aufgrund der Laufzeit von ++ benötigt inorder (leftist [1..n]) eine Laufzeit proportional zu n 2. Übung: Für inorder läßt sich eine Implementation finden, die linear in n ist. Die Lösung orientiert sich an der Idee zur Beschleunigung von reverse aus Abschnitt 8.2. O 2 Breitendurchlauf Ein Breitendurchlauf eines Baumes zählt die Knoten ebenenweise von der Wurzel ausgehend auf. Wir setzen dazu eine Hilfsfunktion traverse ein, die eine Liste ts von Teilbäumen (einer Ebene) erhält, und deren Knoten entsprechend aufzählt: traverse traverse [] traverse ts :: = = [BinTree α] -> [α] [] roots ts ++ traverse (sons ts) Unseren Breitendurchlauf erhalten wir dann einfach mittels levelorder t = traverse [t] © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 137 Es fehlen lediglich noch die Funktionen roots zur Bestimmung aller Wurzeln bzw. sons zur Bestimmung aller Teilbäume einer Liste von Bäumen: roots :: roots [] = roots (Empty :ts) = roots ((Node _ a _):ts) = sons sons [] sons (Empty :ts) sons ((Node l _ r):ts) [BinTree α] -> [α] [] roots ts a : roots ts :: [BinTree α] -> [BinTree α] = [] = sons ts = l : r : sons ts Mit der Hilfe von list comprehensions (siehe Abschnitt 8.5), lassen sich beide Funktionen elegant als Einzeiler realisieren: roots ts = [ a | Node _ a _ <- ts ] sons ts = [ t | Node l _ r <- ts, t <- [l, r] ] © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 138 9.3.4 fold über Bäumen Das in Abschnitt 8.1 besprochene allgemeine Rekursionsschema über Listen (implementiert durch foldr/foldl) läßt sich auf andere konstruierte algebraische Datentypen übertragen. foldr ( ⊕ ) z xs ersetzt in der Liste xs die Vorkommen der Listenkonstruktoren (:) durch Aufrufe von ⊕ bzw. [] durch z. Ganz analog läßt sich eine Funktion tfold (tree fold ) über BinTrees definieren: tfold tfold f z Empty tfold f z (Node ` a r) (β -> α -> β -> β) -> β -> BinTree α -> β z f (tfold f z `) a (tfold f z r) :: = = Der Effekt von tfold auf den Binärbaum atree aus den vorigen Abschnitten ist damit: f 2TTTTTT f 1 tfold f z atree _ z jjj jjjj j j j jjj jjjj ?? ?? ?? z TTTT TTTT TTT f 6OOO f 4 ?? ?? ?? z f 3 z © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen ooo ooo o o oo OOO OOO O f 7 z ?? ?? ?? z ?? ?? ?? z 139 Die rekursiven Funktionen size und depth aus Abschnitt 9.3 können alternativ durch tfold implementiert werden: size, depth :: BinTree α -> Integer size = tfold (\l _ r -> l + 1 + r) 0 depth = tfold (\l _ r -> 1 + l ‘max‘ r) 0 Die Tiefendurchläufe können ebenfalls als Instanzen von tfold verstanden werden: inorder, preorder, postorder :: BinTree α -> [α] inorder = tfold (\l a r -> l ++ [a] ++ r) [] preorder = tfold (\l a r -> [a] ++ l ++ r) [] postorder = tfold (\l a r -> l ++ r ++ [a]) [] Schließlich ist auch leftmost’ mittels tfold ausdrückbar: leftmost’ :: BinTree α -> Maybe α leftmost’ = tfold (\l a r -> case l of Nothing -> Just a Just b -> Just b) Nothing © 2003 T. Grust · Funktionale Programmierung: 9. Algebraische Datentypen 140