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:
(ns clj-java.core
(:import [data Person Book BookStore]))
Java-Beispielklassen
package data;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public class Person {
public String name;
public Person() {}
public Person(String name) { this.name = name; }
public String getName() { return name; }
public void setName(String name) {this.name = name; }
@Override
public String toString() { return getName(); }
}
public class Book {
private String title;
private long price;
private final String isbn;
private long year;
private Person author;
public static int VAT_RATE = 7;
public Book(String isbn) {
if (isbn == null) {
throw new IllegalArgumentException("Invalid ISBN");
}
this.isbn = isbn;
}
public long getPrice() {
return price * (100 + VAT_RATE) / 100;
}
public void setPrice(long price) {
this.price = price;
}
public String getIsbn() {
return isbn;
}
@Override
public String toString() {
return getTitle();
}
// außerdem Getter- und Setter-Methoden für
// title, year und author
// sowie equals und hashCode an Hand der isbn
}
public class BookStore {
private Map<Book, Long> inventory = new HashMap<>();
public List<Book> getAvailableBooks() {
return new LinkedList<>(inventory.keySet());
}
public Map<Book, Long> getInventory() {
return new HashMap<>(inventory);
}
public long getQuantity(Book b) {
return inventory.get(b);
}
public void addBook(Book b, long quantity) {
Long currentQuantity = inventory.get(b);
if (currentQuantity == null) {
currentQuantity = 0L;
}
inventory.put(b, quantity + currentQuantity);
}
public void addBooks(Map<Book, Long> delivery) {
for (Entry<Book, Long> entry : delivery.entrySet()) {
addBook(entry.getKey(), entry.getValue());
}
}
}
public interface BookFilter {
public List<Book> filterBooks(List<Book> books);
}
public class Primitives {
public static String test(Number o) { return "Number"; }
public static String test(int i) { return "int"; }
public static String intOnly(int i) { return "intOnly"; }
}
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]:
:java-source-paths ["java-src"]
Die Optionen für den Java-Übersetzer werden zum Beispiel folgendermaßen festgelegt:
:javac-options ["-target" "1.6" "-source" "1.6"]
Unabhängig davon kann noch definiert werden, welche der Clojure-Namespaces bei lein compile
übersetzt werden sollen [2]:
:aot [pure-clj.helper]
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:
(def a1 (new Person))
(def a2 (Person. "Astrid Lindgren"))
(def b1 (new Book "978-3789129407"))
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.
=> (.name a2)
"Astrid Lindgren"
=> (.getTitle b1)
nil
=> (.setTitle b1 "Ronja Räubertochter")
nil
=> (.setAuthor b1 a2)
nil
=> (.setYear b1 1981)
nil
=> (Book/VAT_RATE)
7
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.
=> (.. b1 getAuthor name)
"Astrid Lindgren"
=> (-> b1 .getAuthor .name)
"Astrid Lindgren"
=> (.. b1 getIsbn length)
14
=> (-> b1 .getIsbn .length)
14
=> (-> b1 .getIsbn count) ;using function count instead of String.length
14
=> (-> b1 .getAuthor .name (= "Astrid Lindgren"))
true
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:
=> (set! (.name a1) "Jane Austen")
"Jane Austen"
=> (.name a1)
"Jane Austen"
=> (set! Book/VAT_RATE 10)
10
=> (set! (.title b1) "Ronja Räubertochter")
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:
=> (type a1)
data.Person
=> (instance? Person a1)
true
=> (instance? String a1)
false
=> (instance? Integer 3)
false
=> (type 3)
java.lang.Long
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:
(def isbn-1 (memfn getIsbn))
(def isbn-2 #(.getIsbn %))
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:
(def b2
(doto (Book. "978-3423141628")
(.setAuthor a1)
(.setTitle "Emma")
(.setPrice 930)
(.setYear 1815)))
=> (.getTitle b2)
"Emma"
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
):
(def x 2)
=> (data.Primitives/test 2)
"int"
=> (data.Primitives/test x)
"Number"
=> (data.Primitives/test (int x))
"int"
Bei nicht überladenen Methoden funktioniert die automatische Umwandlung zu primitiven Typen:
=> (data.Primitives/intOnly 2)
"intOnly"
=> (data.Primitives/intOnly x)
"intOnly"
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.
(def bookstore
(doto (BookStore.)
(.addBook b1 3)
(.addBook b2 2)))
(def available (.getAvailableBooks bookstore))
(defn book-author [b]
(-> b
(.getAuthor)
(.name)))
=> (map #(.getAuthor %) available)
(#<Person Astrid Lindgren> #<Person Jane Austen>)
=> (count available)
2
=> (filter #(= "Jane Austen" (book-author %)) available)
(#<Book Emma>)
=> (sort-by book-author available)
(#<Book Ronja Räubertochter> #<Book Emma>)
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.
(def delivery {b1 4})
=> (instance? java.util.Map delivery)
true
=> (.getQuantity bookstore b1)
7
=> (.addBooks bookstore delivery)
nil
=> (.getQuantity bookstore b1)
11
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:
[(f x) (f x)]
(let [y (f x)]
[y y])
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.
=> (bean a1)
{:name "Jane Austen", :class data.Person}
=> (bean b1)
{:year 1981, :title "Ronja Räubertochter", :price 0,
:isbn "978-3789129407", :class data.Book,
:author #<Person Astrid Lindgren>}
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.
=> (from-java a1)
{:name "Jane Austen"}
=> (from-java b1)
{:year 1981, :title "Ronja Räubertochter", :price 0,
:isbn "978-3789129407", :author {:name "Astrid Lindgren"}}
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.
(defmethod from-java BookStore [instance]
{:inventory
(into {}
(map (fn [book] [(from-java book)
(.getQuantity instance book)])
(.getAvailableBooks instance)))})
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:
(def a3 {:name "Enid Blyton"})
=> (to-java Person a3)
#<Person Enid Blyton>
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.
(defmethod to-java [Book clojure.lang.APersistentMap] [clazz props]
(let [desc (.getPropertyDescriptors
(java.beans.Introspector/getBeanInfo clazz))
setter-map (into {} (map (fn [d] [(keyword (.getName d))
(.getWriteMethod d)])
desc))
book (Book. (:isbn props))]
(doseq [[prop value] (dissoc props :isbn)]
(let [setter (prop setter-map)
prop-type (get (.getParameterTypes setter) 0)]
(.invoke setter book (into-array [(to-java prop-type value)]))))
book))
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:
=> (into-array [3.0 5.0 2.0])
#<Double[] [Ljava.lang.Double;@e49fec8>
=> (into-array [3 "Hallo" 2.0])
IllegalArgumentException array element type mismatch
Alternativ kann der Typ auch explizit angegeben werden:
=> (into-array Object [3 "Hallo" 2.0])
#<Object[] [Ljava.lang.Object;@4d8f29af>
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:
=> (into-array (map int-array [[1 2 3] [2 3 4] [3 4 5]]))
#<int[][] [[I@5afa3f93>
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.
(def before-1900
(reify BookFilter
(filterBooks [this books]
(filter #(< (.getYear %) 1900) books))))
=> (.filterBooks before-1900 [b1 b2])
(#<Book Emma>)
=> (class before-1900)
clj_java.core$reify__1832
Benötigt man den this
-Parameter in einer Funktion nicht, so kann dies
verdeutlicht werden, indem man _
nutzt. Auch kann der Filter parametrisiert
werden.
(defn before-year-filter [year]
(reify BookFilter
(filterBooks [_ books]
(filter #(< (.getYear %) year) books))))
=> (.filterBooks (before-year-filter 1900) [b1 b2])
(#<Book Emma>)
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.
(defn new-book [isbn author title]
(doto (proxy [Book] [isbn]
(toString [] (str (.getAuthor this) ": " (.getTitle this))))
(.setAuthor author)
(.setTitle title)))
(def b3 (new-book "978-3789141676" a2 "Mio, mein Mio"))
=> (.toString b3)
"Astrid Lindgren: Mio, mein Mio"
=> (class b3)
clj_java.core.proxy$data.Book$ff19274a
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.
=> (try
(.length (.name (Person.)))
(catch Exception e (println "caught: " e)))
caught: #<NullPointerException java.lang.NullPointerException>
nil
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:
IFn plus = Clojure.var("clojure.core", "+");
plus.invoke(3,3); // -> 6
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:
IFn rand = Clojure.var("clojure.core", "rand");
rand.invoke(); // Zufallszahl im Bereich [0;1)
rand.invoke(10); // Zufallszahl im Bereich [0;10)
rand.invoke(10, 20); // -> java.lang.reflect.InvocationTargetException
Mithilfe dieses API lassen sich Clojure-Funktionen auch mit dem in Java 8 eingeführten Stream-API kombinieren:
IFn inc = Clojure.var("clojure.core", "inc");
List<Long> values = Arrays.asList(3L, 17L, 5L);
values.stream().map((v) -> inc.invoke(v)).toArray(); // -> [4, 18, 6]
Will man auf andere Clojure-Namespaces zugreifen, muss man diese zunächst mit
require
laden:
IFn require = Clojure.var("clojure.core", "require");
require.invoke(Clojure.read("clj-java.core"));
IFn beforeYearFilter = Clojure.var("clj-java.core", "before-year-filter");
BookFilter f = (BookFilter) beforeYearFilter.invoke(1900);
Mithilfe von Clojure.read
lassen sich auch beliebige Clojure-Datenstrukturen
erstellen:
IFn assoc = Clojure.var("clojure.core", "assoc");
Object cljMap = Clojure.read("{:a 1 :b 2}");
Object cljKeyword = Clojure.read(":c");
assoc.invoke(cljMap, cljKeyword, 3); // -> {:c 3, :a 1, :b 2}
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:
(compile 'namespace)
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
(ns pure-clj.helper)
(defn interleaveStrings [a b] (apply str (interleave a b)))
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:
IFn x = new pure_clj.helper$interleaveStrings();
x.invoke("ABCD", "123"); // -> "A1B2C3"
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.
(ns pure-clj.helper
(:gen-class
:name pure_clj.ClojureHelper
:prefix ""
:methods [[interleaveStrings [String String] String]]))
(defn interleaveStrings [_ a b] (apply str (interleave a b)))
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:
package pure_clj;
public class ClojureHelper {
public String interleaveStrings(String a, String b) {...}
}
Diese kann dann wie gewohnt verwendet werden:
pure_clj.ClojureHelper helper = new ClojureHelper();
String s = helper.interleaveStrings("XYZ", "1234"); // -> "X1Y2Z3"
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.
(ns pure-clj.helper
(:gen-class
:name pure_clj.ClojureHelper
:prefix ""
:methods [^{:static true} [interleaveStrings [String String] String]]))
(defn interleaveStrings [a b] (apply str (interleave a b)))
Dann kann sie folgendermaßen benutzt werden:
String s = pure_clj.ClojureHelper.interleaveStrings("XYZ", "1234");
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].