Beispiele in Haskell und Java


... [Informatikseminar 2002] ... [Thema Paradigmenvergleich] ... [Merkmale Haskell / Java]

Ü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)

Eine einfache rekursive Funktion. In der zweiten Zeile ist der Basisfall 0 definiert, darunter der rekursive Aufruf (lineare Rekusion). Rekursion ist notwendig um Schleifen zu programmieren, da es ohne Zuweisungen auch keine Zählvariablen und damit kein Abbruchkriterium gibt.
  
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)

Diese Funktion führt die 'Guards' ein, eine Möglichkeit zur Fallunterscheidung. Um das Ergebniss zu berechnen, wird die Definition von oben nach unten sequentiell durchgegangen und auf die Bedingungen rechtstehend neben den Guards ('|') getestet. Wird die Bedingung zu true ausgewertet, dann wird das rechts neben dem Gleichheitszeichen stehende Code-Stück zur Berechnung verwendet.

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.
Konkret wird hier eine Liste von Fliesskommazahlen traversiert und die Rundungsfunktion 'round' auf jedes Element angewendet. Ergebniss ist die Liste der gerundeten Zahlen.

Anwendung:

> roundList [1.2, 2.5, 0.87773]
> [1, 3, 1]

> roundList totalPosSales
> ...
Um 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.


... [Informatikseminar 2002] ... [Einleitung / Kontext] ... [Sprachmerkmale im Vergleich]