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 Datentypendata
deklariert neue Datentypennewtype
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.
type
in ghciPrelude>-- 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.
type
in ghciPrelude> 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:
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)
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
:
:info
-Anweisung in ghciPrelude> :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ägungenFalse
undTrue
besteht. Die pipe | zwischen den Ausprägungen steht für das Oder. In Haskell sind diese Ausprägungen type constructors. Die Definition des TypsBool
steht in der DateiGHC.Types
.- Die darunter stehenden Zeilen
instance Bounded Bool -- Defined in `GHC.Enum'
usw. zeigen, dass demBool
-Typ die Eigenschaften der TypklasseBounded
usw. zugeordnet sind. Was das bedeutet, wird weiter unter erläutert. - Mit
:info
lässt sich auch abfragen, in welcher Typklasse die type constructorFalse
bzwTrue
definiert sind, und dass sie zum TypBool
gehören. - Die Abfrage
:info String
zeigt, dass einString
in Haskell nichts anderes ist als eine Liste vonChar
. 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 ausa
, 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 dieinstance
-Anweisungen kommen wir weiter unten zu sprechen.