Abstrakte Datentypen und das Typsystem von Haskell

... [Seminar "Funktionale Programmierung"] ... [Inhaltsübersicht] ... [Polymorphie] ... [Typklassen] ...


4. Higher-Order Functions


4.1. Was ist das?

Eine oft wiederkehrende Aufgabe ist es, eine bestimmte Funktion auf einzelne Elemente einer Liste anzuwenden. So könnte ich eine Liste von Zahlen haben und möchte jede Zahl um 1 erhöhen. Denkbar wäre folgende Funktion:

incList :: [Int] -> [Int]
incList [] = []
incList [x:xs] = (x + 1) : incList xs

Andererseits kann ich mir eine Funktion bauen, die wiederum jede Zahl der Liste um 1 verringert.

decList :: [Int] -> [Int]
decList [] = []
decList [x:xs] = (x - 1) : decList xs

Beide Funktionen arbeiten sehr ähnlich. Sie unterscheiden sich nur dadurch, dass incList den + -Operator und decList den - -Operator auf die einzelnen Listenelemente anwendet. Da dieser Unterschied sehr gering ist, bietet es sich an, die Funktion, die auf die einzelnen Listenelemente angewendet werden soll, als Parameter zu übergeben. Bisher wurden allerdings immer nur Datentypen an Funktionen übergeben und keine Funktionen selbst. Dies ist aber problemlos in Haskell möglich. Die "map"-Funktion zeigt beispielhaft wie das funktioniert:

map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs

Hier gibt (a -> b) an, dass als Parameter eine Funktion mit jener Signatur erwartet wird. Mit Hilfe der "map"-Funktion könnte ich also "incList" folgendermaßen konstruieren:

inc :: Int -> Int
inc a = a + 1

incList2 :: [Int] -> [Int]
incList2 xs = map inc xs
Das Übergeben von Funktionen als Parameter ist typisch für Higher-Order Functions.


4.2. Currying

Die formalen Parameter einer Funktion wurden bisher auf folgende Weise angegeben:

add1 :: Int -> Int -> Int
add1 a b = a + b

Allerdings ist es auch möglich, sämtliche Parameter mittels eines Tupels zu übergeben:

add2 :: (Int,Int) -> Int
add2 (a,b) = a + b

Diese Schreibweise ist der Funktionsdeklaration vieler modularer oder objektorientierter Programmiersprachen, wie zum Beispiel Pascal oder Java, ähnlich. Auch dort werden die Parameter geklammert in einem Tupel übergeben werden. Allerdings hat diese Weise Nachteile gegenüber der oberen Deklaration ohne Tupel. Um die Funktion zu verwenden, müssen stets sämtliche Parameter bekannt sein und übergeben werden. Bei der add1 -Funktion ist es möglich, diese nur verkürzt auszuwerten, beziehungsweise der Funktion nur einen Teil der Parameter zu übergeben. In Kapitel 2 über Typsignaturen wurde schon angesprochen, dass sich die Pfeile der Signatur als "wird abgebildet auf" lesen lassen. Werte ich nun den ersten Parameter von add1 aus, so ergibt sich daraus eine neue Funktion. Die Signatur dieser neuen Funktion ist jene, auf welche sich der erste Parameter von add1 abbildet. Möchte ich eine Funktion haben, welche einen beliebigen Wert um 1 erhöht, so ließe sich diese aus der add1 -Funktion folgendermaßen konstruieren.

inc :: Int -> Int
inc a = add1 1 a

Bei add2 wäre dies nicht möglich, da ich in der Signatur nur einen "Abbildungspfeil" habe und alle Parameter sich im Tupel verbergen. Das Notieren der Parameter, wie es bei add1 geschieht und die Möglichkeit des verkürzten Auswertens wird als "Currying" bezeichnet. Die inc -Funktion ließe sich mit Hilfe des "Curryings" noch weiter vereinfachen.

inc :: Int -> Int
inc = add1 1
Da add1 zwei Parameter erwartet und einer schon angegeben ist, ist auch für inc klar, dass es noch einen zu erwarten hat.


... [Seminar "Funktionale Programmierung"] ... [Inhaltsübersicht] ... [Polymorphie] ... [Typklassen] ...