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:

Die Funktion head und ihre Parameterleiste
Prelude> 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 Funktion head wird angewandt auf eine Liste mit Elementen, die vom Typ a sind (der Typ a darf nicht mit einer Variablen verwechselt werden; es ist ein nicht weiter spezifiziert Typ wie etwa Bool oder Char). Das Ergebnis ist ein einzelner Wert vom Typ a (in unserem Fall der Int-Typ 1).

Beim Entwurf von add, einer Funktion, die zwei Werte addiert, erscheint folgende type signature:

Die Funktion add und ihre Parameterleiste
Prelude> 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 Typklasse Num, also für jede Zahl. Im Umkehrschluss heißt das, dass add bei Typklassen wie String oder Bool nicht anwendbar ist.
  • a -> a -> a bedeutet, es gibt zwei Eingabeparameter aus der Typklasse Num und einen Ausgabeparameter, ebenfalls aus der Typklasse Num. 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:

Beispiele mit Tupeln
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 ( ):

Die Funktion 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:

Typen von High Order Functions
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) mittels map 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 bei map (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 Eingabeparameter a und einem Ausgabeparameter b, und einem weiteren Eingabeparameter mit einer Liste vom Typ a. Das Ergebnis ist eine Liste vom Typ b. Das Neue an dieser Typdeklaration ist die Angabe eines Funktionstypen in der Eingabeparameterleiste in Klammern.
weitere Beispiele für Typen von High Order Functions
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

Funktionen und ihre Parameterleisten in ghci
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:

Die Funktion even in ghci
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:

Typdeklaration von even
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.