© DrHitch/Shutterstock.com
Clojure

2 Polyglotte Programmierung mit Clojure


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?

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]
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("Inval
id 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"; }
}

Listing 2.1

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 (Kasten: „Polyglotte Projekte“). Nun kann man die betreffenden Klassen mit vollqualifiziertem Namen ansprechen, z. B. data.Person (Listing 2.1). Alternativ verwendet man entsprechende import-Anweisungen:

(ns clj-java.core 
(:import [data Person Book BookStore]))

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 (Listing 2.2).

=> (.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

Listing 2.2

Der Aufruf von getTitle() liefert nil, was äquivalent zu null in Java ist.

Um mehrere Attributzugriffe bzw. Methodenaufrufe hintereinander zu hä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 (Listing 2.3).

=> (.. b1 getAuthor name) 
"Astrid Lindgren"
=> (-> b1 .getAuthor .name)
"Astrid Lindgren"
=> (.. b1 getIsbn length)
14
=> (-> b1 .getIsbn .length)
14
=> (-> b1 .getIsbn count) ;using clj function count instead of String.length
14
=> (-> b1 .getAuthor .name (= "Astrid Lindgren"))
true

Listing 2.3

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

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang