9 Algebraische Datentypen 9.1 Deklaration eines algebraischen

Werbung
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
Herunterladen