Es ist heute durchaus üblich, verschiedene Komponenten in verschiedenen Sprachen zu implementieren. Das hat den Vorteil, dass für jedes Problem eine geeignete Sprache gewählt, die verschiedenen Vorteile der Sprache und ihrer Ökosystem kombiniert und die unterschiedlichen Expertisen der Entwickler bestmöglich genutzt werden können. Häufig sind diese Komponenten durch HTTP oder andere Protokolle voneinander entkoppelt. Auf der JVM können Komponenten viel direkter und feingranularer miteinander kombiniert werden. Doch auch hier bietet es sich an, klare Schnittstellen und Verantwortlichkeiten zu ziehen. Durch diese Technik kann schon existierender Quellcode weiter verwendet und so ein Umstieg auf eine andere Sprache Stück für Stück vollzogen werden.
Wie problemlos lässt sich Clojure mit anderen Sprachen kombinieren? Wie kann man eine Clojure-Bibliothek ohne weiteres aus einer Java-Anwendung heraus nutzen und umgekehrt? Allgemein lässt sich sagen, dass die Verwendung von Java-Bibliotheken in anderen JVM-Sprachen kein Problem darstellt. Doch welche Hilfsmittel gibt es hier, sodass sich die Verwendung von Java auf Grund der verschiedenen Paradigmen nicht wie ein Fremdkörper anfühlt?
Hello Java! Clojure is calling
Um Java-Klassen in Clojure nutzen zu können, benötigt man nicht viel. Zunächst
müssen sich die Java-Klassen im Classpath befinden (siehe Kasten „Polyglotte
Projekte“). Nun kann man die betreffenden Klassen mit vollqualifiziertem Namen
ansprechen, z. B. data.Person
(siehe Kasten „Java-Beispielklassen“). Alternativ
verwendet man entsprechende import
-Anweisungen:
Polyglotte Projekte
Leiningen ist das Standard-Build-Werkzeug in der Clojure-Welt. Will man in einem Leiningen-Projekt Java-Bibliotheken nutzen, so können diese einfach als Abhängigkeiten eingetragen werden, da Leiningen und Maven kompatibel sind. Umgekehrt ist auch das Einbinden einer Clojure-Bibliothek als Maven-Dependency ohne weiteres möglich.
Leiningen bietet die Möglichkeit, in der Konfigurationsdatei project.clj
Verzeichnisse mit Java-Code zu definieren, die dann mit lein javac
übersetzt werden können [1]:
Die Optionen für den Java-Übersetzer werden zum Beispiel folgendermaßen festgelegt:
Unabhängig davon kann noch definiert werden, welche der Clojure-Namespaces bei lein compile
übersetzt werden sollen [2]:
Anschließend können in diesem Clojure-Namespace Objekte dieser Klassen
entweder mit new
oder mit Classname.
erzeugt werden. Erwartet der
Konstruktor Argumente, so werden diese wie bei jedem Funktionsaufruf in
Clojure mitgegeben:
Mit .attributName
beziehungsweise .methodenName
erfolgt der Zugriff auf
sichtbare Attribute bzw. Methoden. Dabei wird das Objekt als erster Parameter
mitgegeben. Auf statische Elemente wird mit /
zugegriffen.
Der Aufruf von getTitle()
liefert nil
was äquivalent zu null
in Java ist.
Um mehrere Attributzugriffe bzw. Methodenaufrufe hintereinander zuhängen, kann
das dot-dot-Makro (..
) verwendet werden. Idiomatischer ist die Verwendung
des thread-first-Makros (->
). Dabei wird das Ergebnis des ersten Ausdrucks
als erstes Argument des nachfolgenden Funktionsaufrufs verwendet (und immer so
weiter). Damit lassen sich auch Aufrufe von Java-Methoden mit Aufrufen von
Clojure-Funktionen kombinieren.
Da viele der grundlegenden Clojure-Funktionen auch für Java-Interfaces und
-Klassen, wie zum Beispiel Collection
, Iterable
, Map
oder
CharSequence
, definiert sind, funktionieren auch andere darauf aufbauende
Funktionen mit Java-Objekten. So greift zum Beispiel =
in Clojure bei
Java-Objekten auf equals()
zurück. Soll auf die Objektidentität geprüft
werden, so geht dies mit der identical?
-Funktion. Die Clojure-Funktion ==
ist jedoch nur auf Zahlen definiert! Mithilfe von set!
lassen sich sichtbare
Attribute, die nicht als final
deklariert sind, verändern:
Der letzte Zugriff führt zu einer IllegalArgumentException
, da das Attribut
nicht sichtbar ist. Eine Änderung über die Setter-Methode ist natürlich ohne
weiteres möglich. Würde es sich bei VAT_RATE
um eine Konstante handeln, so
würde der Aufruf zum Ändern des Mehrwertsteuersatzes zu einem
IllegalAccessError
führen. Mit type
bzw. instance?
kann der Typ eines
Objekts festgestellt bzw. überprüft werden:
Um eine Funktionsreferenz für eine Java-Funktion zu erhalten, kann memfn
genutzt werden, allerdings eignen sich anonyme Clojure-Funktionen dafür
genauso, und es gibt dabei keine Beschränkung auf Java-Methoden:
Ausgestattet mit diesen Hilfsmitteln lassen sich Java-Klassen ohne weiteres
relativ komfortabel nutzen und mit Clojure-Funktionen kombinieren. Mithilfe
des thread-first-Makros lassen sich Verkettungen von Aufrufen, zum Beispiel
bei Fluent-Interfaces, gut handhaben. Allerdings werden in Java-APIs oft auch
Setter-Methoden bzw. andere Methoden ohne Rückgabetyp genutzt. Mithilfe von
doto
können mehrerer solcher Aufrufe sehr angenehm umgesetzt werden:
Primitive Typen
Bei all diesen Varianten ist zu berücksichtigen, dass Rückgabewerte von
primitiven Typen sofort in die zugehörigen Wrapper-Klassen umgewandelt werden,
solange sie nicht direkt an eine Methode weitergereicht werden, die einen
Parameter eines primitiven Typs erwartet. Eine Umwandlung zu einem primitiven
Typen ist mit Funktionen wie int
, char
und double
möglich (siehe
Kasten „Java-Beispielklassen“ für data.Primitives
):
Bei nicht überladenen Methoden funktioniert die automatische Umwandlung zu primitiven Typen:
Wenn die Java-Methoden long
und double
statt int
und float
erwarten,
wird die Interoperabilität zwischen Clojure und Java erleichtert, da explizite
Typumwandlungen entfallen können.
Ohne Sammlungen geht es nicht
Über kurz oder lang stößt man bei der Verwendung von Java-Code auch auf Java-Collections, die sich ebenfalls problemlos in Clojure nutzen lassen.
Umgekehrt implementieren Clojure-Collections Java-Interfaces und können somit
direkt an Java-Funktionen übergeben werden. Dabei sollte aber berücksichtigt
werden, dass die Clojure-Collections unveränderlich sind und deswegen der
Aufruf von verändernden Methoden, wie zum Beispiel add
, eine
UnsupportedOperationException
wirft.
Veränderungen sind nicht immer gut
Bei der Verwendung von veränderlichen Java-Objekten stellt man schnell fest, dass diese im absoluten Gegensatz zur Verwendung von unveränderlichen Datenstrukturen in der funktionalen Programmierung stehen. Denn verändert man den Zustand eines Objekts, so hat ein augenscheinlich gleicher Ausdruck plötzlich ein anderes Ergebnis.
In einer rein funktionalen Welt, in der die Funktion f
keinen Seiteneffekt
besitzt, wären die folgenden Ausdrücke identisch:
Besitzt f
aber einen Seiteneffekt, trifft dies leider nicht mehr zu.
Bohnen mit Clojure
Ist man hauptsächlich an den in den Java-Objekten enthaltenen Daten
interessiert (zum Beispiel beim Datenbankzugriff oder dem Konsumieren eines
Services) und will diese in Clojure weiterverarbeiten, bietet sich deshalb
eine Konvertierung der Java-Objekte in Clojure-Datenstrukturen, insbesondere
Maps, an. Handelt es sich bei den Objekten um Java-Beans, so kann dies leicht
mithilfe der bean
-Funktion geschehen. Auf diese Weise kann auf die Daten
idiomatisch zugegriffen und diese können neu aggregiert werden.
Dabei gilt es zu beachten, dass die Umwandlung nicht rekursiv erfolgt. Die
Bibliothek java.data
[3] ermöglicht mithilfe der Funktion
from-java
eine rekursive Umwandlung von Beans in Clojure-Maps. Zyklen im
Objektgraphen führen hier verständlicherweise zu einem Stack Overflow.
Collections und Arrays werden ebenfalls entsprechend umgewandelt.
Bei der bean
-Funktion wird auch die Java-Class mit in die Clojure-Map
übertragen, bei from-java
hingegen nicht. Diese Umwandlung hilft jedoch nur,
wenn es sich bei den Objekten um reine Datencontainer handelt, die keine
zusätzliche Geschäftslogik bieten. Gerade bei Objekten, deren
Zustandsänderungen von Interesse sind, wie zum Beispiel dem BookStore
, wird
es schwierig, da die Veränderbarkeit dieser Objekte der funktionalen
Programmierung widerspricht. In diesem Fall sollte man abwägen, ob man die
Funktionalität nachbaut, ggf. kapselt oder ob man mit den oben genannten
Mitteln arbeiten will und sich dabei der Veränderbarkeit der Objekte jederzeit
bewusst ist.
Bei einer automatischen Umwandlung ist auch zu prüfen, ob der gesamte Zustand
bzw. alle relevanten Teile durch das Bean-Interface abgedeckt sind. Gäbe es
zum Beispiel im BookStore
die Methode getInventory()
nicht, so wäre zwar
der komplette Zustand des Objekts jederzeit abfragbar, aber durch die
automatische Umwandlung würde er verloren gehen. Außerdem ist zu beachten,
dass bei der automatischen Umwandlung die in Java-Maps enthaltenen Objekte
nicht umgewandelt werden, bei anderen Collections jedoch schon.
Allerdings ist es auch möglich, eine Implementierung für from-java
für
einzelne Klassen zu implementieren, zum Beispiel für den Fall, dass die
Methode getInventory()
nicht existieren würde und man auch die rekursive
Umwandlung innerhalb von Maps umsetzen will.
Führt auch ein Weg zurück?
Der bisher beschriebene Weg ist zum Konsumieren von Daten aus
Java-Bibliotheken oder dort zur Verfügung gestellter Funktionalität bestens
geeignet. Auch für die Erzeugung von Java-Objekten gibt es entsprechende Hilfe.
Eine Möglichkeit ist es, einen passenden Konstruktor aufzurufen und
gegebenenfalls Attribute zu setzen oder Methoden aufzurufen, um den
gewünschten Zustand herbeizuführen. Alternativ bietet die
java.data
-Bibliothek mit der to-java
-Funktion das Pendant zu from-java
.
Die Funktion erwartet als erstes Argument die Klasse des zu erzeugenden
Objekts, als zweites eine Map mit den Attributen, sodass eine Umwandlung von
Clojure-Maps in Java-Objekte ohne den manuellen Aufruf von Setter-Methoden
möglich ist:
Auch to-java
kann für eine Klasse selbst implementiert werden. Hilfreich
wären Implementierungen für Book
und BookStore
. Denn die Erzeugung eines
Book
-Objekts mit der Standardimplementierung von to-java
schlägt fehl, da
die Book
-Klasse keinen Standardkonstruktor besitzt. Bei der Erzeugung eines
BookStore
-Objekts wäre eine unveränderliche Map für das Attribut inventory
ebenfalls problematisch.
Es gibt auch Fälle, in denen man beim Aufruf von Java-Methoden Arrays
übergeben muss, wie zum Beispiel dem Aufruf einer Java-Methode über Reflection.
Mit into-array
werden Arrays mit Elementen eines nicht primitiven Typs
erzeugt. Dies ist vor allem beim Aufruf von überladenen Methoden zu
berücksichtigen. Als Elementtyp wird der Typ des ersten Elements genommen und
geprüft, ob alle anderen Elemente einen kompatiblen Typen, d. h. den gleichen
oder einen Subtyp, haben:
Alternativ kann der Typ auch explizit angegeben werden:
Für das explizite Erzeugen von Arrays primitiver Typen gibt es Hilfsmethoden,
wie zum Beispiel int-array
und char-array
. Ebenso gibt es Hilfsfunktionen
zum Erzeugen mehrdimensionaler Object-Arrays (to-array-2d
). Will man ein
mehrdimensionales int
-Array erzeugen, so kann dies leicht mit map
geschehen:
Jenseits von Bohnen und Feldern
Bisher haben wir uns primär mit Beans befasst, aber in vielen Fällen wird man
auch auf andere Klassen und Interfaces stoßen. Mithilfe des reify
-Makros
lässt sich ein Objekt erzeugen, das ein Interface implementiert, ähnlich wie
das mit anonymen Klassen in Java möglich ist. So können wir zum Beispiel einen
BookFilter
implementieren, der nur Bücher durchlässt, die vor 1900
geschrieben wurden.
Benötigt man den this
-Parameter in einer Funktion nicht, so kann dies
verdeutlicht werden, indem man _
nutzt. Auch kann der Filter parametrisiert
werden.
Das reify
-Makro ist nicht nur im Fall von Interoperabilität interessant,
sondern kann auch bei Clojure-Protokollen und -Typen zum Einsatz kommen. Will
man eine konkrete Klasse erweitern, so funktioniert dies mit dem für
Interoperabilität gedachten proxy
-Makro. So können wir beispielsweise
Book
-Objekte mit überschriebener toString()
-Methode erzeugen. Dabei ist
der zweite Parameter des Makros die Liste der Argumente für den Aufruf des
super
-Konstruktors.
Außerdem implementieren Clojure-Funktionen ein paar hilfreiche Interfaces, wie
zum Beispiel Callable
, Runnable
und Comparable
. Um als Callable
oder
Runnable
genutzt zu werden, darf die Funktion keine Argumente erwarten, im
Fall von Comparable
genau zwei. Die mit Java 8 neu eingeführten Interfaces
wie Predicate
oder Function
werden bisher noch nicht implementiert.
Wenn Ausnahmen die Regel sind
Will man Java-Bibliotheken benutzen, muss man sich auch um die Behandlung von
Exceptions kümmern, was mithilfe des try
-catch
-Konstrukts kein Problem ist.
In Clojure muss das Auftreten von Exceptions nicht deklariert werden. Ebenso müssen auftretende Exceptions nicht sofort behandelt, sondern können beliebig weitergereicht werden.
Ein Blick von der anderen Seite
Wir haben gesehen, dass jede existierende Java-Bibliothek je nach Funktionalität und Struktur mehr oder weniger idiomatisch in Clojure- Programmen genutzt werden kann. Sei es als Datenquelle oder -senke oder zur Vermeidung von Neuimplementierung.
Um in Clojure implementierte Funktionen in Java-Code einbinden zu können, gibt
es zwei Alternativen. Seit Clojure 1.6 gibt es ein sehr schlankes, von Clojure
bereitgestelltes Java-API [4], das mithilfe von
clojure.java.api.Clojure
und clojure.lang.IFn
den Zugriff auf alle
Clojure-Funktionen ermöglicht. Alle anderen Klassen und Interfaces sollten als
Implementierungsdetails verstanden und deshalb nicht verwendet werden.
Alle in clojure.core
enthaltenen Funktionen sind ohne weiteres verfügbar und
können mithilfe von Clojure.var(namespaceName, functionName)
geladen werden:
Dabei verliert man jedoch sämtliche Typsicherheit, und es können Exceptions auftreten, zum Beispiel, wenn die Funktion nicht mit der entsprechenden Signatur definiert ist:
Mithilfe dieses API lassen sich Clojure-Funktionen auch mit dem in Java 8 eingeführten Stream-API kombinieren:
Will man auf andere Clojure-Namespaces zugreifen, muss man diese zunächst mit
require
laden:
Mithilfe von Clojure.read
lassen sich auch beliebige Clojure-Datenstrukturen
erstellen:
Beim Zugriff über dieses API macht es keinen Unterschied, ob der Clojure-Code im Rahmen einer Bibliothek vorliegt oder ob es sich um ein polyglottes Projekt handelt.
Seiner Zeit voraus
Die andere Alternative ist Ahead-of-time (AOT) Compilation [5]. In
diesem Fall werden Clojure-Namespaces zu entsprechenden class
-Files
übersetzt und somit für anderen JVM-Quellcode nutzbar. Die Übersetzung kann
zum Beispiel manuell erfolgen mit:
Dabei wird dann unter anderem für jede Funktion eine eigene Klasse mit dem
Namen namespace$functionName
erzeugt. Zu finden sind sie in dem Verzeichnis,
das in der Variable *compile-path*
definiert ist. So wird beispielsweise aus
dem folgenden Clojure-Namespace
unter anderem die Klasse pure-clj.helper$interleaveStrings
erzeugt, die das
Interface IFn
implementiert. So kann dann im Java-Code die
interleaveStrings
-Funktion aufgerufen werden:
Diese Variante bringt keinen großen Vorteil zum neuen Java-API mit sich, außer dass die Existenz der verwendeten Funktionen durch den Übersetzer sichergestellt werden kann.
Es gibt jedoch die Möglichkeit, den Clojure-Code anzureichern, sodass Java-Klassen mit entsprechend getypten Signaturen generiert werden können. Da die explizite Anreicherung des Codes nötig ist, ist eine spontane Wiederverwendung nicht ohne weiteres möglich. Die Anpassungen im Clojure-Code sind zwar nicht kompliziert, müssen aber manuell gepflegt werden. Sind im Clojure-Code jedoch alle Vorkehrungen getroffen, ist im Java-Code kein Unterschied zu in Java implementierten Klassen zu erkennen.
Zur Anreicherung des Clojure-Codes wird die :gen-class
-Anweisung in der
Namespace-Deklaration ergänzt. Hierbei lassen sich dann der Name der
generierten Klasse sowie die Signaturen für die einzelnen Funktionen festlegen.
Wichtig dabei ist, dass alle nichtstatischen Methoden als ersten Parameter
this
übergeben bekommen. Außerdem ist das Präfix für alle nichtstatischen
Methoden standardmäßig auf "-"
gesetzt. Mit den obigen Angaben wird eine
Klasse mit folgenden Signaturen generiert:
Diese kann dann wie gewohnt verwendet werden:
Dabei sind die Methoden nun typsicher, allerdings kann es immer noch zu Laufzeitfehlern kommen, wenn zum Beispiel die Clojure-Funktion nicht mit dem passenden Präfix oder der entsprechenden Parameteranzahl existiert.
Benötigt eine Methode kein Objekt, kann sie natürlich auch als statische Methode generiert werden.
Dann kann sie folgendermaßen benutzt werden:
Mithilfe von :gen-class
können auch die Oberklasse und implementierte
Interfaces sowie Konstruktorsignaturen und die Main-Methode definiert werden.
So können zum Beispiel aus Clojure-Code Servlets generiert werden. Außerdem
gibt es die Möglichkeit, einen Zustand zu spezifizieren, wenn man zum Beispiel
ein Interface wie Iterable
implementieren will, das einen Zustand benötigt.
Jedes Clojure-Protokoll definiert automatisch ein gleichnamiges Java-Interface.
Im Fall von polyglotten Projekten kann man mit definterface
Java-Interfaces
definieren, die dann auch primitive Datentypen als Parameter erwarten können.
Da dies der einzige Vorteil ist, sollte definterface
nur benutzt werden,
wenn primitive Datentypen zwingend benötigt werden.
Typen, die mithilfe von deftype
oder defrecord
definiert wurden, erzeugen
ebenfalls entsprechende aus Java nutzbare Klassen, ohne dass explizit
:gen-class
genutzt werden muss [6].
Allerdings fehlt auch hier jegliche Typsicherheit, da die möglichen type-hints noch nicht dafür genutzt werden.
Grenzenlose Freiheit!
Die Verwendung des neu eingeführten Java-API sollte auf Grund der fehlenden
Typsicherheit vorher gut durchdacht werden. Dennoch bietet es einen bequemen
Weg, Clojure-Code einbinden zu können, der nicht für die Verwendung aus Java
vorgesehen wurde. Will man ohne permanente Typumwandlungen im Java-Code
auskommen, sollte die Variante mit AOT-Übersetzung und :gen-class
genutzt
werden, wobei einem auch hier bewusst sein muss, dass die Bindung an die
Clojure-Funktionen erst zur Laufzeit erfolgt. Dabei sollte man außerdem
zyklische Abhängigkeiten zwischen Java- und Clojure-Code vermeiden, da dies
die Übersetzung deutlich erschwert. Auch wenn die Integration auch auf sehr
feingranularer Ebene möglich ist, sollten dennoch klare Grenzen, zum Beispiel
in Form von Interfaces, zwischen den Clojure- und Java-Teilen definiert werden.
Wir haben gesehen, dass die JVM viele elegante Möglichkeiten zur polyglotten Programmierung mit Clojure bietet. Auch die Build-Systeme verursachen keine Probleme. In vielen Fällen können Bibliotheken problemlos verwendet werden, ohne dass sie explizit für die Interoperabilität entwickelt wurden. Auch die Zukunft von Clojure wird in diesem Bereich sicherlich noch einiges Neue bringen [7].