Funktionale Programmierung mit Haskell/ Typen von Funktionen
In einer funktionalen Programmiersprache wie Haskell haben Funktionen keine Parameterleisten, sondern type signatures. Sie lassen sich über ghci mit :type
oder :t
ermitteln.
Aufbau der type signature
[Bearbeiten]Bei der bereits bekannten Funktion head
, die den ersten Wert einer Liste liefert, sieht die type signature so aus:
head
und ihre ParameterleistePrelude> head [1,2,3,4]
1
Prelude> :type head
head :: [a] -> a
Prelude>
Erläuterung:
- Die Zeichenfolge
::
ist zu lesen als 'ist vom Typ' [a] -> a
steht für: Die Funktionhead
wird angewandt auf eine Liste mit Elementen, die vom Typa
sind (der Typa
darf nicht mit einer Variablen verwechselt werden; es ist ein nicht weiter spezifiziert Typ wie etwaBool
oderChar
). Das Ergebnis ist ein einzelner Wert vom Typa
(in unserem Fall derInt
-Typ 1).
Beim Entwurf von add
, einer Funktion, die zwei Werte addiert, erscheint folgende type signature:
add
und ihre ParameterleistePrelude> let add a b = a + b
Prelude> add 5.5 6
11.5
Prelude> :type add
add :: Num a => a -> a -> a
Prelude>
Erläuterung:
- Die Zeichenfolge
::
ist zu lesen als 'ist vom Typ' Num a =>
steht für: 'Im Folgenden steht a für die TypklasseNum
, also für jede Zahl. Im Umkehrschluss heißt das, dassadd
bei Typklassen wieString
oderBool
nicht anwendbar ist.a -> a -> a
bedeutet, es gibt zwei Eingabeparameter aus der TypklasseNum
und einen Ausgabeparameter, ebenfalls aus der TypklasseNum
. Bei Haskell ist stets genau der letzte Wert der Ausgabeparameter, alle vorhergehenden Werte sind Eingabeparameter.
Die Funktionen fst
und snd
werden auf Tupel angewandt und Tupel können Werte verschiedener Typen besitzen:
Prelude>-- Auch Tupel haben Typen wie Funktionen!
Prelude>-- Dieser Tupel besteht aus einem String (also einer Liste von Char) und einem Numerischen Wert:
Prelude> :t ("drei", 3)
("drei", 3) :: Num t => ([Char], t)
Prelude> fst ("drei", 3)
"drei"
Prelude> snd ("drei", 3)
3
Prelude>-- Da fst immer den ersten Wert liefert, ist der Ausgabeparameter stets ein Wert vom Typ a (hier: [Char])
Prelude> :t fst
fst :: (a, b) -> a
Prelude>-- Da snd immer den zweiten Wert liefert, ist der Ausgabeparameter stets ein Wert vom Typ b (hier: Num)
Prelude> :t snd
snd :: (a, b) -> b
Prelude>
Currying
[Bearbeiten]Der Typ add :: Num a => a -> a -> a
für eine Additionsfunktion ist nicht auf den ersten Blick verständlich, denn hier wird scheinbar nicht zwischen Ein- und Ausgabeparametern unterschieden. Doch eine Haskell-Funktion wird nach dem Currying-Verfahren abgearbeitet. Dies bedeutet, daß jede Funktion "eigentlich" nur auf ein Argument angewandt wird und dann eine neue Funktion liefert, die daraufhin auf das nächste Argument angewandt wird. Eine Funktion add 5 3
wird also in die Funktion add 5
verwandelt, was soviel bedeutet wie "Addiere die Zahl 5 auf den nächsten Parameter". Diese Funktion wird dann auf die Zahl 3
angewandt, worauf das Ergebnis 8 zurückgegeben wird. Daraus ergibt sich, dass eine Haskell-Funktion immer beliebig viele Eingangsparameter besitzt, aber stets nur einen Ausgabeparameter. Die Zeichenfolge ->
kann dann gelesen werden als "wird angewandt auf".
Konstanten
[Bearbeiten]Eine Funktion die keinen Eingangsparameter besitzt, entspricht einer Konstanten. Ein Beispiel dafür ist die Zahl Pi ():
Prelude> :type pi
pi :: Floating a => a
Prelude> pi
3.141592653589793
Prelude>
Typen von High Order Functions
[Bearbeiten]Bei High Order Functions map
werden Funktionen als Eingabeparameter übergeben. Zum Beispiel kann die Funktion (*3)
auf eine Liste angewandt werden:
Prelude> :t (*3)
(*3) :: Num a => a -> a
Prelude> map (*3) [2,3,4,5]
[6,9,12,15]
Prelude> :t map (*3)
map (*3) :: Num b => [b] -> [b]
Prelude> :t odd
odd :: Integral a => a -> Bool
Prelude> map (odd) [2,3,4,5]
[False,True,False,True]
Prelude> :t map (odd)
map (odd) :: Integral a => [a] -> [Bool]
Prelude> :t map
map :: (a -> b) -> [a] -> [b]
Prelude>
Erläuterung:
- Die Funktion
(*3)
verwandelt, wie oben beschrieben, einen Numerischen Wert (Num
) in einen anderen Numerischen Wert - Wenn also
(*3)
mittelsmap
auf eine Liste angewandt wird, wird eine Liste Numerischer Werte eingegeben, und eine Liste Numerischer Werte ist das Ergebnis, also
Num b => [b] -> [b]
- Bei einer Funktion wie
odd
, die aussagt, ob eine ganze Zahl ungerade ist oder nicht, ist der Eingabetyp nicht gleich dem Ausgabetyp. Daher ist beimap (odd)
der Eingabeparameter eine Liste mit ganzzahligen Werten ([Integral]
) und der Ausgabeparameter eine Liste mit boolschen Werten ([Bool]
) - Im allgemeinen Fall benötigt
map
also eine Funktion mit einem Eingabeparametera
und einem Ausgabeparameterb
, und einem weiteren Eingabeparameter mit einer Liste vom Typa
. Das Ergebnis ist eine Liste vom Typb
. Das Neue an dieser Typdeklaration ist die Angabe eines Funktionstypen in der Eingabeparameterleiste in Klammern.
Prelude>-- zip verbindet die Inhalte zweier Funktionen zu Tupeln:
Prelude> zip [1,2,3,4] [True, False, True, False]
[(1,True),(2,False),(3,True),(4,False)]
Prelude> :t zip
zip :: [a] -> [b] -> [(a, b)]
Prelude>-- zipWith hat als ersten Wert eine Funktion mit zwei Eingabeparameter a und b und einen Ausgabeparameter c
Prelude>-- sowie zwei Listen passenden Typs. Das Ergebnis ist eine Liste von Typen, die dem Ausgabetyp der Funktion entsprechen
Prelude> :t zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
Prelude> zipWith (*) [1,2,3,4] [10,20,30,40]
[10,40,90,160]
Prelude> zipWith (/) [1,2,3,4] [10,20,30,40]
[0.1,0.1,0.1,0.1]
Prelude>
Automatische Anlage der type signature
[Bearbeiten]Beim Schreiben einer Funktion ist es den Autor überlassen, ob er die type signature angibt. Beim Entwurf von add
wurde keine type signature angegeben. Der Compiler erkennt von selbst, dass Num
die naheliegendste Typklasse ist.
Nach der Einführung der Typklassen kann der Blick auf die Parameter von Funktionen gelegt werden. Hier einige Funktionen und deren type signatures (Parameterleisten):
Funktionen mit beliebigen Typen
[Bearbeiten]Prelude>-- Die Multiplkation mit drei kann mit jedem Numerischen Wert gemacht werden,
Prelude>-- daher legt Haskell Num als Ein- und Ausgabeparameter fest:
Prelude> :t (*3)
(*3) :: Num a => a -> a
Prelude>-- Bei einer Stringoperation wird [Char] vorausgesetzt.
Prelude> :t (++"Stringverlängerung")
(++"Stringverlängerung") :: [Char] -> [Char]
Prelude>-- Divisionen sind mit Fractional, also teilbaren Zahlen sinnvoll.
Prelude>:t (/3)
(/3) :: Fractional a => a -> a
Prelude>
Prelude>-- length wird stets auf eine Liste beliebigen Typs angewandt ([a]) und liefert stets eine ganze Zahl zurück:
Prelude> length [4,5,6,7]
4
Prelude> :t length
length :: [a] -> Int
Prelude>-- Die <code>head</code>-Funktion arbeitet ebenfalls auf einer Liste,
Prelude>-- sie liefert aber als Ergebnis genau ein Argument des Typs, der auch in der Liste geführt wird (hier: Char).
Prelude> head ['a', 'b', 'c', 'd']
'a'
Prelude> :t head
head :: [a] -> a
Prelude>
Funktionen mit bestimmten Typen
[Bearbeiten]Bei Listen und Tupeln sind grundsätzlich alle Typen möglich. Viele Funktionen erlauben aber nur ganz bestimmte Typen, zum Beispiel erhält die Funktion even
einen ganzzahligen Wert und entscheidet dann, ob dieser Wert gerade oder ungerade ist:
Prelude> even 8
True
Prelude> even 7777777777777777777777777777777
False
Prelude> even 7.8
<interactive>:32:1:
No instance for (Integral a0) arising from a use of `even'
...
Prelude> :t even
even :: Integral a => a -> Bool
Prelude>
even
braucht also einen ganzzahligen Wert (bei einer Fließzahl erscheint ein Fehler) und liefert einen Boolschen Wert zurück. In der Typdeklaration even :: Integral a => a -> Bool
taucht ein neues Zeichen =>
auf. Die Deklaration kann so gelesen werden: "Die Funktion even
benutzt das Integral
a
, es erhält dieses Integral als Eingabeparameter und gibt einen Typ Bool
zurück". Links vom =>
steht der class constraint (auf Deutsch die Typklasse), der in der Typdeklaration verwendet wird.
Integral
ist aber keine Typklasse, die wir bereits kennen. Ein Blick in die Typdeklaration mit :info
hilft weiter:
Prelude> :i Integral
class (Real a, Enum a) => Integral a where
quot :: a -> a -> a
rem :: a -> a -> a
div :: a -> a -> a
mod :: a -> a -> a
quotRem :: a -> a -> (a, a)
divMod :: a -> a -> (a, a)
toInteger :: a -> Integer
-- Defined in `GHC.Real'
instance Integral Integer -- Defined in `GHC.Real'
instance Integral Int -- Defined in `GHC.Real'
Diese Typdeklaration besagt, dass Integral
in verschiedenen Funktionen (quot
, rem
usw.) verwendet wird, und dass diese Typklasse Eigenschaften von Integer
und Int
besitzt. Es verwirrt ein wenig, dass Integral
eine Unterklasse von Real
ist, denn Real
steht auch für Gleitkommazahlen. Dass Integral
eine Unterklasse von Enum
ist, leuchtet daher leichter ein, denn jede ganze Zahl hat einen Vorgänger und einen Nachfolger.
Die Funktion fromIntegral
[Bearbeiten]Eine Funktion, die eine Zahl auf die Eigenschaft "ist Quadratzahl" prüft, könnte so lauten: Nimm die Quadratwurzel von x
, runde ab und multipliziere das Ergebnis mit sich selbst. Wenn das Ergebnis gleich x
ist, dann ist x
eine Quadratzahl. Zwei Tests mit den Zahlen 8 und 9 liefern sinnvolle Ergebnisse, also sollte sich diese Formel für die Funktion istQuadtatzahl
eignen:
Prelude> truncate(sqrt(9)) * truncate(sqrt(9)) == 9
True
Prelude> truncate(sqrt(8)) * truncate(sqrt(8)) == 8
False
Prelude> let istQuadtatzahl x = truncate(sqrt(x)) * truncate(sqrt(x)) == x
Warum liefert diese Funktion nur Fehlermeldungen? Beim Betrachten der type signatures fallen Unstimmigkeiten auf:
Prelude> :t sqrt
sqrt :: Floating a => a -> a
Prelude> :t truncate
truncate :: (GHC.Real.Integral b, RealFrac a) => a -> b
Prelude> :t 8
8 :: Num a => a
Prelude>
sqrt
rechnet damit, eine Floating
-Zahl zu bekommen und gibt auch eine zurück. truncate
erwartet aber einen Wert vom Typ RealFrac
, um sie in Integral
zu verwandeln.
Eine Zahl wie 8 oder 9 ist vom Typ Num
und damit sehr flexibel. Haskell betrachtet den Kontext der Gleichung und formt Zahl in Double
um. Damit kommt sowohl sqrt
als auch truncate
zurecht. Bei einem unbekannten Typ x
beharrt sqrt
darauf, das Ergebnis in Floating
zu verwandeln, was truncate
nicht verarbeiten kann.
Eine Lösung hierfür liefert die Funktion fromIntegral
:
Prelude> :t fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b
Prelude> let istQuadtatzahl x = let y = truncate $ sqrt (fromIntegral x) in y*y == x
Prelude> istQuadtatzahl 8
False
Prelude> istQuadtatzahl 9
True
Prelude>
fromIntegral
verwandelt jeden Zahlenwert in den allgemein verständlichen Wert Num
. Damit wird die "untypisierte" variable x
in das gleiche Format gebracht wie eine Zahl 8 oder 9. Dann macht der Interpreter daraus den für die beiden Funktionen sqrt
und truncate
verständlichen Typ Double
.
Als Grafik lassen sich die Zusammenhänge besser verdeutlichen:
fromIntegral
versetzt also den Zahlenwert wieder in den "Urzustand", mit dem alle Funktionen umgehen können.
Typen von Rechenzeichen
[Bearbeiten]Da Rechenzeichen Funktionen sind, besitzen sie auch Typen:
Prelude> :t (+)
(+) :: Num a => a -> a -> a
Prelude> :t (-)
(-) :: Num a => a -> a -> a
Prelude> :t (*)
(*) :: Num a => a -> a -> a
Prelude> :t (/)
(/) :: Fractional a => a -> a -> a
Prelude> :t (>)
(>) :: Ord a => a -> a -> Bool
Prelude> :t (&&)
(&&) :: Bool -> Bool -> Bool
Prelude>
Wie erwartet, erhält die Addition zwei Werte vom Typ Num
(also beliebige Zahlen) und gibt auch einen solchen Typ als Ergebnis zurück. Ebenso verhalten sich die Subtraktion und die Multiplikation. Die Division arbeitet mit dem Datentyp Fractional
Der Vergleichoperator >
(größer als) vergleicht zwei Daten, die die Typklasse Ord
besitzen.
Die Und-Verknüpfung &&
prüft zwei boolsche Werte und gibt auch einen boolschen Wert zurück.