Grenzenlose Freiheit?

Polyglotte Programmierung mit Clojure

„Die Grenzen meiner Sprache bedeuten die Grenzen meiner Welt“, stellte schon Ludwig Wittgenstein fest. Auch wenn zwei Programmiersprachen Turing-vollständig sind und somit die gleichen Probleme lösen können, beeinflussen ihre Eigenschaften dennoch die Art und Qualität der Problemlösung. Mit der JVM steht uns heute eine Plattform zur Verfügung, bei der wir für jedes Problem die am besten geeignete Sprache wählen können. Auch wenn sich prinzipiell jede JVM-Sprache mit jeder anderen kombinieren lässt, gibt es einige Stolpersteine. In diesem Artikel wollen wir analysieren, wie gut sich Clojure mit Java verträgt.

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][box_01]

Weitere Informationen zu polyglotten Projekten

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 1 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 2, 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 3. 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 4.

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 5.

Referenzen

  1. https://github.com/clojure/java.data  ↩

  2. https://github.com/clojure/clojure/blob/master/changes.md#21-java-api  ↩

  3. http://clojure.org/compilation  ↩

  4. http://clojure.org/datatypes  ↩

  5. http://dev.clojure.org/display/design/Release.Next+Planning  ↩

TAGS

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen

Finden können Sie uns auch auf