Übersicht:
round :: Float -> Int round x = entier (x + 0.5)
(1)
Grundsätzlich muss man in Haskell (und funktionalen Programmiersprachen
allgemein) zwischen Input und Output einer Funktion deutlich unterscheiden !
Es gibt aufgrund der Wertsemantik von Variablen natürlich keine, wie aus
Pascal bekannten, Var-Parameter.
In Beispiel (1) ist eine einfache Funktion definiert. Die oberste Zeile ist
die Schnittstellendefinition, auch als Signatur bezeichnet. Nach dem Funktionsnamen
('round'), gefolgt von zwei Doppelpunkten wird der Parametertyp 'Float', sowie
der Ergebnisstyp 'Int' deklariert. Besteht der Ergebnisstyp aus mehr als einem
Wert, muss ein zusammengesetzter Typ, z.B. ein Tupel verwendet werden. Die zweite
Zeile ist die Implementation. Die Funktion nimmt ein Floatwert als Parameter
('x') und gibt den gerundeten Wert zurück.
sales :: Int -> Float sales n = volume n - costs n
(2)Eine weitere einfache Funktion. Sie nimmt einen Parameter entgegen und berechnet das Ergebniss unter Zuhilfenahme zweier weiterer Funktionen und gibt es zurück. Zu beachten ist der Ergebnisstyp 'Float'. Scheinbar haben die Funktionen 'volume' und 'costs' auch den Ergebnisstyp 'Float'.
totalSales :: Int -> Float totalSales 0 = 0 totalSales n = sales (n-1) + totalSales (n-1)
(3a)
totalSales :: Int -> [Float] totalSales 0 = [] totalSales n = sales (n-1) : totalSales (n-1)
(3b)
Dieselbe rekursive Funktion, allerdings mit dem Ergebnisstyp 'Liste
von Float'. Anstatt die Teilergebnisse aufzusummieren, wird eine Liste aller
Teilergebnisse aufgebaut (Konkatenations-Operator ':'). Basisfall ist hier
die leere Liste (zweite Zeile).
totalPosSales :: Int -> [Float]
totalPosSales 0 = []
totalPosSales n
| sales (n-1) > 0 = sales (n-1) : totalSales (n-1)
| otherwise = totalPosSales (n-1) (4)
maxMonth :: Int -> Int
maxMonth 0 = 0 maxMonth n | sales n > sales old = n | otherwise = old where old = maxMonth (n-1) (5)
Diese Funktion führt lokale Definitionen ein und veranschaulicht die
'Abseitsregel' (Layout-Rule). Analog zur Abseitsregel im Fussball werden so
Codestücke einer Definition zugeordnet. Der Skopus ergibt sich so aus
der Formatierung des Quelltextes.
Ausserdem wird der Bezeichner 'old' hier lokal definiert ('where'-Klausel).
Vorteil ist, dass der Wert des Bezeichners 'old' nur einmal berechnet werden
muss, obwohl er mehrfach verwendet wird.
roundList :: [Float] -> [Int] roundList i = [ round x | x <- i ] (6)Diese Funktion führt ein weiteres Sprachmerkmal von Haskell ein, die List-Comprehension. Damit ist es möglich, einen Ausdruck auf alle Elemente einer Liste anzuwenden. Durch den als 'Pattern-Matching' bekannten Mechanismus wird der Parameter der Funktion ('Liste von Float', [Float]) dem Bezeichner i zugeordnet. Das List-Comprehension-Konstrukt geht durch alle Element der Liste i ('für alle x Element von i'), ggf. selektiv durch ein Prädikat, und wertet den links neben dem '|'-Zeichen stehenden Ausdruck darauf an. Ergebniss ist wieder eine Liste vom Typ des Ausdrucks.
Anwendung: > roundList [1.2, 2.5, 0.87773] > [1, 3, 1] > roundList totalPosSalesUm diesen Mechanismus in Java abzubilden ohne Zählvariablen zu benutzen, ist es nötig das folgende Iterationsinterface zu deklarieren.
> ...
interface Enumeration { boolean hasMoreElements(); Object nextElement(); } Vector roundList (Enumeration i) { Vector res = new Vector(); while ( i.hasMoreElements() ) { res = res.addElement( round( i.nextElement() )); } return res; }Zu Bemerken ist, dass hier die Allokation einer Ergebnissvariablen nötig ist (Vector res). Um bedingte List-Comprehensions in Java umzusetzen, wäre eine "Filter Enumeration" notwendig. Hier nicht weiter betrachtet.
map :: (Float -> Int) -> [Float] -> [Int] map f i = [ f x | x <- i ]
(7)
Diese Funtion führt ein weiteres mächtiges Sprachmerkmal ein, die
Verwendung von 'Higher-Order-Functions'. War die vorhergehende Funktion noch
starr bezüglich des auf die Liste anzuwendenden Ausdrucks, so ist diese
Funktion flexibel. Funtionen sind in Haskell 'Werte erster Ordnung' ("first-class-citizens")
und können so mit anderen Funktionen parametrisiert werden, man spricht
auch von "algorithmischer Abstraktion". So wird dem Funktionssymbol
f hier der Parameter (Float -> Int) zugeordnet, was einer Funktion entspricht,
die ein Floatwert auf ein Int abbildet. Der Ausdruck 'f x', welcher auf jedes
Element der Liste angewendet wird (zweite Zeile, List-Comprehension) ist somit
"abstrakt" und es wird erst bei der Anwendung der Funktion entschieden,
welche Funktion (die ja als Parameter übergeben wird) auf die Elemente
der Liste angewendet wird.
Anwendung: roundList = map round -- Definition nicht Anwendung ! -- Partielle Anwendung der Fkt. 'map' (zweiter Parameter "fehlt", s.o.) doubleList = map double -- auch hier: Definition der Fkt. 'doubleList' > roundList [1.5, 2.5, 0.723] > [2, 3, 1] > doubleList [1, 2, 3] > [2, 4, 6]Um in Java Funktionen mit anderen Funktionen zu parametrisieren, muss man diese als Objekt "verpacken" und übergeben. Nachteil ist neben der Schreibarbeit durch das Definieren eines geeigneten Interfaces der Overhead, der durch die Verwendung eines weiteren Objektes entsteht :
Vector map (Function f, Enumeration i) { Set res = new Vector(); while ( i.hasMoreElements() ) { res = res.addElement( f.apply( i.nextElement() )); } return res; } interface Function { float apply( int i ); } interface Enumeration { boolean hasMoreElements(); Object nextElement(); }
map :: (a -> b) -> [a] -> [b] map f i = [ f x | x <- i ] (8)
Dieses Beispiel führt die Polymorphie ein, welche besonders in Kombination
mir Higher-Order-Functions zu sehr modularen und wiederverwendbaren, weil
allgemein definierten Funktionen führt.
Alle klein geschriebenen Variablen sind in Haskell 'Typvariablen'. Sie können
für jeden beliebigen Typ stehen und sind somit 'polymorph'. Damit ist
es möglich, parametrisierbare Typen zu verwenden. Wir haben hier also
zwei Dimensionen der Abstraktion: Einmal die algorithmische, durch Verwendung
einer Funktion als Parameter ( (a -> b) ), sowie die Datenabstraktion durch
Verwendung von Typvariabeln ( a , b ). Dass der geforderte Ergebnisstyp berechnet
wird, wird dadurch sichergestellt, dass eine Funktion übergeben werden
muss, die einen (beliebigen) Typ a auf einen (wiederum beliebigen) Typ b abbildet,
und genau diese beiden Typen (nämlich a und b) auch auch als Parameter-
bzw. Ergebnisstyp verwendet werden.
Zur Veranschaulichung:
roundList = map round -- round vom Typ (Float -> Int) upperList :: [Char] -> [Char] upperList = map upper -- upper vom Typ (Char -> Char)
Dabei wird die gleiche Funktion 'map' verwendet, parametrisiert mit unterschiedlichen
Typen.
Wie implementiert man dieses nun in Java ? Dies ist nicht so weiteres möglich,
aber eine Erweiterung der Sprache durch ähnliche Konzepte ist geplant
('Generic Java Extension'). Behelfsweise müsste man das folgende Interface
mit deklarieren. Da Umwandlungen in den Java-Typ 'Object' aber nicht umkehrbar
eindeutig sind, ist die oben beschriebene Typsicherheit nicht mehr gegeben.
Vector construct (Function f, Enumeration i) { Vector res = new Vector(); while ( i.hasMoreElements() ) { res = res.addElement( f.apply( i.nextElement() )); } return res; } interface Function { Object apply( Object o ); }
Zu den polymorphen Typvariablen ähnliche Konzepte sind nur z.B. mit C++
Templates oder der oben genannten zukünftigen Generic Java Extension
möglich, welche sich an diesem Konzept orientieren wird.