Funktional Programmieren in Java
Derzeit erhält das funktionale Programmieren [1] sehr viel Beachtung – nicht nur in der Community, sondern auch in dieser Kolumne. Während sich verschiedene Beiträge eher um funktionale Aspekte der JVM-Sprachen wie Clojure oder Scala drehen, stellt dieser Artikel die Möglichkeiten der Bibliothek Totally Lazy [2] vor, die vollständig ohne eigene Compiler oder Laufzeit-Zaubereien zurecht kommt.
Totally Lazy
Das Grundprinzip der überschaubaren Bibliothek ist simpel. Mit Hilfe von
Schnittstellen bildet das Framework die Abstraktionen, die nötig sind, um
gängige Higher-Order-Functions wie zum Beispiel map
und reduce
zu
unterstützen.
Totally Lazy definiert diese Abstraktionen mit Generics, sodass beim Hintereinanderschalten von Funktionen die Typsicherheit gewährleistet bleibt und wir weiterhin in der Entwicklungsumgebung unserer Wahl mit <Strg>+<SPACE> programmieren können.
Funktionsparameter für die Higher-Order-Functions sind zu einem großen Teil bereits vordefiniert, können aber genauso gut selbst implementiert werden. Dazu muss man nur die entsprechende Schnittstelle realisieren, die von der Higher-Order-Function als Parametertyp erwartet wird.
Das Kernkonzept von Totally Lazy ist die verzögerte Auswertung von Sequenzen, ähnlich wie dies auch in Clojure geschieht. Das bedeutet, solange keine Werte aus einer Sequenz konkret gebraucht werden, wird auch nicht über die Sequenz iteriert.
Dazu verwendet Totally Lazy eine eigene Datenstruktur Sequence
, die selbst
java.lang.Iterable
realisiert. Higher-Order- Functions liefern stets
Sequence
-Instanzen zurück, wenn als Ergebnis eine Menge von Werten
erwartet wird.
Totally Lazy – Kurzüberblick
Die Konvertierung von Java-Collections zu Totally-Lazy-Sequenzen und zurück:
Funktionen, die Sequenzen zurück liefern, werden lazy ausgewertet:
Funktionen, die einen anderen Rückgabetyp haben, werden eager ausgewertet:
Mit Hilfe sogenannter Generatoren können Sequenzen erzeugt werden:
Ulkiger funktionaler Programmcode
Wir Java-Entwickler sind es häufig gewohnt, über Listen zu iterieren.
Dies würde man mit Totally Lazy schreiben können:
Dabei würden wir den Block aus der oberen for-Schleife nun in eine eigene
Klasse namens IntelligenteVerarbeitungVonE
in die Methode call(Element
e)
auslagern. Alternativ könnten wir auch (da wir noch keine Lambdas zur
Verfügung haben), direkt eine anonyme innere Klasse instanziieren:
Für die einfachen Beispiele können wir also festhalten, dass sich der Quelltext nicht unbedingt verbessert. Der Artikel wäre also hiermit am Ende, hätte das Ganze nicht noch etwas Gutes.
Komplexität zerlegbar machen
Auch wenn die dritte Variante insgesamt fünf Zeilen Programmcode brauchte (und drei Zeilen mit schließenden Klammern), um etwas zu schreiben, was man mit foreach auch in drei Zeilen hätte schreiben können, kann Totally Lazy zur Lesbarkeit von Programmfragmenten beitragen. Beispielsweise kennen Sie sicherlich in Ihren Programmen ähnliche Codefragmente:
Dies kann man mit Totally Lazy einfacher ausdrücken. Zur Erhöhung der
Lesbarkeit sind die anonymen inneren Klassen nicht inline, sondern vorab
instanziiert und den Variablen ist VIPKunde
sowie
intelligenteVIPVerarbeitung
zugewiesen. Dann liest sich obiges Fragment
so:
Auch wenn die Verwendung einer solchen Fluent-API aussieht, als würde
mehrfach über die Liste iteriert werden, ist dem nicht so. Über die Elemente
der Kundenliste wird einmal iteriert. Für jedes Element, das dem Prädikat
istVIPKunde
genügt, ruft map
das Callable-Objekt
intelligenteVIPVerarbeitung
auf und sammelt das Ergebnis in der
Ergebnissequenz.
Vorteilhafte Struktur
Da Totally Lazy intern auf java.lang.Iterable
und java.util.Iterator
setzt, kann die Ergebnisauswertung verzögert werden. Dies erlaubt
gleichermaßen das Definieren unendlicher Folgen wie die partielle Auswertung
von langen Sequenzen mit beliebig vielen Elementen. Zudem können die
Funktionen von Totally Lazy direkt auf den java.util.Collections
angewendet werden. Statt
schreibt man dann mit den richtigen statischen Imports
Die Funktionen von Totally Lazy kennt man aus verschiedenen anderen Sprachen – zum Beispiel aus ML, Haskell oder Lisp. Dadurch ist das Lesen von Quellcode ohne größere Überraschungen möglich. Einen Überblick über einige der Funktionen bietet der Kasten „Totally Lazy – Kurzüberblick“.
Es wird weder ein spezielles Instrumentieren von Klassen benötigt, noch propagiert Totally Lazy Informationen über ThreadLocal. Für die Wartung genügt also ein grundlegendes Verständnis der Higher-Order-Functions und der „Funktionen“, die als Parameter übergeben werden.
In der Java-Welt zuhause
Die Bibliothek zeigt, wie weit funktionale Konzepte in der Implementierung verwendet werden können. Meine bevorzugte Entwicklungsumgebung stellt den Aufruf auch noch kompakt dar, sodass der Code dank Codevervollständigung nicht nur schnell geschrieben ist, sondern auch noch leicht gelesen werden kann. Abbildung 1 zeigt anhand des letzten Codefragments aus „Ulkiger funktionaler Programmcode“, wie die Entwicklungsumgebung die Instanziierung und Definition einer anonymen inneren Klasse auf das Wesentliche reduziert.
Das ist nicht mehr weit weg von dem, was man in anderen Programmiersprachen lesen würde. Ein Klick auf das „+“ expandiert den Text zur vollständigen Form. Darüber hinaus kann man das Aufbereiten der Ergebnisliste den Higher- Order- Functions überlassen. Java-Methoden lassen sich dann besser lesen, wenn der Teil des Quelltextes entfällt, der sonst für den Listenaufbau benötigt würde.
Der Klassiker mit Totally Lazy implementiert
Wenn wir beispielsweise die Umsätze unserer Top-10-Kunden – sortiert nach Kundennamen – ausgeben sollten, sähe die Implementierung unserer Logik recht einfach aus:
Klar, wer eine relationale Datenbank in Reichweite hat, könnte genauso gut diese das Filtern übernehmen lassen.
Weitere Funktionen
Häufig wollen wir auch die restlichen Elemente einer Sequenz sinnvoll
weiterverarbeiten. Mit der Funktion partition
lassen sich Sequenzen
einfach in zwei Gruppen unterteilen: die erste Gruppe mit all den Elementen,
für die das angegebene Prädikat true liefert. Die zweite Gruppe beinhaltet
den Rest.
Hier kann die individuelle Verarbeitung erfolgen. Für die VIP-Kunden könnte eine Einzelsatz-Behandlung anstehen:
Während das Gros der Kunden über den großen Retail-Kamm geschert wird:
Wem die funktionale Schreibweise zu aufdringlich ist und zu wenig nach
Java-Slang aussieht, kann ohne Weiteres an Methoden delegieren, die selbst
wiederum gewohntes Java verwenden und beispielsweise mit foreach
über die
Collections iterieren.
Sie wollen Elemente durchnummerieren? – Mit Totally Lazy ist das kein
Problem. Die Funktion zip
erlaubt es, zwei Sequenzen zusammenzufügen.
Daraus resultiert eine Sequenz, die so lang ist wie die kürzere der beiden
Sequenzen und als Elemente jeweils ein Wert-Paar enthält. Der erste Wert
stammt aus der ersten Sequenz, der zweite aus der zweiten. Mit
erhalten wir also eine Sequenz von Paaren:
[1, Kunde@Instanz1], [2, Kunde@Instanz2], [3, Kunde@Instanz3]
die natürlich erst ausgewertet wird, wenn die Werte benötigt werden.
Paralleles Verarbeiten von Sequenzen
Totally Lazy bietet Funktionen an, deren Namen mit Concurrently enden, um die Verarbeitung von Sequenzen nebenläufig auszuführen. Dies ist sinnvoll, wenn die Verarbeitung eines Elements potenziell lang dauert, aber aufgrund der Rechnerarchitektur auch parallelisiert werden kann.
Die auf Concurrently
endenden Methoden verwenden intern einen
java.util.concurrent.Executor
, um die Funktionsparameter der Higher-
Order-Functions nebenläufig aufzurufen. Dies kann bei lang laufenden
Funktionen die Durchlaufzeit erheblich verkürzen. Um die Auswertung der
Sequence zu erzwingen, können wir unter anderem die Funktion forAll
verwenden.
Der Vergleich im Kleinen – auf einer Maschine mit vier Kernen – lässt sich
sehen. Mit folgendem (Java7)-Code wird zweimal die Sequenz 1,2,3
mit der
Funktion veryLongRunningFunction
verarbeitet. Dabei wird die Zeit
innerhalb des try
-Blocks mit einer eigens geschriebenen Hilfsklasse
StopWatch
gemessen. Ist der try
-Block durchlaufen, gibt die StopWatch
automatisch die Zeit aus:
Der Quelltext in beiden Blöcken unterscheidet sich nur in dem Namen der
StopWatch
sowie in dem Aufruf der Higher-Order-Function. Im ersten Block
wird das sequenzielle map
aufgerufen, im zweiten das nebenläufige
mapConcurrently
. Die Methode forAll
erzwingt die Auswertung der
Sequence. Die Ausführung innerhalb eines kleinen Tests ergibt:
Die nutzlose Wartezeit in der veryLongRunningFunction
wird durch die
Nebenläufigkeit also besser genutzt.
Eigene Funktionen definieren
Der Kasten „Totally Lazy – Kurzüberblick“ bietet eine kleine Übersicht über die gängigen Funktionen/Methoden, die Totally Lazy bietet, um Sequenzen zu verarbeiten. Wem das nicht reicht, der kann mit denselben Mitteln eigene Funktionen definieren, ohne dabei auf Drittbibliotheken angewiesen zu sein.
Totally Lazy auf Steroiden
Wer bereit ist, zur Laufzeit seinen Bytecode instrumentieren zu lassen oder einen Zwischenschritt beim Build einzufügen, der die Bytecode-Manipulation übernimmt, kann mit Hilfe der zusätzlichen Bibliothek Enumerable [3] Lambdas verwenden. Im Kontext von Enumerable sind Lambdas anonyme Funktionen mit nur einem Ausdruck. Darin unterscheiden sich die Lambdas von Enumerable zum Beispiel von Blöcken in Ruby.
Wenn wir Laufzeit-Instrumentierung von Enumerable einbinden, können wir die
JVM mit der zusätzlichen javaagent
-Direktive starten:
Dann können wir in unserem Java-Code auch Abkürzungen schreiben wie:
anstatt dies ausschreiben zu müssen:
Damit würde unser Quellcode von oben auch ohne IDE-Unterstützung sehr leserlich:
Unsere Selektion der VIP-Kunden sähe damit dann so aus:
Da die Parameter innerhalb des Lambdas über statische Imports eingebunden werden, sind dies keine einfachen Bezeichner, sondern referenzieren statische Attribute innerhalb der Enumerable-Parameters-Klasse. Das Framework sorgt bei der Bytecode-Manipulation dafür, dass hier jedes Lambda seine eigenen Parameter erhält.
Da aber Enumerable nichts von unserem Code weiß, gibt es dort auch keine
entsprechend typisierten Parameter. Wir müssen daher den allgemeinen
Parameter obj
vom Typ java.lang.Object
verwenden und selbst casten.
Die Verwendung eigener Attribute funktioniert nicht ohne Framework- Anpassungen, da die erforderliche Bytecode-Manipulation nur auf den von Enumerable deklarierten Parametern funktioniert.
Zusammenfassung
Totally Lazy ist eine kleine Bibliothek, die ohne zusätzliche Abhängigkeiten eingebunden werden kann. Sie bietet eine Reihe von Callable-Klassen an, die gängige Funktionen implementieren. Mit umfangreichen Higher-Order-Functions, die man so aus Sprachen wie ML kennt, ist die Verarbeitung von Collections sehr natürlich. Die funktionalen Elemente lassen sich leicht mit anderen Programmteilen kombinieren, ohne dabei unnatürlich zu wirken.
Der Name ist Programm. Sequenzen werden erst verarbeitet, wenn die Ergebnisse benötigt werden. Das heißt, wenn Werte aggregiert werden oder aber ausgegeben werden sollen. Darüber hinaus können Entwicklungsumgebungen anonyme innere Klassen verkürzt darstellen, sodass man meint, Java könne bereits heute Lambdas.
Wer dann auch noch Lambdas verwenden möchte, kann mit der Bibliothek Enumerable ebenfalls dafür Unterstützung bekommen. Dies funktioniert dann aber nur mit Bytecode-Manipulation – entweder zur Laufzeit beim Laden der Klassen per Instrumentierung oder aber per Bytecode-Manipulation zur Build- Zeit.
Referenzen
-
D. Wampler, Functional Programming for Java Developers, O'Reilly, 2011 ↩