Abstrakte Datentypen und das Typsystem von Haskell

... [Seminar "Funktionale Programmierung"] ... [Inhaltsübersicht] ... [Higher-Order Functions] ... [Typinferenz] ...


5. Typklassen


5.1. Wozu?

Im Kapitel über die Polymorphie wurde schon festgestellt, dass polymorphe Datentypen eine Obermenge aller möglichen Typen darstellen. Sie stellen also die allgemeinste Form eines Datentypen dar. Im Gegensatz dazu wurden schon konkrete oder auch monomorphe Datentypen vorgestellt, die selbst sehr speziell sind, wie zum Beispiel der Int -Typ. Nun kann es sein, dass meine Anforderungen einen Datentypen verlangen, welcher nicht so allgemein gefaßt ist, wie ein polymorpher Datentyp, aber auch nicht so speziell ausgeprägt ist, wie ein konkreter Datentyp. So möchte ich zum Beispiel eine Funktion zum Addieren zweier Werte nicht für alle möglichen Datentypen und auch nicht für einen konkreten Datentypen definieren, sondern nur für alle möglichen typen die Zahlen darstellen. Gesucht wird also eine Möglichkeit, die einen Typ liefert, der sich zwischen der generellen polymorphen Typdefinition und der speziellen konkreten Typdefinition bewegt.

(+) :: a -> a -> a            -- Zu generell
(+) :: Int -> Int -> Int -- Zu speziell

Möglich ist dies mit Typklassen. Mit Hilfe der Typklasse kann ich festlegen, dass der Typ, für den ich meine Funktion definiere, bestimmte Eigenschaften vorweist.

(+) :: Num a => a -> a -> a 

Das => Symbol gibt an, dass der Typ von a Mitglied einer bestimmten Klasse sein muß, damit er mit dieser Funktion arbeiten darf. In dem Beispiel oben muß der Typ von a Mitglied der Klasse Num sein, einer Klasse für alle Zahlen, damit dieser addiert werden darf. Die in Haskell eingebauten Datentypen Float und Int sind beide Mitglied der Klasse Num , ließen sich also addieren. Haskell bietet noch weitere vordefinierte Klassen, wie zum Beispiel Eq, um sicherzustellen, dass der Typ Äquivalenztests durchführen kann oder entsprechend die Klasse Ord für Vergleichsoperationen.

5.2. Eigene Klassen definieren

Um nun nicht stets mit den schon vordefinierten Klassen arbeiten zu müssen, ist es möglich, eigene zu definieren. Als Beispiel wird hier die Klasse für Äquivalenztests gebaut.

class Eq a where
(==) :: a -> a -> Bool

Diese Klasse trägt den Namen Eq und soll all diejenigen Typen zusammenfassen, für die der == -Operator definiert ist. Damit ist erstmal die Klasse Eq bekannt gemacht worden. Allerdings hat diese noch keine Mitglieder. Um nun ein Datentyp Mitglied der Klasse werden zu lassen, muß eine Instanz von dieser Klasse über den gewünschten Datentyp gebildet werden. Für einen einfachen Datentyp wie Int könnte dies so aussehen:

instance Eq Int where
x == y = intEq x y

Die Funktion intEq ist dabei die grundlegene Funktion zum Vergleichen von Int -Werten. Auch für den schon aus Kapitel 2 bekannten Datentyp Tree läßt sich eine solche Instanz bilden.

data Tree a = Leaf a | Branch Tree a Tree a

instance Eq a => Eq (Tree a) where
Leaf a == Leaf b = a == b
Branch l1 r1 == Branch l2 r2 = l1==l2 && r1==r2
_ == _ = False

Der == -Operator wurde hier mehrfach implementiert. Je nachdem welches Muster paßt, also ob Leaf mit Leaf oder Branch mit Branch verglichen werden soll, wird der hierzu entsprechende Ausdruck ausgewertet. _ == _ ist ein Muster, welches auf alle Gleichheitsoperationen paßt. Da auch der Datentyp ausgewertet wird, mit dem die Baumstruktur aufgebaut ist und dieser hier ja polymorph definiert ist, muß auch angegeben werden, dass seine mögliche spätere Ausprägung eine Instanz der Klasse Eq besitzten muß. Dies wird mit dem Ausdruck Eq a => Eq (Tree a) sichergestellt.

Wird der Gleichheitsoperator nun in einer Funktion benutzt, so geschieht folgendes: Das Typsystem von Haskell überprüft, mit welchem Datentyp die Vergleichsoperation arbeiten soll. Dann wird die entsprechende Instanz zu diesem Datentyp herausgesucht und die Operation aus der Funktion mit der aus der Instanz überladen. Das Überladen von Funktionen ist eine Eigenschaft der Ad-hoc Polymorphie, welche eigentlich vom Hindley-Milner Typsystem ausgeschlossen ist. In Haskell wurde das Hindley-Milner Typsystem allerdings so erweitert, dass auch eine Ad-hoc Polymorphie mit Hilfe der Typklassen möglich ist.

Einer class-Definition lassen sich noch "default functions" mitgeben. Diese Funktionen werden verwendet, falls in der Instanz dieser Operator nicht definiert ist. Für die Klasse Eq läßt sich neben dem == -Operator noch der /= - Operator implementieren, mit dem zwei Werte auf "Ungleichheit" überprüft werden. Der /= -Operator läßt sich mit Hilfe des == -Operators zusammenbauen.

class Eq a where 
(==),(/=) :: a -> a -> Bool
x /= y = not (x==y)
x == y = not (x/= y)

Die obigen Eq Instanzen von Int und Tree sind mit dieser Definition immer noch gültig, besitzen jetzt allerdings auch den /= -Operator. Dieser ist aufgrund der "default function" x /= y = not (x==y) definiert.

5.3. Vererbung

Klassen lassen sich vererben, um so Operationen, die schon in anderen Klassen definiert wurden, zu übernehmen und um neue zu erweitern. So könnte die oben definierte Eq -Klasse noch um Operationen, wie <, >, <= oder >= erweitert werden. Um die <= und >= -Operationen zu implentieren, ist es auch nötig zu wissen, wie sich Werte auf Gleichheit überprüfen lassen. Das ist genau das, was die Klasse Eq schon kann. Also wird eine neue Klasse Ord geschaffen, welche von Eq erbt.

class Eq a => Ord a where
(<),(<=),(>=),(>) :: a -> a -> Bool
max, min :: a -> a -> a

Eine Instanz dieser Klasse kann so aussehen:

instance Ord a => Ord (Tree a) where
Leaf _ < Branch _ _ = True
Leaf a < Leaf b = a < b
Branch _ _ < Leaf _ = False
Branch l1 r1 < Branch l2 r2 = l1 < l2 && r1 < r2
t1 <= t2 = t1 < t2 || t1 == t2
. . .
Die > und >= -Operatoren lassen sich ähnlich definieren.

5.4. Ableitungen

Das Notieren der Instanzen für viele Datentypen ist sehr mühselig, und Haskell bietet eine möglichkeit, sich hier Arbeit zu sparen. Wird ein neuer Datentyp erstellt, so läßt sich dieser von einer Klasse oder mehreren Klassen ableiten. Die Instanz zu diesem neuen Datentyp wird dabei automatisch gebildet. Die Klassen, von denen der Datentyp ableiten soll, werden in der data-Deklaration hinter dem Wort deriving angegeben. Der Tree ließe sich mit einer solchen Ableitung so deklarieren:

data Tree a = Leaf a | Branch Tree a Tree a
deriving Ord

Die Instanz, die oben noch von Hand notiert wurde, wird hier nun automatisch erzeugt.

Oft ist bei Datentypen auch eine Ableitung zu der Klasse Show zu sehen. Damit läßt sich eine Variable jenes Typs auf dem Bildschirm ausgeben. Möchte ich meinen Baum auf dem Bildschirm ausgeben, so leite ich diesen zusätzlich von Show ab.

data Tree a = Leaf a | Branch Tree a Tree a
deriving (Ord,Show)

... [Seminar "Funktionale Programmierung"] ... [Inhaltsübersicht] ... [Higher-Order Functions] ... [Typinferenz] ...