Funktionale Programmierung mit Haskell/ Typdefinitionen

In einem der ersten Kapitel wurden bereits einige Typen besprochen: Int, Integer, Float und Double sind Numerische Typen, Char ist ein alphanumerischer Typ und Bool ist ein Aufzählungstp mit den Ausprägungen TRUE und FALSE. Selbstverständlich gibt es in Haskell die Möglichkeit, weitere Typen zu definieren (die übrigens alle mit einem Großbuchstaben beginnen müssen). Dafür gibt es folgende Anweisungen:

  • type deklariert neue Typnamen auf der Basis bestehender Datentypen
  • data deklariert neue Datentypen
  • newtype ist eine Mischung aus den obigen Anweisungen und dient mehr der Optimierung. Er wird hier nicht näher erläutert.

Die type-Deklaration

Bearbeiten

Wie oben erklärt, werden mit type neue Typnamen auf der Basis bestehender Datentypen deklariert. Auf diese Weise werden Synonyme für bestimmte Datentypen geschaffen, z.B.

Deklaration von Datentypen mittels type in ghci
Prelude>-- Eine Liste von Zeichen (Char) wird auch ein String genannt (diese Definition ist bereits in Haskell hinterlegt)
Prelude> type String = [Char]

Prelude> -- Hier wird ein Typ Tag geschaffen:
Prelude> type Tag = Int
Prelude> let a=6::Tag
Prelude> a
6
Prelude> :t a
a :: Tag
Prelude>

Diese Definition scheint nicht viel zu bewirken: Statt [Char] schreibt man nun eben String und statt Int Tag. Die neuen Typen sind also nur Synonyme für bereits bestehenden Typen.

Trotzdem hat diese Namensvergabe einen wichtigen Grund: eine Funktion, deren Eingabeparameter vom Typ Tag ist, wird nie einen Wert vom Typ Int als Eingabe akzeptieren, obwohl dies doch technisch keinen Unterschied macht. Diese strenge Typisierung in Haskell ist eine wertvolle Hilfe bei der Fehlervermeidung, da nur Werte zusammenkommen, die per Definition zusammen gehören.

Die Zuweisung let a=6::Tag legt fest, dass die Variable a nicht nur den Wert 6 bekommt, sondern auch den Typ Tag. Die Zeichenfolge :: kann man lesen als ist vom Typ.

Deklaration von Datentypen mittels type in ghci
Prelude> type Tag = Int
Prelude> type Monat = Int
Prelude> type Jahr = Int
Prelude> type Datum = (Jahr, Monat, Tag) -- Definition des Datentyps Datum
Prelude> let a= (2010,8,15)::Datum -- Zuweisung eines Werts mit dem Datentyp Datum
Prelude> a
(2010,8,15)
Prelude> let b=(20,8)::Datum -- Zuweisung eines falschen Werts

<interactive>:15:7:
    Couldn't match type `(t0, t1)' with `(Jahr, Monat, Tag)'
    Expected type: Datum
      Actual type: (t0, t1)
    In the expression: (20, 8) :: Datum
    In an equation for `b': b = (20, 8) :: Datum

Prelude> let c = (2010, 10, '1')::Datum -- Ein Datum mit einen Char am Ende ist nicht erlaubt

<interactive>:17:20:
    Couldn't match type ‘Char’ with ‘Int’
    Expected type: Tag
      Actual type: Char
    In the expression: '1'
    In the expression: (2010, 10, '1') :: Datum
    In an equation for ‘c’: c = (2010, 10, '1') :: Datum
Prelude>

Bei der Zuweisung let b=(20,8)::Datum erfüllt der Datentyp Datum schon seinen Zweck: Ein 2er-Tupel lässt sich nicht als Datum definieren. Würde diese Zuweisung in einem Programm stehen (und nicht wie hier im interaktiven Modus), dann würde der Compiler einen Fehler erkennen und abbrechen. So sorgt Haskell für Typsicherheit.


Die data-Deklaration

Bearbeiten

Bool ist, wie bereits erwähnt, definiert durch data Bool = False | True. Das | steht für das Wort oder. Für diesen Typ kommen also nur Werte in Frage, die als Aufzählungstypen definiert wurden. Bei der Verwendung dieses Typs in einem Funktionsaufruf muss also für jede Ausprägung eine eigene "Version" der Funktion vorliegen, wie hier bei der Funktion nicht, die einen Wahrheitswert ins Gegenteil umdreht:

Datei negation.hs
nicht True = False
nicht False = True

Der Dialog ist:

die <code>nicht</code>-Funktion
Prelude> :l negation.hs 
[1 of 1] Compiling Main             ( negation.hs, interpreted )
Ok, modules loaded: Main.
Prelude> nicht True
False
Prelude> nicht False
True

Die Zuordnung zur richtigen "Version" der Funktion heißt Pattern Matching. Würde eine der Zeilen fehlen, dann würde ghci einen Fehler Non-exhaustive patterns, also "nicht ausreichende Muster" melden.

Hier ein zweites Beispiel anhand eines Skatspiels, mit einer Funktion, die den Kartenwert ermittelt:

Definition eines Skatspiels
Prelude> data Wert = Sechs|Sieben|Acht|Neun|Zehn|Bube|Dame|Koenig|As
Prelude> data Farbe = Kreuz|Herz|Pik|Karo
Preldue> type Karte = (Farbe, Wert)
Haskell-Programm in Datei kartenwert.hs
data Wert  = Sechs|Sieben|Acht|Neun|Zehn|Bube|Dame|Koenig|As
data Farbe = data Farbe = Kreuz|Herz|Pik|Karo
type Karte = (Farbe, Wert)

kartenwert (_,Sechs) = 0
kartenwert (_,Sieben) = 0
kartenwert (_,Acht) = 0
kartenwert (_,Neun) = 0
kartenwert (_,Zehn) = 10
kartenwert (_,Bube) = 2
kartenwert (_,Dame) = 3
kartenwert (_,Koenig) = 4
kartenwert (_,As) = 11

So wird der Kartenwert abgefragt:

Definition eines Skatspiels
Prelude> :l kartenwert.hs 
[1 of 1] Compiling Main             ( kartenwert.hs, interpreted )
Ok, modules loaded: Main.
Prelude> kartenwert (Herz, Dame)
3
Prelude> kartenwert (Karo, Sieben)
0

Da die Farbe einer Karte bei der Kartenwertermittlung egal ist, kann anstelle der Farbe die Wildcard gesetzt werden.







Die :info-Anweisung

Bearbeiten

Zum besseren Verständnis von type und data hilft die :info-Anweisung des ghci, abgekürzt :i:

Abfragen mit der :info-Anweisung in ghci
Prelude> :info Bool
data Bool = False | True 	-- Defined in `GHC.Types'
instance Bounded Bool -- Defined in `GHC.Enum'
instance Enum Bool -- Defined in `GHC.Enum'
instance Eq Bool -- Defined in `GHC.Classes'
instance Ord Bool -- Defined in `GHC.Classes'
instance Read Bool -- Defined in `GHC.Read'
instance Show Bool -- Defined in `GHC.Show'

Prelude> :info False
data Bool = False | ... 	-- Defined in `GHC.Types'

Prelude> :i True
data Bool = ... | True 	-- Defined in `GHC.Types'

Prelude> :info String
type String = [Char] 	-- Defined in `GHC.Base'

Prelude> :i []          -- Definition der Haskell-Liste
data [] a = [] | a : [a] 	-- Defined in `GHC.Types'
instance Eq a => Eq [a] -- Defined in `GHC.Classes'
instance Monad [] -- Defined in `GHC.Base'
instance Functor [] -- Defined in `GHC.Base'
instance Ord a => Ord [a] -- Defined in `GHC.Classes'
instance Read a => Read [a] -- Defined in `GHC.Read'
instance Show a => Show [a] -- Defined in `GHC.Show'
Prelude>

Die :info-Anweisung gibt nützliche Hinweise darauf, wo und wie die Typen definiert sind:

  • Bool ist ein Aufzählungstyp, der aus den Ausprägungen False und True besteht. Die pipe | zwischen den Ausprägungen steht für das Oder. In Haskell sind diese Ausprägungen type constructors. Die Definition des Typs Bool steht in der Datei GHC.Types.
  • Die darunter stehenden Zeilen instance Bounded Bool -- Defined in `GHC.Enum' usw. zeigen, dass dem Bool-Typ die Eigenschaften der Typklasse Bounded usw. zugeordnet sind. Was das bedeutet, wird weiter unter erläutert.
  • Mit :info lässt sich auch abfragen, in welcher Typklasse die type constructor False bzw True definiert sind, und dass sie zum Typ Bool gehören.
  • Die Abfrage :info String zeigt, dass ein String in Haskell nichts anderes ist als eine Liste von Char. Deshalb lassen sich die Listen-Funktionen auch so einfach auf Strings anwenden.
  • Die letzte Abfrage auf die Liste [] sagt uns: Eine Liste ist entweder leer ([]) oder sie besteht aus a, gefolgt von einer Liste  : [a]. Es ist also eine rekursive Definition, und wir haben hier auch wieder den Doppelpunkt-Operator, den wir schon bei den Listen kennen gelernt haben. Auf die instance-Anweisungen kommen wir weiter unten zu sprechen.