Abstrakte Datentypen und das Typsystem von Haskell

... [Seminar "Funktionale Programmierung"] ... [Inhaltsübersicht] ... [Typen in Haskell] ... [Higher-Order Functions] ...

3. Polymorphie


3.1. parametrische Polymorphie

Oftmals gibt es Funktionen, deren Algorithmus nicht von einem speziellen Datentyp abhängig ist, sondern mit jedem beliebigen Datentyp arbeiten kann. So ist zum Beispiel das Ermitteln der Länge einer Liste unabhängig davon, mit welchem konkreten Datentyp die Liste aufgebaut ist. Es ist nur nötig, die Anzahl der Listenelemente zu zählen. Das Hindley-Milner Typsystem, auf welchem Haskell basiert, bietet hierfür polymorphe Datentypen an. Der polymorphe Datentyp ist dabei ein Stellvertreter für einen beliebigen, aber noch unbestimmten, konkreten Datentyp. Er stellt somit eine Obermenge aller möglichen Typausprägungen dar. In der Syntax von Haskell unterscheiden sich polymorphe Datentypen von konkreten Datentypen dadurch, dass sie mit einem kleinen Buchstaben beginnen. Hier ist ein sehr einfaches Beispiel der Identitätsfunktion mit einem polymorphen Datentyp:

id :: a -> a
id x = x

Das a in der Signatur kann also für jeden belibiegen Datentyp stehen.
Die schon oben angesprochene Längenfunktion für eine Liste ist bei Haskell folgendermaßen implementiert:

length :: [a] -> Int
length [] = 0
length (x:xs) = 1 + length xs

Auch hier ist die Funktion nicht von einem konkreten Datentyp abhängig, sondern kann mit beliebigen verwendet werden. Wird ein konkreter Datentyp nun auf die length-Funktion angewendet, so nimmt der polymorphe Datentyp a eine konkrete Ausprägung an.

length ['c','d']

Die Signatur läßt sich nun folgendermaßen notieren

length :: [Char] -> Int
Das Verwenden von polymorphen Datentypen in den formalen Parametern oder des Funktionsergebnisses der Typsignatur ist das, was die parametrische Polymorphie ausmacht.

3.2. Ad-hoc Polymorphie

Die Ad-hoc Polymorphie, wird dadurch bestimmt, dass nicht der Algorithmus einer zu definierenden Funktion mit einem beliebigen konkreten Datentyp arbeiten kann, sondern dass verschiedene konkrete Datentypen unterschiedliche Algorithmen benötigen. So gibt es in Java den + -Operator, der zum Einen zwei Zahlen addiert und andererseits zwei Strings miteinander verknüpft. Das Addieren von Zahlen und das Zusammenfügen von Strings ist dabei so verschieden, dass es sich mit einer parametrischen Polymorphie nicht lösen ließe. Der + -Operator ist also zweimal zu implementieren: einmal für Zahlen, einmal für Strings. Wird dieser + -Operator verwendet, so muß entschieden werden, welcher Datentyp vorliegt, eine Zahl oder ein String, und dann die entsprechende Implementierung gewählt werden. Der Operator wird also stets überladen.

Das Hindley-Milner Typsystem kann von sich aus nur eine parametrische Polymorphie und unterstützt keine Ad-hoc Polymorphie. In Haskell wurde das Typsystem allerdings erweitert, sodass auch Ad-hoc Polymorphie möglich ist. Näheres dazu gibt es in dem Kapitel über Typklassen.


3.3. Typparameter

Im vorherigen Kapitel wurde in einem Beispiel eine Datenstruktur für einen Baum notiert.

data Tree = Leaf Int | Branch Tree Tree

In diesem Baum können allerdings nur Werte vom Typ "Int" abgelegt werden. Da sich dieselben Baumstrukturen auch mit Char, Float oder anderen Datentypen bilden lassen, bietet es sich an, auch hier mit polymorphen Datentypen zu arbeiten. Der Typdeklaration wird ein Typparameter mitgegeben, welcher polymorph ist und auch hier wieder ein Stellvertreter für einen konkret ausgeprägten Datentyp darstellt. Der Baum sieht dann so aus:

data Tree a = Leaf a | Branch Tree a Tree a
aTree :: Tree Int
aTree = Branch (Branch (Leaf 5) (Leaf 2)) (Leaf 6)

In der Signatur von aTree ist zu sehen, welcher konkrete Datentyp der Typvariablen von Leaf in diesem Baum zugewiesen wird. Hier ist es ein Int.

3.4. Zu Haskells vordefinierten Typen

Typen, die im Grundsystem von Haskell mitgeliefert werden, stellen im Grunde nichts Besonderes dar. Sie lassen sich mit den bisher kennengelernten Methoden annähernd abbilden. So könnte die Definition eines "Char"-Typs so ausehen:

data Char = 'a' | 'b' | 'c' | 'd' ...      --kein gültiger Haskell-Code!!!
'A' | 'B' | 'C' | 'D' ...
'1' | '2' | '3' | '4' ...

Allerdings ist diese Notation nicht haskellkonform und ist nicht übersetzbar. Aber sie zeigt, dass der "Char"-Datentyp nichts anderes als ein Aufzählungstyp ist, wie er schon in Kapitel 2 erwähnt wurde. Ebenso läßt sich eine mögliche Definition von Tupeln betrachten:

data (a,b)     = (a,b)       --Pseudo-Code!!!!!
data (a,b,c) = (a,b,c)
data (a,b,c,d) = (a,b,c,d)
. . .

Oder von Listen

data [a] = [] | a : [a]      --Pseudo-Code!!!!!!
Aus diesem Pseudo-Code wird auch klar, wo der Unterschied zwischen einer Liste und einem Tupel besteht. Die Elemente einer Liste basieren jeweils auf dem selben konkreten Datentyp. Die einzelnen Elemente eines Tupels können jeweils verschiedene konkrete Datentypen darstellen.


... [Seminar "Funktionale Programmierung"] ... [Inhaltsübersicht] ... [Typen in Haskell] ... [Higher-Order Functions] ...