4  Java

Die Java Platform, Standard Edition 6 API Specification listet über 3700 Klassen auf, und im Internet finden sich unzählige qualitativ hochwertige Programme und Bibliotheken, die nicht nur in Java geschrieben, sondern auch unter freien Lizenzen verfügbar sind. In den meisten Fällen erlauben die Lizenzen auch eine kommerzielle Verwendung. Dieses reichliche Angebot hat dazu geführt, dass kommerzielle Softwareentwicklung heute oft darin besteht, die passenden Bibliotheken zu finden, zu konfigurieren und sie dann mit dem noch fehlenden eigenen Code zusammenzufügen. Clojure integriert sich so nahtlos in die Java-Landschaft, dass die Verwendung beider Sprachen in einem Programm in fast allen Fällen möglich ist, was ein großer Vorteil ist. Manch neue Sprache scheitert an ihrer zu kleinen Standardbibliothek, aber auch Sprachen mit langer Geschichte, gerade in der Lisp-Familie, haben oft eine zerklüftete Bibliotheken-Landschaft. Für Common Lisp finden sich entgegen der landläufigen Meinung zwar zahlreiche Bibliotheken für diverse Anforderungen, doch ist deren Integration oft unzulänglich, und auch die Qualität der Dokumentation oder die Vollständigkeit der Implementation lassen häufig zu wünschen übrig. Selbst auf der JVM laufende Sprachen, die von ihrer kanonischen Implementation portiert wurden, wie Jython oder JRuby, fügen sich nicht vollkommen nahtlos ein, da sie durch ihre primäre Plattform nicht immer mit Javas Typsystem harmonieren. Harmonie mit der JVMPython beispielsweise verfügt über keinen Typen für einen Character und kennt nur Doubles, die allerdings in Python „float“ heißen. Deutlich schwieriger wird es aber in den Fällen, in denen eine Bibliothek, egal ob Teil des Standards oder extern, weitere Abhängigkeiten hat. Die C-Implementation von Python beispielsweise unterstützt die Entwicklung von Terminal-Interfaces mit der Curses-Bibliothek, Jython hingegen nicht. Nichtsdestotrotz sind diese Sprachen natürlich der JVM sehr nah und integrieren sich recht gut in die Java-Infrastruktur.

Bindungsschicht

Im allgemeinen Fall ist für die Interaktion zwischen zwei Sprachen eine Bindungsschicht unerlässlich; ohne sie sind die in einer Sprache deklarierten Funktionen und Klassen in der anderen nicht verfügbar. Die reflektiven Fähigkeiten von Java ermöglichen es, zur Laufzeit deklarierte Typen zu inspizieren, und gestatten Clojure somit ohne zusätzlichen Aufwand den Zugriff auf existierenden Code.

Dieses Kapitel beschreibt die Verwandtschaft von Clojure und Java. Dabei richten wir unser Augenmerk nicht ausschließlich auf den üblichen Fall, dass Java-Klassen aus Clojure verwendet werden sollen, was bereits in Abschnitt 2.5 grundlegend beschrieben wurde. Auch die Gegenrichtung, Clojure aus Java zu verwenden, findet Beachtung. Letzteres ist auf zwei Ebenen möglich.

 4.1  Java aus Clojure
  4.1.1  Konstruktoren und Methoden
  4.1.2  Weitere Funktionen auf Objektinstanzen
  4.1.3  Java-Arrays
 4.2  Interfaces und abgeleitete Klassen
  4.2.1  Proxy
  4.2.2  Klassen kompilieren
 4.3  Beispiel: Plot einer Bifurkation
 4.4  Clojure als Skriptsprache
 4.5  Clojure ist auch eine Bibliothek
  4.5.1  Sequences
  4.5.2  Persistente Datenstrukturen
  4.5.3  STM
  4.5.4  Fazit
 4.6  Tuning und HotSpot
 4.7  Auslieferung
 4.8  Hintergrund: Details zur Implementation
  4.8.1  Layout des Quelltexts
  4.8.2  Metaprogrammierung

4.1  Java aus Clojure

Java-Klassen und Methoden direkt aus Clojure zu verwenden, gilt in der Clojure-Welt nicht als unfein oder Lösung zweiter Klasse. Es ist vollkommen idiomatischer Code. Tatsächlich rät Rich Hickey sogar von der Entwicklung von Wrapper-Bibliotheken, die lediglich den Zugriff auf bestimmte Klassen vereinfachen sollen, ab. Java bietet bereits in seiner Standard Edition ein so umfangreiches Ensemble von Lösungen, dass ein pragmatischer Ansatz, der versucht, in möglichst kurzer Zeit qualitativ möglichst hochwertige Programme zu schaffen, dieses Angebot nicht ignorieren darf. Einfache Syntax: PunktWenn aber dieser Zugriff als so relevant betrachtet wird, muss er einfach erfolgen können. Das ist in Clojure der Fall: Vereinfacht gesagt ist Clojures Syntax für den Griff in die Java-Welt der Punkt.

4.1.1  Konstruktoren und Methoden

Einer der ersten Schritte ist die Instantiierung von Klassen. dies wurde bereits in der Einleitung in Abschnitt 2.5 verwendet. Die ausführliche Form ist die Verwendung von new:

  (new Object)

Eher übliche Kurzform

Obwohl diese Form gerade für Clojure-Einsteiger und Java-Programmierer sicherlich vertrauter ist, taucht in realem Clojure-Code die kürzere Form deutlich häufiger auf. Diese sieht als Clojure-Code so aus, als wäre der Klassenname mit einem Punkt als Suffix ein Funktionsaufruf:

  (Object.)

Eventuelle Argumente an den Konstruktor werden in diesen Aufruf mit eingeschlossen:

  user> (String. "Hallo Welt")
  "Hallo Welt"
  user> (Integer. 99)
  99
  user> (StringBuffer.)
  #<StringBuffer >
  user> (StringBuffer. 100)
  #<StringBuffer >
  user> (StringBuffer. "Komm an Bord")
  #<StringBuffer Komm an Bord>

Qualifizierte Namen

Diese Beispiele konnten den unqualifizierten Klassennamen verwenden, da sie aus dem Paket java.lang stammen, das in der REPL-Sitzung bereits geladen ist. Falls ein Paket noch nicht importiert wurde (siehe Abschnitt 2.18.1), muss der vollständige Name qualifiziert werden.

  user> (java.io.File. "/etc/hosts")
  #<File /etc/hosts>
  user> (import ’[java.io File])
  java.io.File
  user> (File. "/etc/hosts")
  #<File /etc/hosts>

Aufrufen von Methoden

Nachdem eine Instanz vorliegt, sollen in der Regel auf dieser Instanz Methoden aufgerufen werden. Erneut spielt der Punkt bei der Syntax eine entscheidende Rolle, denn der Methodenname mit einem führenden Punkt wird hier wie eine Clojure-Funktion verwendet. Genau genommen existieren für das Aufrufen von Methoden drei alternative Formen.

  (.methode KlasseOderInstanz parameter*)
  (. KlasseOderInstanz (methode parameter*))
  (. KlasseOderInstanz methode Parameter*)

Der erste Ausdruck, bei dem der Name der Methode die Position des Funktionsaufrufs einnimmt, entspricht der Syntax eines normalen Clojure-Aufrufs. Diese Variante ist wohl auch die derzeit am häufigsten auftretende, was eine rekursive Suche nach „(\. “ und „(\.[a-zA-Z]“ im Wurzelverzeichnis der Contrib-Bibliothek mit ca. 700:30 andeutet. Für die meisten Java-Programmierer hingegen wird die alternative Reihenfolge vertrauter erscheinen. Das zusätzliche Paar Klammern, das im ersten Ausdruck den Methodennamen und die übergebenen Parameter umfasst, ist optional. Es kann jedoch einerseits die Lesbarkeit verbessern, bietet andererseits aber auch eine Alternative, die bei der Codeerzeugung durch Makros hilfreich sein kann.

Statische Felder und Methoden

Die letzten beiden Ausdrücke sind auch für das Aufrufen von statischen Methoden sowie den Zugriff auf statische Felder verwendbar, allerdings ist in diesem Fall eine weitere Syntax üblicher, die den Schrägstrich (Slash) als Separator verwendet:
  (Klasse/Feld-oder-Methode)

Die Berechnung der Fläche eines Kreises mit zufälligem Radius demonstriert die Instantiierung eines Objekts der Klasse Random, einen Methodenaufruf auf diesem Objekt und die Verwendung des statischen Felds PI aus dem Math-Paket.

  user> (import ’[java.util Random])
  java.util.Random
  user> (def rnd (Random.))
  #’user/rnd
  user> (defn kreisflaeche [radius]
          (* 2 Math/PI radius))
  #’user/circle-area
  user> (let [radius (. rnd nextInt 20)
              flaech (kreisflaeche radius)]
          (println "radius:" radius
                   "Flaeche:" flaech))
  radius: 8 Flaeche: 50.26548245743669
  nil

Locale, Kalender, Formatierung

Das folgende Beispiel zeigt einen Ausschnitt aus einer REPL-Sitzung, in der zunächst kurze Experimente mit Locale und TimeZone erfolgen, bis dann eine Instanz von GregorianCalendar erzeugt wird, auf der die Methode set mit der Signatur für das Setzen von Datum und Uhrzeit (sechs Argumente vom Typ int) aufgerufen wird. Dieses Datum wird dann mit format und Javas Formatierungsanweisung für eine Zeitangabe unter Berücksichtigung der Locale und Zeitzone (%tc) formatiert und als Rückgabewert an der REPL sichtbar.
  user> (import ’[java.util TimeZone
                            GregorianCalendar
                            Locale])
  java.util.Locale
  user> (Locale/US)
  #<Locale en_US>
  user> (doseq [id (filter
                    #(re-find #"America/L" %)
                    (TimeZone/getAvailableIDs))]
          (println id))
  America/Los_Angeles
  America/Lima
  America/Louisville
  America/La_Paz
  nil
  user> (let [greg (GregorianCalendar.
                    (TimeZone/getTimeZone
                     "America/Los_Angeles")
                    (Locale/US))]
          (.set greg 1993 11 4 6 0 0)
          (format "his final tour %tc" greg))
  "his final tour Sa Dez 04 06:00:00 PST 1993"

Weniger Wiederholungen

Häufig müssen auf einer Instanz mehrere Methoden aufgerufen werden. In Java führt das zu einer immerwährenden Wiederholung der Instanz für jeden Aufruf. Auch in Clojure-Code kann diese Form gewählt werden. Das folgende Beispiel zeigt das anhand einer etwas unsinnigen Methode, einen String „Hallo Welt“ zu erzeugen, indem es einen StringBuilder instantiiert und verschiedene Methoden auf diesem aufruft.
  user> (let [sb (StringBuilder.)]
          (.append sb "Hello")
          (.append sb " ")
          (.append sb "World")
          (.setCharAt sb 1 \a)
          (.replace sb 6 9 "Welt")
          (.deleteCharAt sb 11)
          (.deleteCharAt sb 10))
  #<StringBuilder Hallo Welt>

doto

Dieses sicherlich künstlich konstruierte Beispiel dient hier der Demonstration: Es kann deutlich eleganter durch die Verwendung des Makros doto geschrieben werden. Dieses Makro nimmt ein erstes Argument, häufig eine instantiierte Java-Klasse, entgegen und fügt dieses in alle darauf folgenden Ausdrücke als zweites Argument ein. Das obige Beispiel kann somit kompakter geschrieben werden:
  user> (doto (StringBuilder.)
          (.append "Hello")
          (.append " ")
          (.append "World")
          (.setCharAt 1 \a)
          (.replace 6 9 "Welt")
          (.deleteCharAt 11)
          (.deleteCharAt 10))
  #<StringBuilder Hallo Welt>

Rückgabewert

Beachtenswert bei doto ist der Rückgabewert. Es wird das im ersten Ausdruck erzeugte Objekt als Resultat des gesamten Ausdrucks zurückgeliefert. Somit sind die folgenden Schreibweisen äquivalent:
  user> (let [ra (java.util.Random.)]
          (.setSeed ra 100)
          (.nextFloat ra))
  0.7220096
  user> (.nextFloat (doto (java.util.Random.)
                      (.setSeed 100)))
  0.7220096

Hier kann der Aufruf von nextFloat nicht innerhalb des Rumpfes von doto erfolgen, da der Rückgabewert von doto das erzeugte Objekt ist. Gewünscht wird in diesem Fall aber die mit dem Zufallszahlengenerator erzeugte Zahl.

Ein weiteres, unvollständiges Beispiel verdeutlicht, was hier ausgedrückt werden soll:

  (let [aus-knopf (JButton. "Aus")
        an-knopf (doto (JButton. "An")
                   (.setEnabled false))])

In diesem Beispiel werden zwei Buttons erzeugt, die in einem anderen Codeabschnitt gezeichnet werden können. Dem Status der Drückbarkeit folgend, ist das dahinter liegende System aktuell „an“, kann aber durch Drücken des aktiven Buttons mit der Beschriftung „Aus“ ausgeschaltet werden. Der Button „An“ ist nicht aktiv, und genau diese Eigenschaft wurde durch das doto-Makro direkt im Zusammenhang mit der Instantiierung festgelegt, indem im Body von doto (.setEnabled false) aufgerufen wurde. Das Resultat ist eine deutlich „funktionalere“ Schreibweise eines an sich nicht funktionalen Vorgangs, nämlich der Erzeugung eines Objekts einer bestimmten Klasse und der darauf folgenden Manipulation dieses Objekts.

Makro-Lösung

In Abschnitt 2.12.3 haben wir die Nützlichkeit von Makros unter anderem mit der Wiederholung einer Instanz von JFrame begründet und eine Lösung angekündigt. Diese Lösung ist doto, und das Beispiel aus jenem Abschnitt lässt sich wie folgt komprimieren:
  user> (import ’[javax.swing JFrame JLabel])
  user> (doto (JFrame.)
          (.add (JLabel. "leWidget"))
          (.pack)
          (.setVisible true))
Der Swing-Thread
Beispiele wie das eben gezeigte erzeugen leicht von der REPL aus Swing-Anwendungen. Bei einem Aufruf direkt von der REPL aus läuft der Programmteil, der die Oberfläche zeichnet, aber im gleichen Thread ab wie die REPL auch. Native Java-Anwendungen tragen Sorge, dass der für das Zeichnen der Oberfläche und das Bearbeiten der verschiedenen Events verantwortliche Code in einem einzigen, dedizierten Thread abläuft [67]. Der Beitrag von Graham Hamilton [26] beschreibt detailliert die Hintergründe dieser Entscheidung. Kurz zusammengefasst handelt es sich um Probleme beim Locking, wenn einige Programmteile aus hohen Abstraktionsschichten, andere aus tief liegenden, betriebssystemnahen Schichten stammen. Korrekt wäre daher der Aufruf des UI-Codes mit Hilfe der statischen Methode invokeLater aus SwingUtilities.

InMethodenaufrufe auf Methodenrückgabewerten vielen Fällen wird auf dem Rückgabewert einer Methode erneut eine Methode aufgerufen. In Clojure-Code existiert auch für diesen Anwendungsfall eine alternative Ausdrucksform. Als Beispiel verwenden wir hier eine XSL-Transformation, der das folgende, minimale DocBook-Beispiel zugrunde liegt.

 
1<?xml version="1.0" encoding="UTF-8" ?> 
2<book> 
3  <bookinfo> 
4    <title>Beispiel-XML</title> 
5  </bookinfo> 
6  <chapter> 
7    <title>Beschreibung</title> 
8    <para> 
9      Dieses Dokument dient als XML-Beispiel im Clojure-Buch. 
10    </para> 
11  </chapter> 
12</book>

Dies wird durch ein ganz simples XSL-Stylesheet konvertiert, das eine Kopie des Originals erzeugt.

 
1<?xml version="1.0" encoding="UTF-8" ?> 
2<xsl:stylesheet 
3    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
4    version="1.0"> 
5  <xsl:output method="xml" indent="yes" /> 
6 
7  <xsl:template match="/"> 
8    <xsl:apply-templates /> 
9  </xsl:template> 
10  <xsl:template match="*"> 
11    <xsl:copy> 
12      <xsl:copy-of select="@*" /> 
13      <xsl:apply-templates /> 
14    </xsl:copy> 
15  </xsl:template> 
16 
17</xsl:stylesheet>

Für dieses XSL-Stylesheet wird eine Instanz von Transformer aus einer TransformerFactory erzeugt. Auf dem Transformer erfolgt dann ein Aufruf von der Methode transformer, der das XML-Dokument als Argument übergeben wird. Das Resultat – hier inklusive der Imports und der Contrib-Bibliothek java-utils, aus der as-file importiert wird – ist durch die Schachtelung der vielen Aufrufe nicht für jeden leicht lesbar.

  user> (import ’[javax.xml.transform
                  Transformer TransformerFactory])
  javax.xml.transform.TransformerFactory
  user> (import ’[javax.xml.transform.stream
                  StreamSource StreamResult])
  javax.xml.transform.stream.StreamResult
  user> (use ’[clojure.contrib.java-utils
               :only (as-file)])
  nil
  user> (.transform
         (.newTransformer
          (TransformerFactory/newInstance)
          (StreamSource. (as-file "listings/db.xsl")))
         (StreamSource. (as-file "listings/source.xml"))
         (StreamResult. *out*))
  <?xml version="1.0" encoding="UTF-8"?>
  <book>
    <bookinfo>
      <title>Beispiel-XML</title>
    </bookinfo>
    <chapter>
      <title>Beschreibung</title>
      <para>
        Dieses Dokument dient als XML-Beispiel im Clojure-Buch.
      </para>
    </chapter>
  </book>
  nil
  

Lesbarer mit Dot-Dot

Abhilfe verschafft der Dot-Dot-Operator, genauer das Dot-Dot-Makro. Dieses signalisiert durch seinen Namen, dass es an die Stelle von mehreren Punkten tritt. In diesem Falle handelt es sich um die Punkte, die im Java-Code durch das Aneinanderfügen – Chaining – von Methodenaufrufen entstehen. Das Resultat ist leichter lesbar als die vorige Variante in Clojure und als die Java-Version, die sich vermutlich über mehrere Zeilen erstrecken würde.
  user> (.. (TransformerFactory/newInstance)
            (newTransformer
             (StreamSource. (as-file "listings/db.xsl")))
            (transform
             (StreamSource. (as-file "listings/source.xml"))
             (StreamResult. *out*)))
  ;; Ausgabe wie oben

Ein Makro nebenbei

Mit einem einfachen Makro (oder einer ähnlichen Funktion) lassen sich noch die Wiederholungen von StreamSource enfernen:
  (defmacro stream-file [file]
    ‘(StreamSource. (as-file ~file)))
  
  (.. (TransformerFactory/newInstance)
      (newTransformer (stream-file "listings/db.xsl"))
      (transform (stream-file "listings/source.xml")
                 (StreamResult. *out*)))
  ;; Ausgabe wie oben

Dot-Dot macht den Ausdruck nicht notwendigerweise kürzer, aber – vor allem durch die Einrückung des Quelltexts – vermutlich für die meisten Anwender besser lesbar.

4.1.2  Weitere Funktionen auf Objektinstanzen

Java kennt das Schlüsselwort instanceOf , mit dem zur Laufzeit überprüft werden kann, ob ein Objekt einer bestimmten Klasse angehört. In Clojure ist dies mit dem Prädikat instance? möglich:

  user> (instance? Number (Integer. 1))
  true
  user> (instance? String (Integer. 1))
  false
  user> (instance? String "Hallo Welt")
  true
  user> (instance? Float 1.23)
  false
  user> (type 1.23)
  java.lang.Double
  user> (instance? Double 1.23)
  true

Java-Methoden als Funktionen

Bei der Verwendung von Funktionen höherer Ordnung, wie filter oder map, im Zusammenhang mit Java-Objekten fällt ein häufig auftretendes Muster auf: Es kommt eine anonyme Funktion zum Einsatz, die eine Methode auf dem übergebenen Argument aufruft. Diese etwas ausführlichere Form ist notwendig, weil Java-Methoden keine Clojure-Funktionen sind und somit nicht in der gleichen Art und Weise übergeben werden können. Für dieses Muster bietet Clojure mit dem Makro memfn eine Variante, mit der es möglich wird, auch Java-Methoden fast wie echte Clojure-Funktionen zu verwenden.

  user> (map #(.toUpperCase %) ["hello" "world"])
  ("HELLO" "WORLD")
  user> (map (memfn toUpperCase) ["hello" "world"])
  ("HELLO" "WORLD")
  user> (map (memfn getClass) ["Die" 2 :Trauben])
  (java.lang.String java.lang.Integer clojure.lang.Keyword)

Allerdings scheint sich die Clojure-Gemeinde gegen die Verwendung dieser Funktion entschieden zu haben. Die Verwendung ist letztlich Geschmackssache.

Beans

Eine JavaBean ist eine Klasse, die bestimmten Konventionen unterliegt. Insbesondere existieren für alle Instanzvariablen (in diesem Zusammenhang als „Properties“ bezeichnet) Get- und Set-Methoden, deren Namen mit der jeweiligen Variablen übereinstimmen. Die Funktion bean erzeugt aus einer Bean-Instanz eine Map, deren Schlüssel den Namen der Properties entsprechen, aber Keywords sind. Somit ergibt sich ein lesender Zugriff auf die Properties einer JavaBean.

  user> (use ’(clojure.contrib [repl-utils :only (show)]))
  nil
  user> (import ’[java.util Locale])
  java.util.Locale
  user> (show Locale)
  ...
  [33] getCountry : String ()
  ...
  [36] getDisplayLanguage : String ()
  ...
  user> (select-keys (bean (Locale/getDefault))
                     [:country :displayLanguage])
  {:displayLanguage "Deutsch", :country "DE"}

Schreiben in Objekten

Schreibender Zugriff auf Klassen- und Instanzvariablen ist mit dem Operator set! möglich, der auch auf Vars operieren kann. Er tauchte bereits in Abschnitt 2.12.2 auf. Dieser Operator erwartet als erstes Argument die Information, was geschrieben werden soll, und danach den zu schreibenden Wert. Im ersten Argument können sowohl statische Variablen als auch Instanzvariablen auftauchen.

  ;; existiere diese Klasse:
  ;; public class Foo {
  ;;   public static String bar;
  ;;   public String baz;
  ;; }
  user> (import ’Foo)
  Foo
  user> (set! Foo/bar "test")
  "test"
  user> (let [F (Foo.)] (set! (. F baz) "test"))
  "test"

4.1.3  Java-Arrays

In vielen bestehenden Java-Projekten und -Bibliotheken werden Java-Arrays von primitiven Datentypen wie int, long, etc. verwendet. Diese integrieren sich nicht so nahtlos in Clojure wie Javas Objekte. Ihre Verbreitung verlangt auch von Clojure die Behandlung von Java-Arrays.

Die gute Nachricht zuerst: Arrays können als Grundlage für Sequences dienen. Daher ist lesender Zugriff mit den bekannten Funktionen möglich, wie am Ende dieses Abschnitts demonstriert wird. Allerdings sind spezielle Funktionen für das Erzeugen sowie für indexbasierten Zugriff notwendig.

  user> (let [a (make-array String 8)]
    (dotimes [i 8]
      (aset a i (str "entry " i)))
    (dotimes [i (alength a)]
      (println i ":" (aget a i))))
  0 : entry 0
  1 : entry 1
  2 : entry 2
  3 : entry 3
  4 : entry 4
  5 : entry 5
  6 : entry 6
  7 : entry 7
  nil

Die Funktion make-array erwartet mindestens den Objekttyp und die Länge des Arrays als Parameter. Die Verwendung der Funktionen alength, aget und aset sollte anhand des obigen Beispiels deutlich werden.

Primitive Datentypen

Arrays von primitiven Datentypen können erzeugt werden, indem als Objekttyp die statische Variable TYPE der entsprechenden Klasse verwendet wird, zum Beispiel Integer/TYPE.

Es ist aus Performancegründen jedoch empfehlenswert, nicht mit den generischen Methoden auf Arrays von primitiven Typen zu operieren, sondern die typspezifischen Methoden wie int-array, byte-array, char-array usw. zum Erzeugen und Setzen von Werten zu verwenden.

  (defn t1 [anz]
    (let [a (make-array Integer/TYPE anz)]
      (dotimes [i anz]
        (aset a i i))))
  
  (defn t2 [anz]
    (let [a (int-array anz)]
      (dotimes [i anz]
        (aset-int a i i))))
  
  user> (time (t1 10000))
  "Elapsed time: 560.231 msecs"
  nil
  user> (time (t2 10000))
  "Elapsed time: 4.976 msecs"
  nil

Vektoren von Grunddatentypen

Eine Alternative zu Arrays von Grunddatentypen ist das seit Clojure 1.2 verfügbare vector-of, das einen Vektor von Grunddatentypen erzeugt, der sich wie ein Clojure-Vektor verhält. Sofern nicht Anforderungen von verwendeten Bibliotheken die Verwendung von Arrays verlangen, ist der Vektor vermutlich die bessere Wahl, da er sich nahtlos in Clojure integriert, wohingegen Arrays eine Sonderbehandlung verlangen.

Aus Seqs

Die Funktion into-array erlaubt das Erzeugen eines Java-Arrays aus einer Sequence. Optional kann der Typ angegeben werden, andernfalls ermittelt Clojure ihn aus dem ersten Element.

  user> (into-array Integer [1 2 3])
  #<Integer[] [Ljava.lang.Integer;@706d756b>
  user> (into-array ’("Dies" "wird" "ein" "String[]"))
  #<String[] [Ljava.lang.String;@44be6435>

Weitere Funktionen

Die Webseite von Clojure beschreibt noch weitere Funktionen, die auf Java-Arrays spezialisiert sind. Die Erzeugung von Arrays aus Datenstrukturen und das Kopieren von Array-Werten sind darunter. Auch die funktionale Verwendung kann mit amap und areduce erreicht werden.

areduce

Im Folgenden demonstrieren wir die Verwendung von areduce. Dessen formale Syntax lautet:
  (areduce a idx ret init expr)

Die Funktion iteriert über alle Elemente des Arrays a und bindet bei jedem Durchlauf den aktuellen Index an die Variable idx. Zusätzlich wird die Variable ret an das bisherige Ergebnis gebunden, was beim ersten Element des Arrays mit dem Inhalt von init vorbelegt ist. Mit diesen Variablenbindungen führt areduce dann den Ausdruck expr aus, dessen Rückgabewert im nächsten Schritt in ret zu finden sein wird.

Für die Demonstration dieser Funktion werden zunächst zwei Vars angelegt, die mit Hilfe von int-array an ein Array von ints gebunden werden.

  user> (def a1 (int-array [1 3 2 4]))
  #’user/a1
  user> a1
  #<int[] [I@235e9d>
  user> (def a2 (int-array [2 7 3 6]))
  #’user/a2

Der nächste Schritt ist Definition und Aufruf der Funktion alle>1?, deren Aufgabe es ist, zu testen, ob alle Elemente eines Arrays größer als 1 sind. In dieser Funktion findet areduce Verwendung.

  (defn alle>1? [a]
    (areduce a i ret true
             (and (> (aget a i) 1)
                  ret)))
  
  user> (alle>1? a1)
  false
  user> (alle>1? a2)
  true

amap

Auf ähnliche Weise versucht amap das Konzept von map auf Arrays zu übertragen. Auch hier wird das Array selbst mit einer Laufvariablen verwendet. Das Array, das als Rückgabe dient, wird mit einer Kopie des Ursprungsarrays vorbelegt und die einzelnen Elemente werden mit dem Ergebnis der Evaluation des Ausdrucks übergeben. Dabei ist zu beachten, dass der Typ des Arrays sich nicht ändern darf. Es ist nicht möglich, aus einem Array von Characters eines von Floats zu machen.
  user> (def a (into-array "hallo"))
  user> (aget (amap a i ret
                    (Character/toUpperCase (aget a i)))
              2)
  \L

Sequence

Da mit Hilfe von seq auch aus einem Array eine Sequence erstellt werden kann, ist es meist einfacher und eleganter, diesen Weg zu gehen. Das Beispiel zu areduce würde dann mit every? implementiert werden können.
  user> (every? #(> % 1) (seq a1))
  false
  user> (every? #(> % 1) (seq a2))
  true

4.2  Interfaces und abgeleitete Klassen

Clojures Zusammenarbeit mit Java beschränkt sich nicht auf die Verwendung von Javas Klassen in Clojure. Die Integration mit verschiedenen Bibliotheken verlangt das Ableiten von Klassen, das Implementieren von Interfaces, und manchmal beginnt ein Programm sein Leben nicht an der REPL, sondern in einer Main-Funktion von Java, die dafür sorgt, dass benötigte Klassen nachgeladen werden. Auch diese Anforderungen deckt Clojure ab. Clojure-Code kann zu Klassen kompiliert werden, die von anderen abgeleitet sind oder Interfaces implementieren oder auch einfach so, wie sie sind, verwendet werden. Auch ohne kompilierte Klassen können mit Clojure Interfaces implementiert und Klassen abgeleitet werden: mit Hilfe eines Proxy-Objekts.

4.2.1  Proxy

Eine sehr einfache Form der Integration mit fremden Bibliotheken besteht darin, ein Proxy-Objekt mit Hilfe des Makros proxy zu erstellen. Dessen formale Syntax lautet:

  (proxy [KlassenUndInterfaces+] [args*] & fns)

Interfaces und Basisklassen

Das erste Argument ist ein Vektor. Er enthält Interfaces, die implementiert werden sollen, sowie Klassen, von denen abgeleitet werden soll. Der zweite Vektor nimmt Argumente für die Konstruktoren auf. Darauf folgen die Definitionen der notwendigen Funktionen.

reify

Diese Möglichkeit, ein Interface zu implementieren, bringt Clojure seit langem mit. Vor dem Einsatz von proxy sollte jedoch die Möglichkeit, reify zu verwenden, geprüft werden. Diese Möglichkeit besteht seit Clojure 1.2. Eine Beschreibung erfolgt in Abschnitt 5.3.3.

Beispiel mit Swing

Ein Anwendungsfall ist ein ActionListener, der in einer Swing-Oberfläche an verschiedene Komponenten übergeben werden kann und, etwa im Fall eines JButton den nach einem Klick auszuführenden Code kapselt. Das folgende Beispiel erzeugt ein solches Proxy-Objekt, das das Interface ActionListener implementiert. Mit diesem Objekt kann ein einfacher Java-Frame erstellt werden, der einen Knopf beinhaltet, auf dessen Druck das Proxy-Objekt reagiert.
Close-Button
Viele Swing-Beispiele erzeugen einen neuen JFrame und setzen mit Hilfe der Methode setDefaultCloseOperation die Aktion, die beim Schließen des Swing-Fensters durchgeführt werden soll, auf die statische Konstante JFrame/EXIT_ON_CLOSE. Das empfiehlt sich jedoch nicht für Swing-Fenster, die aus Clojure erzeugt werden, da damit auch die Clojure-Laufzeitumgebung beim Schließen des Fensters beendet wird.

  user> (import [java.awt.event ActionListener]
                [javax.swing JFrame JButton])
  javax.swing.JButton
  user> (doto (JFrame.)
          (.add (doto (JButton. "Picard")
                  (.addActionListener
                   (proxy [ActionListener] []
                     (actionPerformed [e]
                                      (println "Energie!"))))))
          (.pack)
          (.setVisible true))
  #<JFrame javax.swing.JFrame[
    ;; Ausgabe gekürzt...
   rootPaneCheckingEnabled=true]>
  user> ;; Button druecken: Energie!

Nur das Notwendigste

Eine angenehme Eigenschaft dieser Form der Implementation eines Interface ist, dass Clojure erlaubt, nur die Funktionen zu implementieren, die gebraucht werden. Zwar verlangt Java die vollständige Implementation, doch Clojure belegt nicht implementierte Methoden mit einem Default, der im Falle der Verwendung eine UnsupportedOperationException wirft.

4.2.2  Klassen kompilieren

Im Umfeld von Java ist die Klasse die Lingua Franca. Für Clojure ist es unabdingbar, Klassen erzeugen zu können, wenn es sich in diesem Umfeld platzieren möchte. Hinter den Kulissen geschieht das oft für den Clojure-Programmierer unbemerkt. Gleichzeitig ist es aber auch möglich, Klassen gezielt zu erzeugen. Diese können danach sowohl von Clojure- als auch von Java-Programmen verwendet werden. Auf diese Weise lassen sich auch Klassen in Clojure erstellen, die ein Interface implementieren oder von einer anderen – möglicherweise abstrakten – Klasse ableiten.

Grundlagen

Ein erstes einfaches Beispiel zeigt das Vorgehen beim Erzeugen solcher in Clojure geschriebenen Klassen. Zudem behandelt es die bereits in Abschnitt 2.18.1 angesprochene Problematik der unterschiedlichen Auffassung von gültigen Identifiern von Clojure und Java: Der Namensraum enthält in Clojure ein Minuszeichen, das im Java-Umfeld durch einen Unterstrich ersetzt wird.

Zunächst werden zwei Verzeichnisse benötigt:

  shell> mkdir -p de/clojure_buch/
  shell> mkdir classes

Quelle

Das erste Verzeichnis beinhaltet eine Datei mit Namen hildegard.clj und folgendem Inhalt:

  (ns de.clojure-buch.hildegard
    (:gen-class))
  
  (defn -main []
    (println "Bitte sagen Sie jetzt nichts, Hildegard"))

Erzeugen der Klasse

Das sind lediglich zwei einfache Ausdrücke. Der erste definiert einen Namespace, dessen Name ein Minuszeichen enthält, der zweite eine Funktion mit dem speziellen Namen -main. Auffällig ist das neue Schlüsselwort :gen-class in der Namespace-Definition. Dieses sorgt dafür, dass eine Klasse kompiliert wird. Die Kompilation wird durch den Befehl compile angestoßen. Dieser Aufruf kann von der REPL erfolgen, aber auch die Verwendung auf der Kommandozeile ist möglich, wie im folgenden Beispiel gezeigt:

  shell> java -cp ./:classes/:clojure.jar clojure.main \
    -e "(compile ’de.clojure-buch.hildegard)"

Resultierende Klassen

Die resultierenden Klassen werden im Verzeichnis classes abgelegt. Genau genommen gibt die Variable *compile-path* den Ort an. Dieser muss sich im Classpath befinden. Bei Angabe dieses Verzeichnisses im Classpath und der aufzurufenden Klasse kann die durch -main definierte Main-Methode von der Kommandozeile aufgerufen werden:

  shell> java -cp ./:classes/:clojure.jar \
    de.clojure_buch.hildegard
  Bitte sagen Sie jetzt nichts, Hildegard

Davon, dass Clojures Compiler im Hintergrund aktiv war, zeugt das passende Verzeichnis-Listing im classes-Unterordner:

  shell> ls classes/de/clojure_buch/
  classes/de/clojure_buch/hildegard$_main.class
  classes/de/clojure_buch/hildegard$loading__4403__auto__.class
  classes/de/clojure_buch/hildegard.class
  classes/de/clojure_buch/hildegard__init.class

Clojure sowie Java

Wenn Clojure einerseits Klassen aus Java verwenden, andererseits aber auch Klassen für Java erzeugen kann, liegt es nahe, dass sich in Clojure Klassen schreiben lassen, die sowohl aus Clojure- als auch aus Java-Code verwendet werden können.

Erweiterung des Namespace

Das folgende Beispiel ähnelt dem zur Demonstration eines Proxy-Objekts aus Abschnitt 4.2.1. Es erzeugt eine Klasse, die das Interface ActionListener implementiert. Dazu verwendet es neben dem bereits gesehenen Schlüsselwort :gen-class weitere Schlüsselwörter in der Definition des Namespace.

  (ns de.clojure-buch.GenCl
    (:import [javax.swing JFrame JButton])
    (:gen-class
     :implements [java.awt.event.ActionListener]
     :init init
     :constructors {[String] []}
     :state state))

Name, Interface

Diese Definition weist Clojures Compiler an, eine Klasse mit Namen GenCl im Namespace de.clojure-buch zu erzeugen. Der Name richtet sich hier nach dem Namen des Namespace, kann aber optional auch durch Verwendung des Schlüsselwortes :name explizit angegeben werden. Die Angabe von :implements weist darauf hin, dass die resultierende Klasse das angegebene Interface implementieren wird.

Statusinformation

Um das Beispiel etwas zu erweitern, hält diese Klasse interne Statusinformationen vor. Bei aus Clojure erzeugten Klassen gibt es exakt eine interne Statusvariable. Der Name dieser Variablen wird mit dem Schlüsselwort :state angegeben. Damit dieser Status beim Aufruf des Konstruktors gesetzt wird, definiert das Beispiel eine spezielle InitInit-Funktion, deren Name durch das Schlüsselwort :init angegeben wird. Die Implementation dieser Funktion erfolgt dann in einer Funktion mit dem angegebenen Namen, versehen mit einem Präfix „-“.

  (defn -init [s]
    [[] (atom s)])

Beachtenswert ist hier der Rückgabewert. Diese Form der Init-Funktionen verlangt, dass ein Vektor als Resultat geliefert wird. Spezieller RückgabewertDas erste Element dieses Vektors gibt die Argumente an den Konstruktor der abzuleitenden Klasse an. Das zweite Argument liefert die interne Statusvariable. In diesem Falle verwenden wir ein Atom. Das ist hier eigentlich überflüssig, da diese Klasse keine Änderung am Status vorsieht. Diese Form demonstriert jedoch ein übliches Verfahren.

Implementation des Interface

Abschließend folgt die Implementation der Interface-Funktion actionPerformed, ebenfalls mit einem Präfix versehen. Diese Funktion greift durch Dereferenzieren auf den internen Status zu. Das erste Argument von -actionPerformed ist das Objekt selbst, per Konvention mit „this“ bezeichnet.

  (defn- -actionPerformed [this e]
    (println "Hello" @(.state this)))

Alternativer Aufruf des Compilers

Das Kompilieren dieser Klasse übernimmt dieses Mal ein alternativer Aufruf. Statt den Clojure-Code zum Kompilieren per -e zu übergeben, wird die Klasse clojure.lang.Compile als Hauptprogramm verwendet. Ihr kann durch Definition der Property clojure.compile.path auf der Kommandozeile der Pfad für die erzeugten Klassen übergeben werden, die in Clojure zur Laufzeit als *compile-path* verfügbar ist.

  shell> java -cp clojure.jar:./:./classes/ \
              -Dclojure.compile.path=classes \
              clojure.lang.Compile  \
              de.clojure-buch.GenCl
  Compiling de.clojure-buch.GenCl to classes

Verwendung in Clojure

Im Unterordner classes/de/clojure_buch findet sich darauf die Klassendatei GenCl.class zusammen mit den Hilfsklassen, die auch beim vorigen Beispiel schon auftauchten. Die so erzeugte Klasse kann nun direkt von einer REPL ebenso wie aus einem Clojure-Namespace verwendet werden. Das folgende Beispiel importiert die notwendigen Klassen und verwendet GenCl dann als ActionListener.
  shell> java -cp classes/:clojure.jar clojure.main
  Clojure 1.2.0
  user=> (import [de.clojure_buch GenCl])
  de.clojure_buch.GenCl
  user=> (import [javax.swing JFrame JButton])
  javax.swing.JButton
  user=> (doto (JFrame.)
           (.add (doto (JButton. "Hello")
                   (.addActionListener (GenCl. "Clojure"))))
           (.pack)
           (.setVisible true))
  #<JFrame javax.swing.JFrame[ ;; ... Ausgabe gekuerzt
  ;; Druecke Button im neuen JFrame
  Hello Clojure

Verwendung in Java

Nachdem diese Klasse, die den Druck auf einen JButton abfängt, für Clojure zur Verfügung steht, zeigt das folgende Beispiel, dass auch die Verfügbarkeit aus einem Java-Programm gegeben ist. Dazu braucht Java eine Klasse, die als Container für die Main-Funktion fungiert. Dazu dient die Datei ProgGenCl.java im Verzeichnis de/clojure_buch mit folgendem Inhalt.

 
1package de.clojure_buch; 
2 
3import de.clojure_buch.GenCl; 
4import javax.swing.JFrame; 
5import javax.swing.JButton; 
6 
7public class ProgGenCl { 
8    public static void main(String args[]) { 
9        GenCl gcl = new GenCl(args[0]); 
10        JFrame  f = new JFrame(); 
11        JButton b = new JButton("Hallo"); 
12        b.addActionListener(gcl); 
13        f.add(b); 
14        f.pack(); 
15        f.setVisible(true); 
16    } 
17}

Der Java-Compiler kann diese Datei kompilieren:

  shell> javac -cp classes/ de/clojure_buch/ProgGenCl.java

Die so erzeugte Klasse wird – wie es von Java-Klassen bekannt ist – von der Kommandozeile aus aufgerufen. In diesem Falle wird „Clojure“ als Argument übergeben, das schlussendlich dem Konstruktor von GenCl übergeben wird und in der Ausgabe nach dem Drücken des Buttons wieder auftaucht.

  shell> java -cp ./:classes/:clojure.jar \
     de.clojure_buch.ProgGenCl Clojure
  # Button drücken
  Hello Clojure
  # Programm mit Strg-c beenden

Wie das Beispiel zeigt, benötigt dieses Programm auch die JAR-Datei von Clojure in seinem Classpath, um zu laufen. Da dieses einfache Programm kein Event an den Close-Button des Fensters gebunden hat, muss es mit „Strg-c“ beendet werden.

Dieses Beispiel hat gezeigt, dass sich in Clojure geschriebene Klassen nicht nur für den Einsatz in Clojure-Programmen eignen, sondern auch für Java-Programme zur Verfügung stehen.

4.3  Beispiel: Plot einer Bifurkation

Die logistische Abbildung ist eine einfache, iterierte Abbildung, die für manche Zahlenwerte zu recht stabilen Zuständen führt, aber bei anderen chaotisches, nicht vorhersagbares Verhalten an den Tag legt. Sie wurde bereits im 19. Jahrhundert aus der Betrachtung der Entwicklung von Populationen entwickelt. An dieser Abbildung lässt sich der Effekt der Bifurkation, also des Aufspaltens in zwei Werte, beobachten.

xn+1 = μxn (1 − xn)
(4.1)

Gleichung (4.1) zeigt die iterierende Vorschrift. Einführendes Material zu den Hintergründen findet sich in einer Präsentation von Jakob Nawrath von der Uni Freiburg [49] und auf der deutschen Wikipedia [79].

Trennen von Anzeige und Berechnung

Eine Implementation in Clojure trennt sinnvollerweise das Modell zur Berechnung von der Anzeige; eine Aufgabe, die mit nativer Java-Entwicklung auch möglich ist, aber durch den erhöhten Aufwand oft nicht stattfindet. Das große Problem dabei ist, dass die Berechnung, je nach Ausstattung des Rechners und Parametern für die logistische Abbildung, zu lange dauert, um im Thread für die Swing-Aktivitäten zu laufen. Dort sollen alle Events zügig verarbeitet werden, damit die Anwendung weiterhin flüssig läuft.

Verteilen der Arbeit

Die Berechnung soll auf mehreren Threads erfolgen, der Zahlenbereich für μ wird dafür zerteilt. Das ist möglich, da die Berechnungen für verschiedene Werte von μ voneinander unabhängig sind. Wir betrachten die einzelnen Punkte, die später abgebildet werden sollen, als Koordinaten und wählen einen Vektor von Vektoren als Datenstruktur. Damit die verschiedenen Threads ihre Ergebnisse dort speichern können, verwenden wir ein Atom. Hintergrund dieser Entscheidung ist, dass die Threads konfliktfrei ihre Daten ablegen sollen sowie dass wir aber nur eine Datenstruktur haben und kein Ensemble, das den Einsatz von STM erfordern würde. Den Anfang macht die Definition des Namensraums und des datenhaltenden Atom.

  (ns de.clojure-buch.java.bifurkation
    (:import [javax.swing JFrame JButton
              SwingUtilities BorderFactory
              JPanel Timer]
             [java.awt Color Dimension]
             [java.awt.event ActionListener]
             [java.awt.image BufferedImage]))
  
  (def main-data (atom []))

Rekursion

Eine iterative Abbildung wie die logistische Abbildung wird rekursiv implementiert. Im Falle von Clojure also mit loop und recur. Um die interessanten Werte der logistischen Abbildung einzufangen, muss die Abbildung zunächst einige Male iteriert werden, ohne die Ergebnisse einzufangen, danach wird eine gewisse Anzahl von Werten aufgenommen und als Resultat zurückgegeben.

  (defn logistische-iteriert [mu spring nimm]
    (let [logabb #(* mu % (- 1.0 %))]
      (loop [x    0.5
             step 0
             acc  []]
        (if (> step (+ spring nimm))
          acc
          (if (< step spring)
            (recur (logabb x) (inc step) acc)
            (recur (logabb x) (inc step)
                   (conj acc x)))))))

Zu beachten ist hier auch die lokale Definition einer kleinen Hilfsfunktion, die letztlich einen Rechenschritt der Iteration implementiert. Diese Definition liegt in Form einer anonymen Funktion mit Read-Syntax vor, die lokal im let gespeichert wird. Alles andere in dieser Funktion dient zur Koordination der Iteration. Die Iteration ist am Ende, wenn die Anzahl der erfolgten Schritte die Summe aus zu überspringenden und zu erfassenden Schritten übersteigt. Dann wird der Akkumulator als Resultat präsentiert. Davor sind die Fälle zu unterscheiden, ob es sich noch um die Einschwingphase handelt oder um die Aufzeichnungsphase. Im ersten Falle wird der Akkumulator zwischen den Schritten nicht verändert, im zweiten Falle wird der aktuelle Wert dort hineingeschrieben.

Diese Berechnung verteilen wir durch die Verwendung von pmap anstelle von map einfach über die verfügbaren Prozessoren. Das Resultat der Iteration, die 5000 Werte verwirft und danach 100 aufzeichnet, wird noch in die korrekte Form für die zentrale Datenstruktur gebracht und dann mit Hilfe von swap! und into in das Atom geschrieben.

  (defn starte-berechnung []
    (dorun
     (pmap
      (fn [mu]
        (let [xs (logistische-iteriert mu 5000 100)
              points (vec (map (fn [x] [mu x]) xs))]
           (swap! main-data into points)))
      (range 2.90 4 0.001))))

Zusammenfassung des funktionalen Modells

Damit ist das funktionale Modell für die Berechnung fertig. Die Verteilung der Arbeit auf Threads findet nur in diesem Bereich statt und ist durch die Verwendung eines Atom abgesichert. Die Verteilung selbst übernimmt pmap auf unspektakuläre Weise. Wer eine Maschine mit vielen Kernen sein Eigen nennt, kann durch Experimente mit map und pmap einen Eindruck von der Leistungssteigerung bekommen.

Pixelberechnungen

Für die Darstellung ist weitere Berechnung notwendig. Der Wertebereich der logistischen Abbildung ist [0 : 1], der Parameter μ wird aus dem Bereich [2, 9 : 4, 0] gewählt. Die Darstellung in einem Fenster oder Bild muss diese Bereiche auf Pixelkoordinaten umrechnen. Dazu ist es notwendig, die Minima und Maxima zu kennen. Wenn wir die Minima und Maxima der Koordinaten gemeinsam als einen Wert betrachten, bietet sich die Funktion reduce an. Diese ist oft die richtige Wahl, wenn eine Sequence auf einen Wert abgebildet werden soll. Die Extrema werden gemeinsam in einer Map mit selbsterklärenden Schlüsseln gespeichert. Diese Map dient auch als Akkumulator. Mit der folgenden Hilfsfunktion stehen die Voraussetzungen dann bereit.

  (defn minmax [acc neu]
    {:xmin (min (:xmin acc) (nth neu 0))
     :xmax (max (:xmax acc) (nth neu 0))
     :ymin (min (:ymin acc) (nth neu 1))
     :ymax (max (:ymax acc) (nth neu 1))})

Ein kurzer Ausflug an die REPL zeigt den Algorithmus hierfür.

  bifurkation> (def test-daten
                    [[0 1] [1 2] [0 0]])
  bifurkation> (let [[x1 y1] (first test-daten)]
                 (reduce minmax
                         {:xmin x1 :xmax x1
                          :ymin y1 :ymax y1}
                         test-daten))
  {:xmin 0, :xmax 1, :ymin 0, :ymax 2}

Aufbauend auf der Lösung für das Auffinden von Minima und Maxima in einem eindimensionalen Datensatz aus Abschnitt 2.17.2 hätte unter Verwendung von juxt eine alternative Implementation gefunden werden können. Das hier gewählte Verfahren demonstriert hingegen, wie komplexere Datenstrukturen als Akkumulator in Rekursionen verwendet werden.

Die vollstände Umrechnung der Ergebnismenge in Pixelkoordinaten berechnet zunächst auf diese Weise die Extremwerte, definiert sich kürzere Namen und skaliert diese Werte dann auf den durch die Abmessungen des Pixelraums gegebenen Bereich.

  (defn normalize [data sx sy]
     (let [[x1 y1] (first data)
           minmax (reduce minmax
                          {:xmin x1 :xmax x1
                           :ymin y1 :ymax y1}
                          data)
           xmin (float (:xmin minmax))
           ymin (float (:ymin minmax))
           xmax (float (:xmax minmax))
           ymax (float (:ymax minmax))]
       (map (fn [[x y]]
              [(int (* sx (/ (- x xmin)
                             (- xmax xmin))))
               (int (- sy
                       (* sy (/ (- y ymin)
                                (- ymax ymin)))))])
            data)))

Grafik

Mit dieser Funktion ist es nun möglich, die berechneten Daten in einen Grafikkontext zu zeichnen. Dafür erzeugt die folgende Funktion (paint-data-img-dots) zunächst einen Snapshot der Daten durch Dereferenzieren und danach, sofern bereits Daten vorliegen, ein BufferedImage, in dem die Pixel durch Aufrufen von setRGB schwarz ((Color/black)) gezeichnet werden. Das resultierende Bild wird in das als g übergebene Graphics-Objekt gezeichnet.
  (defn paint-data-img-dots [g sx sy]
    (let [data @main-data]
      (if (< 0 (count data))
        (let [img (BufferedImage. (inc sx) (inc sy)
                                  BufferedImage/TYPE_INT_ARGB)
              d (normalize data sx sy)
              rgb (.getRGB (Color/black))]
          (doseq [[x y] d]
            (.setRGB img x y rgb))
          (.drawImage g img 0 0 nil))
        (println "Warte auf Daten ..."))))

Diese Methode ist dafür entwickelt, im Callback paintComponent einer JComponent zum Einsatz zu koemmen. Diese Verwendung zeigt die Funktion swing-setup.

  (defn swing-setup [timer sizex sizey]
    (let [p (doto (proxy [JPanel] []
                    (paintComponent
                     [g]
                     (println "Repaint")
                     (proxy-super paintComponent g)
                     (paint-data-img-dots g sizex sizey)))
              (.setBorder
               (BorderFactory/createLineBorder (Color/black)))
              (.setPreferredSize (Dimension. sizex sizey)))
          w (doto (JFrame. "CLJ Bifurkation")
              (.add p)
              (.pack)
              (.setVisible true))]
      (.addActionListener
       timer
       (proxy [java.awt.event.ActionListener] []
         (actionPerformed [e]
                          (println "Timer rennt")
                          (.repaint w)))))
    (.start timer))

Es wird ein JPanel erzeugt, dessen paintComponent-Methode mit Hilfe von proxy implementiert wird. Die Verwendung von doto erlaubt im weiteren Verlauf den Aufruf diverser Methoden direkt bei der Erzeugung des Objekts. In ähnlicher Form wird ein JFrame erzeugt, der das Panel aufnimmt. Kernstück der Darstellung ist ein Timer-Objekt, das dieser Funktion übergeben wird. Wann immer dieser Timer feuert, wird der aktuelle Stand der Berechnung gezeichnet. Das wird durch das Registrieren eines ActionListener mit der Methode addActionListener erreicht. In guter Swing-Manier löst dieses Event lediglich einen Repaint aus, was dazu führt, dass die paintComponent-Methode aufgerufen wird.

Abschließend benötigt dieses Beispiel eine Funktion, die das Ganze koordiniert:

  (defn bifurkation []
    (let [timer (Timer. 250 nil)]
    (SwingUtilities/invokeLater
     #(swing-setup timer 1000 500))
    (dosync
     (reset! main-data []))
    (starte-berechnung)
    (Thread/sleep 5000)
    (.stop timer)
    timer))

Ein Timer-Objekt, das viermal pro Sekunde feuert, wird erzeugt, und dem Swing-Thread wird der Code für die Oberfläche übergeben. Da diese Funktion sicherlich öfter aufgerufen wird, empfiehlt es sich, die gespeicherten Daten zuvor zu löschen und danach die Berechnung zu starten. Im Ganzen wartet diese Funktion dann fünf Sekunden, bis sie den Timer wieder stoppt, womit die kleine Applikation beendet ist. Abbildung 4.1 zeigt das Resultat des Aufrufs

  bifurkation> (bifurkation)
  #<Timer javax.swing.Timer@556d8a64>


Abbildung 4.1: Bifurkationsdiagramm
PIC

4.4  Clojure als Skriptsprache

Die Verwendung existierender Java-Bibliotheken ist die vermutlich häufigste Form der Java-Clojure-Interaktion; Clojure liefert jedoch auch Werkzeuge mit, um eine bestehende – in Java geschriebene Anwendung – durch Clojure zu erweitern. Übliche Motivationen für den Einsatz dieser Variante sind in der Regel die höhere Entwicklungsgeschwindigkeit im Vergleich zum Erstellen vergleichbarer Funktionalität in Java selbst oder die Bereitstellung einer Schnittstelle für die dynamische Erweiterung des Java-Programms, eine Lösung, die auch häufig mit Groovy realisiert wird.

JSR 223

Seit Java 6 gibt es die Scripting API (JSR 223); die Verwendung dieser Abstraktion verbirgt die Implementationsdetails der Skriptsprache vor dem Java-Code und ermöglicht zumindest in der Theorie ein problemloses Austauschen der Skript-Implementation. Tatsächlich gibt es zumindest eine JSR 223 Implementation auf der Grundlage von Clojure [7], die für diese Zwecke einsetzbar wäre und aktiv entwickelt wird.

Im Folgenden zeigen wir grundlegend die Verwendung von in Clojure definierten Funktionen aus einem Java-Programm.

RT

Die Klasse clojure.lang.RT ist eines der Kernstücke von Clojure. Hier findet die Initialisierung der Sprache, das Bootstrapping, statt. So wird zum Beispiel der Namensraum clojure.core erzeugt und mit Variablen sowie Funktionen gefüllt. Hier finden sich auch viele der Funktionen, die für die Interaktion mit Clojure notwendig sind.

Mit Hilfe dieser Klasse kann von Java auf in Clojure definierte Funktionen zugegriffen werden. In einer Clojure-Datei test1.clj sei eine einfache Funktion definiert:

  (ns test1)
  (defn func-in-file [in]
    (apply str (reverse in)))

Diese Datei muss sich im Classpath befinden, um gefunden zu werden, wobei die Regeln für Namensräume und Dateinamen gelten, die in Abschnitt 2.7.2 beschrieben sind. Die so definierte Funktion kann nun aus Java verwendet werden. Dazu lädt das Java-Programm zunächst mit der statischen Methode load aus RT die Datei, ermittelt dann die passende Var anhand des Funktionsnamens und ruft auf dieser die Methode invoke auf.

  RT.load("test1"); // .clj wird angehaengt
  Var func1 = RT.var("test1", "func-in-file");
  System.out.println(func1.invoke("hello"));

Dieses Beispiel zeigt nur die wichtigen Abschnitte des Java-Codes. Die notwendigen Bestandteile eines vollwertigen Programms sind hier nicht abgebildet.

Hooks

Auf diese Weise ließen sich auch Hooks mit vorgegebenen Namen definieren, die vom Java-Programm an definierten und dokumentierten Stellen aufgerufen werden. Im Clojure-Code können dann nahezu beliebige Funktionen an die Hooks gehängt werden und so die Funktion des statischen Java-Programms dynamisch erweitern. Benutzer von Emacs als Texteditor kennen dieses Verfahren von zahlreichen Beispielen.

Vars erzeugen, Compiler

Die Methode var kann auch mit drei Parametern aufgerufen werden, um eine neue Variable anzulegen. Das dritte Argument gibt dann das Root-Binding an. Unter Verwendung der Klasse clojure.lang.Compiler können so neue Funktionen nach Belieben angelegt werden, die sich mit Hilfe von invoke aufrufen lassen. Das folgende Beispiel zeigt einen Codeabschnitt aus einem Java-Programm, das in der Runtime zunächst eine Variable my-global im Namespace my-ns registriert und ihr ein Root-Binding „hello „ gibt. Darauf folgt eine weitere Var, deren Root-Binding jedoch vom Compiler erzeugt wird, der einen String mit Clojure-Code evaluiert.

  RT.var("my-ns", "my-global", "hello ");
  Var my_func = RT.var("my-ns", "my-func",
     Compiler.load(new java.io.StringReader(
        "(fn [in] (str my-ns/my-global in))")));
  
  System.out.println(my_func.invoke("world"));

Namespace

In obigem Beispiel muss die Variable „my-global“ mit ihren vollständig qualifizierten Namen verwendet werden, da das Erzeugen der anonymen Funktion nicht im Namespace „my-ns“ geschieht.

Clojure-Objekte in Interfaces

Clojure-Datenstrukturen können Java-Interfaces implementieren, daher ist es ebenfalls denkbar, den Inhalt einer Clojure-Variablen an entsprechenden Java-Code zu übergeben.

Runnable, Thread

Wie bereits in den Abschnitten 2.7.4 und 2.12.1 erwähnt wurde, implementieren Clojures Funktionen das Interface Runnable. Damit lässt sich das folgende Beispiel nachvollziehen, das eine simple Funktion vom Compiler erzeugen lässt und danach damit einen separaten Thread startet.

  Var f = RT.var("my-ns", "my-func",
     Compiler.load(new java.io.StringReader(
        "(fn [] (doseq [i (range 10)] (println i)))")));
  
  Thread t = new Thread((Runnable)f.get());
  t.start();

Rückgabewert ist nur Object.

Ein Wermutstropfen ist, dass der Typ des Rückgabewertes vieler Funktionen – etwa Var.get – java.lang.Object ist. Daher ist in der Regel eine manuelle Wandlung des Typs durch einen geeigneten Cast notwendig, und Programmierfehler werden erst zur Laufzeit auffallen, da der Java-Compiler keine Typenprüfungen durchführen kann.

4.5  Clojure ist auch eine Bibliothek

Clojure wird als eine JAR-Datei ausgeliefert. In dieser findet sich eine Vielzahl von Klassen, manche mehr, manche weniger interessant. Vor allem die persistenten Datenstrukturen und die Sequence-Funktionalitäten können auch für eigentlich in Java geschriebene Programme interessant sein. Für diese Programme ist es leicht möglich, die JAR-Datei in ihre Sammlung von Bibliotheken aufzunehmen und die dortigen Klassen zu verwenden. Allerdings verlangen die meisten Klassen eine laufende Runtime von Clojure, die beim Starten einen erheblichen Aufwand bedeutet. Das dürfte für Karl Krukow der Anlass gewesen sein, eine Bibliothek zu entwickeln und zu pflegen, die Clojures Datenstrukturen enthält, aber nicht auf die komplette Runtime angewiesen ist [42].

4.5.1  Sequences

Das erste Beispiel in diesem Abschnitt zeigt die Verwendung der Sequence-Funktionen aus einem Java-Programm.

 
1import clojure.lang.RT; 
2import clojure.lang.Var; 
3import clojure.lang.ISeq; 
4import clojure.lang.IFn; 
5import clojure.lang.Namespace; 
6import clojure.lang.Symbol; 
7import clojure.lang.Keyword; 
8import java.util.Map; 
9 
10class SeqFromJava { 
11 
12    enum LastOpeningTag { 
13        TITLE, 
14        LINK, 
15        IGNORED 
16    }; 
17 
18    static class XmlFilter { 
19 
20        LastOpeningTag lo = LastOpeningTag.IGNORED; 
21        boolean seenItem = false; 
22        String lastTitle; 
23        String lastLink; 
24 
25        static final Keyword START_ELEMENT 
26            = Keyword.intern(Symbol.create("start-element")); 
27        static final Keyword END_ELEMENT 
28            = Keyword.intern(Symbol.create("end-element")); 
29        static final Keyword CHARACTERS 
30            = Keyword.intern(Symbol.create("characters")); 
31        static final Keyword NAME 
32            = Keyword.intern(Symbol.create("name")); 
33        static final Keyword ITEM 
34            = Keyword.intern(Symbol.create("item")); 
35        static final Keyword TYPE 
36            = Keyword.intern(Symbol.create("type")); 
37        static final Keyword TITLE 
38            = Keyword.intern(Symbol.create("title")); 
39        static final Keyword LINK 
40            = Keyword.intern(Symbol.create("link")); 
41        static final Keyword STR 
42            = Keyword.intern(Symbol.create("str")); 
43 
44        void filterWithState(Map m) { 
45            if (START_ELEMENT.equals(m.get(TYPE))) { 
46                if (ITEM.equals(m.get(NAME))) 
47                    seenItem = true; 
48                else if (TITLE.equals(m.get(NAME))) 
49                    lo = LastOpeningTag.TITLE; 
50                else if (LINK.equals(m.get(NAME))) 
51                    lo = LastOpeningTag.LINK; 
52                else 
53                    lo = LastOpeningTag.IGNORED; 
54            } else if (CHARACTERS.equals(m.get(TYPE))) { 
55                if (lo.equals(LastOpeningTag.TITLE)) 
56                    lastTitle = (String)m.get(STR); 
57                else if (lo.equals(LastOpeningTag.LINK)) 
58                    lastLink = (String)m.get(STR); 
59            } else if (seenItem && 
60                       END_ELEMENT.equals(m.get(TYPE)) && 
61                       LINK.equals(m.get(NAME))) { 
62                System.out.printf("’%s’\n\t%s\n", lastTitle, 
63                                  lastLink); 
64            } 
65        } 
66    } 
67 
68    public static void main(String[] args) { 
69        try { 
70            RT.CURRENT_NS.bindRoot( 
71               Namespace.findOrCreate(Symbol.create("user")) 
72            ); 
73            RT.load("clojure/contrib/lazy_xml"); 
74            Var seqVar = RT.var("clojure.contrib.lazy-xml", 
75                                "parse-seq"); 
76 
77            ISeq seq = (ISeq)seqVar.invoke( 
78                       "http://www.reddit.com/r/clojure.rss" 
79                       ); 
80            Object o = null; 
81            XmlFilter f = new XmlFilter(); 
82            while ((seq != null) 
83                   && ((o = seq.first()) != null)) { 
84                f.filterWithState((Map)o); 
85                seq = seq.next(); 
86            } 
87        } catch (Exception e) { 
88            e.printStackTrace(); 
89        } 
90        System.exit(0); 
91    } 
92}

Die Zeilen 1–7 importieren die notwendigen Pakete aus dem Sprachumfang von Clojure. Die statische innere Klasse XmlFilter, deren Definition ab Zeile 18 erfolgt, enthält relativ viel gleichartige Wiederholungen zur Definition einiger Keywords. Zudem implementiert sie eine Filtermethode auf einer Java Map.

Ab Zeile 68 beginnt mit der Main-Methode der interessantere Teil. Mit der Methode load der Runtime RT wird die Contrib-Bibliothek clojure.contrib.lazy-xml geladen. Auf diese Weise stehen alle in Clojure implementierten Bibliotheken zur Verfügung. Aus diesem Paket extrahiert der Ausdruck in Zeile 74 eine Var – seqVar – die darauf im Java-Programm zur Verfügung steht. In Clojure werden Funktionen wie andere Typen auch durch Vars dargestellt, und die soeben importierte Var verweist auf die Funktion parse-seq. Diese Funktion ruft der Ausdruck ab Zeile 77 mit Hilfe von invoke auf. Dabei dient der RSS-Feed zum Thema Clojure auf Reddit als Quelle. Das Resultat ist eine ISeq. Ab Zeile 82 iteriert das Programm nun unter Verwendung der Interface-Funktionen von Clojure durch die Seq und wendet den XmlFilter auf jedes einzelne Element an.

Dieses Programm demonstriert exemplarisch die Verwendung der Runtime und der Sequences von Clojure. Nebenbei zeigt das Programm aber auch, wie kompakt in Clojure geschriebener Code ist. Dieser einfache XML-Parser benötigt beinahe 100 Zeilen Quelltext.

4.5.2  Persistente Datenstrukturen

Die Implementation der persistenten Datenstrukturen ist ohne Zweifel ein zentrales Element der Sprache (in Version 1.2 machen sie beinahe ein Fünftel der Java-Quellen aus). Auch in Java-Programmen wird es, ein entsprechendes Design der Algorithmen vorausgesetzt, diverse Anwendungsmöglichkeiten geben.

Ein denkbares Beispiel ist eine Situation, in der sich die Inhalte verschiedener Instanzen eines Container-Objekts teilweise überschneiden. Eine simple Implementation würde die Duplikate mehrfach im Speicher halten, andernfalls müsste man selbst einen Container mit einem Mechanismus zur geteilten Datennutzung erstellen; Letzteres wird durch die Verwendung eines Clojure-Datentyps sehr einfach.

 
1import clojure.lang.PersistentHashSet; 
2import clojure.lang.IPersistentSet; 
3 
4import java.util.Set; 
5import java.util.List; 
6import java.util.Iterator; 
7 
8class Classifier { 
9    static class Category { 
10        Category parent; 
11        String   name; 
12        Set      keywords; 
13 
14        Category(Category parent, String name, 
15                 Set keywords) { 
16            this.parent = parent; 
17            this.name = name; 
18            this.keywords = keywords; 
19        } 
20 
21        Set keys() { 
22            return (Set)unionKeys((IPersistentSet)keywords); 
23        } 
24 
25        private IPersistentSet unionKeys(IPersistentSet 
26                                         other) { 
27            Iterator i = keywords.iterator(); 
28            while (i.hasNext()) { 
29                other = (IPersistentSet)other.cons(i.next()); 
30            } 
31            if (parent != null) 
32                return parent.unionKeys(other); 
33            return other; 
34        } 
35    }; 
36 
37    static Category makeChild(Category parent, String name, 
38                               String ... keywords) { 
39        PersistentHashSet newSet = PersistentHashSet.EMPTY; 
40        for (String k : keywords) { 
41            newSet = (PersistentHashSet)newSet.cons(k); 
42        } 
43        Category c = new Category(parent, name, newSet); 
44        return c; 
45    } 
46 
47    static Category world = 
48        new Category(null, "everything", 
49                     PersistentHashSet.EMPTY); 
50 
51    static Category physics = 
52        makeChild(world, "physics", new String[] 
53            {"matter", "energy", "motion", "spacetime", 
54             "force"}); 
55 
56    static Category classicalMechanics = 
57        makeChild(physics, "classical-mechanics", 
58                   new String[] 
59            {"newton", "force", "body", "mass", "action", 
60             "reaction", "velocity", "euclidean", "speed"}); 
61 
62    static Category quantumMechanics = 
63        makeChild(physics, "quantum-mechanics", new String[] 
64            {"energy", "matter", "wave", "particle", 
65             "duality", "planck", "entanglement", 
66             "collapse"}); 
67 
68    static void print_keywords_of(Category cat) { 
69        System.out.println("Category " + cat.name + 
70                           " contains the keywords:"); 
71        System.out.println(cat.keys()); 
72    } 
73 
74    public static void main(String[] args) { 
75        print_keywords_of(physics); 
76        print_keywords_of(classicalMechanics); 
77    } 
78}

In obigem Beispiel wird das Grundkonstrukt für einen Algorithmus zur Textklassifikation dargestellt: Einer Kategorie werden Stichwörter zugeordnet, und Unterkategorien „erben“ die Begriffe ihrer Eltern.

Selbstverständlich könnte man einen vergleichbaren Effekt auch durch Java-Referenzen erreichen (eine „shallow copy“ eines Containers speichert Referenzen auf die enthaltenen Objekte anstatt Kopien anzulegen) und wäre insbesondere mit interniert gespeicherten Strings ebenfalls speichereffizient. Allerdings eröffnen persistente Container interessante Möglichkeiten; im Zusammenspiel mit Software-Transaktionen (siehe nächster Abschnitt) könnten beispielsweise neue Testdaten versuchsweise trainiert werden, wobei die Laufzeit- und Speicher-kosten für das Anlegen der Kopien minimal wären (und inbesondere nicht linear mit der Anzahl der enthaltenen Elemente skalieren).

4.5.3  STM

Auch Clojures Implementation von Software Transactional Memory lenkt häufig die Aufmerksamkeit von Entwicklern auf Clojure. Auch diese Rosine kann sich ein Java-Programmierer herauspicken. Das folgende Beispiel demonstriert, wie aus Java-Code eine clojure.lang.Ref erzeugt und in einer Transaktion manipuliert werden kann. Die Erzeugung findet im statischen Block ab Zeile 11 statt, in dem zwei Refs a und b auf den String „hello“ und dessen Länge gesetzt werden.

Die Transaktion verlangt eine Klasse, die Callable implementiert als Funktion zum Verändern der Daten. Im Beispiel erledigt das die innere Klasse StmSetter ab Zeile 25. Diese setzt zunächst a auf den übergebenen neuen String und erneut b auf dessen Länge. Die Methode run ab Zeile 40 verwendet die statische Methode runInTransaction aus der Klasse LockingTransaction, der sie die Callable implementierende Instanz übergibt.

Die Main-Methode schließlich setzt per Transaktion die Werte zunächst auf „foo“ und 3, erzeugt dann mit Hilfe einer Exception, die bei der Längenberechnung von null auftritt, einen Konflikt. Dieser steht hier stellvertretend für einen komplexeren Konflikt, der in einem Kontext mit mehreren Threads auftreten könnte. Die zwischenzeitlich erfolgende Ausgabe zeigt, dass die Werte in dieser Transaktion nicht verändert werden.

 
1import clojure.lang.LockingTransaction; 
2import clojure.lang.Ref; 
3import clojure.lang.AFn; 
4import java.util.concurrent.Callable; 
5 
6class stm { 
7    static String hello = "hello"; 
8    static Ref a; 
9    static Ref b; 
10 
11    static { 
12        try { 
13            a = new Ref(hello); 
14            b = new Ref(hello.length()); 
15        } catch (Exception e) { 
16            e.printStackTrace(); 
17        } 
18    } 
19 
20    static void prn() { 
21        System.out.println("a = " + a.deref() + ", b = " 
22                           + b.deref()); 
23    } 
24 
25    static class StmSetter implements Callable { 
26        String input; 
27 
28        StmSetter(String input) { 
29            this.input = input; 
30        } 
31 
32        public Object call() throws Exception { 
33            a.set(input); 
34            b.set(input.length()); 
35 
36            return null; 
37        } 
38    } 
39 
40    static void run(String val) { 
41        try { 
42            LockingTransaction.runInTransaction( 
43                    new StmSetter(val)); 
44        } catch (Exception e) { 
45            System.out.println(e.getClass().getName()); 
46        } 
47    } 
48 
49    public static void main(String[] args) { 
50        try { 
51            prn(); 
52            run("foo"); 
53            prn(); 
54            run(null); 
55            prn(); 
56            run("world"); 
57            prn(); 
58        } catch (Exception e) { 
59            e.printStackTrace(); 
60        } 
61    } 
62}

Auf der Kommandozeile kompiliert mit javac und ausgeführt, ergibt sich folgende Sitzung:

  shell> javac -cp clojure.jar stm.java
  shell> java -cp clojure.jar:./ stm
  a = hello, b = 5
  a = foo, b = 3
  java.lang.NullPointerException
  a = foo, b = 3
  a = world, b = 5

4.5.4  Fazit

Ein wirklich effizienter Einsatz der vorgestellten Komponenten wird vermutlich erst dann möglich, wenn man sich zumindest konzeptionell von einer rein imperativ geprägten Denkweise löst. Andernfalls wird man wahrscheinlich immer wieder vertraute Lösungsansätze verwenden und keinen echten Nutzeffekt erzielen.

Abschließend verbleibt hinsichtlich der Verwendung in Java ein wesentlicher Kritikpunkt: Die Rückgabewerte vieler Funktionen sind schwach typisiert (Object). Dieser Umstand wird durch die fehlende Parametrisierung der persistenten Container verstärkt; man fühlt sich in Zeiten vor der Einführung von Generics zurückversetzt.

Diese Kritik soll in keinem Fall die Sprache oder ihre Implementation herabsetzen. Das Design vieler Klassen macht es offensichtlich, dass eine unabhängige Nutzung als Bibliothek nie der Zielsetzung entsprach, so dass eine Übergabe von Werten als Objects sinnvoll erscheint.

4.6  Tuning und HotSpot

Die JVM liefert mit HotSpot [68] eine Maschine für die Ausführung von Bytecode, die zur Laufzeit Optimierungen vornehmen kann. Der Anspruch ist, in rechenaufwendigen Anwendungen die Performance von nativem C- oder C++-Code zu erreichen. Für Anwender, die bislang keine Erfahrungen mit virtuellen Bytecode-Maschinen haben, wirkt es ungewohnt, dass sich der erzeugte Code nach einer Weile wie von Geisterhand beschleunigt.

Standardabweichung

Ein einfaches Beispiel demonstriert die Wirksamkeit von HotSpots Optimierungen. Dazu berechnet die folgende Funktion die Standardabweichung von Werten, die in einer Sequence übergeben werden.

  (defn std-abw [xs]
    (let [n     (count xs)
          mw    (/ (reduce + xs) n)
          quads (map #(Math/pow (- % mw) 2) xs)]
      (Math/sqrt (/ (reduce + quads) (dec n)))))

Diese Funktion enthält keine Optimierungen und verschwendet zudem noch Zeit mit der Berechnung der Länge durch Aufruf von count. Ist count bei Datentypen wie Vector oder List noch O(1), so wird daraus im Falle einer Sequence eine Operation der Ordnung O(N). Zudem wurde an keiner Stelle irgendein Typ spezifiziert.

Messung

Eine Funktion zum Testen der Performance zeigt der folgende Codeabschnitt. Hier werden 10 000 ganze Zahlen zufällig zwischen 0 und 100 gezogen, aus denen dann die Standardabweichung berechnet wird. Mit Hilfe des Makros time wird ganz einfach die Zeit gemessen.

  (defn viele-berechnungen [wie-oft]
    (dotimes [_ wie-oft]
      (time
       (std-abw
        (vec (take 10000 (repeatedly #(rand-int 100))))))))
  user> (viele-berechnungen 20)
  "Elapsed time: 1155.321966 msecs"
  "Elapsed time: 362.805691 msecs"
  "Elapsed time: 472.227636 msecs"
  "Elapsed time: 358.231114 msecs"
  "Elapsed time: 170.326634 msecs"
  "Elapsed time: 120.865899 msecs"
  "Elapsed time: 118.956424 msecs"
  "Elapsed time: 118.087103 msecs"
  "Elapsed time: 120.922323 msecs"
  "Elapsed time: 118.793614 msecs"
  "Elapsed time: 111.64418 msecs"
  "Elapsed time: 113.938075 msecs"
  "Elapsed time: 111.807744 msecs"
  "Elapsed time: 110.866336 msecs"
  "Elapsed time: 117.818978 msecs"
  "Elapsed time: 115.045278 msecs"
  "Elapsed time: 112.953086 msecs"
  "Elapsed time: 114.27616 msecs"
  "Elapsed time: 124.001107 msecs"
  "Elapsed time: 108.299311 msecs"
  nil

Im Laufe von nur 20 Funktionsaufrufen hat sich die notwendige Zeit auf ein Zehntel reduziert. Diese Messung wurde auf einem DualCore-Notebook unter Ubuntu Linux gemacht. Das ist allerdings unerheblich, da hier nur die relativen Zeiten relevant sind. Weitere Aufrufe zeigen keine signifikante Leistungssteigerung mehr.

  user> (viele-berechnungen 10)
  "Elapsed time: 113.921454 msecs"
  ;; ...
  "Elapsed time: 115.417321 msecs"
  "Elapsed time: 116.979618 msecs"
  nil

Parallel?

Da die jeweilige Berechnung sehr schnell erfolgt, bringt auch eine Parallelisierung in diesem Falle keinen Geschwindigkeitszuwachs. Im Gegenteil, der zusätzliche Aufwand verlangsamt die Berechnung wieder deutlich, wie das folgende Beispiel mit pmap zeigt.

  (defn std-abw [xs]
    (let [n     (count xs)
          mw    (/ (reduce + xs) n)
          quads (pmap #(Math/pow (- % mw) 2) xs)]
      (Math/sqrt (/ (reduce + quads) (dec n)))))
  
  user> (viele-berechnungen 90)
  "Elapsed time: 921.151366 msecs"
  "Elapsed time: 517.00259 msecs"
  ;; ...
  "Elapsed time: 368.08286 msecs"
  "Elapsed time: 380.962187 msecs"
  nil

Vergleich mit Java

Eine native Implementation in Java zeigt eine deutlich bessere Performance:

  shell> java StdAbw
  Elapsed time: 32,912659 msecs
  Elapsed time: 16,886312 msecs
  Elapsed time: 21,772243 msecs
  # gekuerzt, auch hier der Effekt von HotSpot sichtbar
  Elapsed time: 4,732631 msecs

Der Quelltext des Programms ist auf der Webseite zu diesem Buch einsehbar: http://clojure-buch.de/listings/StdAbw.java. Jenes Listing implementiert ein ähnliches Verfahren, die Standardabweichung zu berechnen, wie die Clojure-Variante. Es ist uns bewusst, dass sich die Implementationen im Detail unterscheiden, aber für die Ermittlung einer ungefähren Größenordnung halten wir es für ausreichend.

Reflection

In manchen Fällen verwendet Clojure Reflection, um die richtigen Methoden zu finden, ein Verfahren, das als langsam zu betrachten ist. Solche Fälle können sichtbar gemacht werden, indem die Var *warn-on-reflection* auf true gesetzt wird. Leider bringt das in diesem Falle keine Verbesserung:

  ;; wieder die urspruengliche Version
  user> (set! *warn-on-reflection* true)
  true
  user> (viele-berechnungen 1)
  "Elapsed time: 118.648741 msecs"
  nil

Angabe von Typen

Eine wichtige Optimierung ist die Verwendung von Javas Grunddatentypen, die mit Hilfe einer der Funktionen int, long, float, double, char, boolean, short oder byte erreicht werden kann. Zudem kann der Einsatz von Typhinweisen, die in Abschnitt 2.15 beschrieben wurden, hilfreich sein. Primitive Typen können in Clojure nicht an Funktionen übergeben werden, da die Übergabe immer als Object erfolgt. Lokale Variablenbindungen hingegen können solche Typen verwenden. Es zeigt sich, dass es in diesem Beispiel reicht, lediglich einen primitiven Typen zu erzwingen:
  (defn std-abw [xs]
    (let [n     (count xs)
          mw    (float (/ (reduce + xs) n))
          quads (map #(Math/pow (- % mw) 2) xs)]
      (Math/sqrt (/ (reduce + quads) (dec n)))))
  
  user> (viele-berechnungen 4)
  "Elapsed time: 28.747785 msecs"
  "Elapsed time: 14.277022 msecs"
  "Elapsed time: 8.10453 msecs"
  "Elapsed time: 8.002072 msecs"

Ratio

Es ist zu beachten, dass Clojure bei Verwendung von / mit ganzzahligen Werten den Typ Ratio verwendet. Eine vollständige explizite Typisierung dieses Beispiels brachte keine wesentliche Verbesserung und wird daher hier ausgelassen. Das Resultat ist eine Performance, die von der gleichen Größenordnung wie die native Implementation in Java ist.

4.7  Auslieferung

Die Auslieferung und Installation von in Clojure geschriebenen Programmen, im Englischen als Deployment bezeichnet, kann auf verschiedene Weisen erfolgen.

Java

Zunächst sind die Methoden zu nennen, die sich aus der Verwandtschaft zu Java ergeben. Abschnitt 4.2.2 hat beschrieben, wie mit Clojure eine Klasse inklusive einer Main-Methode definiert und kompiliert werden kann. Das Resultat lässt sich wie jedes andere Java-Programm auch ausliefern. In ähnlicher Weise könnte eine in Java geschriebene Klasse die Main-Funktion beinhalten und Clojure-Code wie in Abschnitt 4.4 beschrieben nachladen. Diese Main-Methode hätte auch die Wahl, Clojure als Bibliothek aufzufassen und gezielt Funktionalitäten zu entnehmen, wie es der Abschnitt 4.5 beschreibt. Die letztgenannte Methode kommt vermutlich eher selten zum Einsatz.

Clojure

Zusätzlich zu diesen Methoden kann auch die Main-Methode von Clojure als Einstiegspunkt in ein Programm verwendet werden. In Abschnitt 2.18 wurden bereits die Konventionen für die Installation von Bibliotheken sowie die Möglichkeit, ein ausführbares Clojure-Programm zu erzeugen, beschrieben. Eng verwandt mit dieser Methode ist auch das explizite Starten eines Programms über die REPL. Da dies aber jederzeit einen manuellen Eingriff erfordert, ist bei produktiven und unternehmenskritischen Anwendungen davon abzusehen. Das Programm sollte beispielsweise bei einem unerwarteten Reboot automatisch wieder starten. Ein solches Programm kann aber auch jederzeit ein Framework wie Swank nachladen. Dieses Werkzeug arbeitet zwischen Emacs und Clojure und erlaubt einem Entwickler, sich mit dem laufenden System nach Belieben zu verbinden. Somit sind zumindest kleinere Deployments wie Bugfixes einzelner Funktionen ohne Neustart des Programms möglich. Es ist offensichtlich, dass dieser Zugang gut abgesichert sein sollte!

Leiningen

Die Clojure-Gemeinschaft setzt zurzeit verstärkt auf die Entwicklung eines Werkzeugs mit Namen „Leiningen“ [24], bei dem unter der Haube Maven [2] seine Arbeit verrichtet. Leiningen unterstützt den Clojure-Entwickler beim Auflösen von Abhängigkeiten wie auch beim Starten einer Clojure-Runtime, die zusätzlich einen Swank-Server starten kann.

JAR

Auch das Ausliefern der benötigten Clojure-Bibliotheken zusammen mit dem Hauptprogramm als eine JAR-Datei ist möglich. Dem Aufruf von Java kann der Name des im JAR befindlichen Clojure-Programms mit einem vorangestellten @ übergeben werden. Die folgende Sitzung in einer Shell (Bash) verdeutlicht das Verfahren.
  shell> mkdir cljar-test
  shell> cd cljar-test
  # requires the files in ..
  shell> unzip -q ../clojure.jar
  shell> cat >> mein-main.clj
  (println "Hello World")
  shell> ls
  clojure/  mein-main.clj  META-INF/
  shell> zip -rq ../cljar-test.jar *
  shell> cd ..
  shell> ls
  cljar-test/  cljar-test.jar
  shell> java -cp cljar-test.jar clojure.main \
   @mein-main.clj
  Hello World

Anwender mit Erfahrung im Ausliefern von Java-Software können ihr Wissen auf Clojure-Software weitestgehend übertragen. Wichtig ist, was auf dem Classpath zu finden ist.

4.8  Hintergrund: Details zur Implementation

Clojure ist eine Programmiersprache, deren Sprachkern in Java implementiert ist. Der gesamte Quelltext von Clojure 1.2 besteht aus etwa 130 Dateien mit etwa 20 000 Zeilen Java-Code (generated using David A. Wheeler’s „SLOCCount“[77]) und knapp 13 000 Zeilen Clojure-Code (generated using find, grep and wc). Das ist ein durchaus bemerkenswert kleiner Kern. Zum Vergleich: Das gleiche Werkzeug liefert für die Quellen von Ruby 1.8.6 etwa 144 000 Zeilen C-Code und etwa 215 000 Zeilen Rubycode, für die Quellen von Python 2.6.4 knapp 390 000 Zeilen C-Code und 370 000 Zeilen Pythoncode. Der Vergleich hinkt natürlich insofern, als beide eine Standardbibliothek mit sich bringen, die bei Clojure zumindest teilweise durch die Standardbibliothek von Java gestellt wird. Der kleine Sprachkern erlaubt es, mit relativ wenig Aufwand einen Blick hinter die Kulissen zu werfen und zum Verständnis der Aspekte der Sprache die Implementation zu konsultieren. Gerade Entwickler mit einem Java-Hintergrund werden hier wenige Überraschungen erleben, da die Implementation von Clojure gängigen Java-Standards entspricht.

Dieser Abschnitt soll einen oberflächlichen Eindruck der Implementation von Clojure vermitteln. Die Beschreibung bezieht sich auf Clojure 1.2; es ist sehr wahrscheinlich, dass sich viele Details im Verlauf der Weiterentwicklung der Sprache ändern werden.

4.8.1  Layout des Quelltexts

Die Implementation von Clojure umfasst grob zwei Teile: den grundlegenden Java-Teil sowie darauf aufbauend die bereits in Clojure implementierten Bestandteile des Sprachumfangs. Der Java-Teil gliedert sich wiederum in die Bytcodemanipulation und die Implementation von clojure.lang. NamenskonventionenLetztere hält aktuell alle Dateien in einem Verzeichnis vor. Dort gelten folgende Namenskonventionen:

Wichtige Klassen

Die folgende Liste benennt einige der wichtigeren Klassen von Clojures Implementation.
RT.java
Die Laufzeitumgebung von Clojure. Nicht nur notwendig für den Betrieb von Clojure, sondern auch für die Verwendung aus Java-Programmen heraus. Enthält Symbole ebenso wie wichtige Funktionen beispielsweise auch für Listen und die Sequence-Abstraktion.
Util.java
Kleine Sammlung von Werkzeugen wie die Erzeugung von Hashcodes oder den Vergleich von Objekten
LispReader.java
Implementation des Readers, der die in Abschnitt 2.18.2 beschriebenen Aufgaben wahrnimmt
LockingTransaction.java
Kernstück der Software Transactional Memory Implementation
Compiler.java
Der Clojure-Compiler. Eine große Datei, die viel der unter der Haube stattfindenden Magie von Clojure enthält. Verwendet die ASM-Bibliothek zur Erzeugung des Bytecodes.
Numbers.java
Implementiert die grundlegenden mathematischen Funktionen für Clojures Zahlentypen.
MultiFn.java
Implementation der Mehrfachmethoden, aus Abschnitt 2.12.4

Klassendiagramm

Der Graph in Abbildung 4.2 zeigt die Vererbungshierarchie der Klassen AFunction sowie Atom, Ref und Var. Zusätzlich enthält er einen nicht unbeträchtlichen Teil der zentralen Interfaces und Klassen des Sprachkerns.

Abbildung 4.2: Darstellung des Zusammenspiels einiger Klassen der Clojure-Implementation
PIC

4.8.2  Metaprogrammierung

Die eigentliche Codeerzeugung geschieht in der Klasse Compiler unter Verwendung der ASM-Bytecode-Bibliothek [10]. Genau genommen kann man Clojure also als MetaprogrammierungMetaprogrammiersprache auffassen, mit der nicht nur Clojure-Code (siehe Abschnitt 2.12.3), sondern auch JVM-Bytecode erzeugt werden kann.

Zusammenspiel der Klassen

Die Motivation für diesen Abschnitt ist jedoch, das Zusammenspiel der Klassen im Namensraum clojure.lang zu demonstrieren. Ein solches Verständnis kann nützlich sein, um zum Beispiel Clojure als Bibliothek zu verwenden oder in ein existierendes Java-Programm einzubetten.

Als einfaches Beispiel sei in einer Datei demo.clj folgender Inhalt gespeichert:

  (ns demo)
  (def meaning-of-life 42)
  (defn say-hello [user]
    (println (str "Hello " user ": "
                  meaning-of-life)))

Um diesen Code kompilieren zu können, muss der Classpath entsprechend angepasst werden: Einerseits muss die clj-Datei erreichbar sein, andererseits wird ein Verzeichnis für die generierten Klassen benötigt, das ebenfalls im Classpath enthalten sein muss. Dieses Verzeichnis lässt sich durch die globale Variable *compile-path* konfigurieren. Der voreingestellte Wert ist „classes“. Wenn dieses Verzeichnis existiert, kann mit der Funktion compile ein vollständiger Namespace in Bytecode übersetzt werden. Dieses Beispiel kompiliert den Namespace durch Starten der REPL und interaktiven Aufruf der Funktion compile.

$ mkdir classes  
$ java -cp clojure.jar:.:classes clojure.main  
user> (compile ’demo)  
demo

Erzeugte Klassen

In diesem Beispiel entstehen drei Klassen mit teilweise recht umständlichen Namen:

Schon auf den ersten Blick sind Zusammenhänge zum Quelltext ersichtlich wie der Name des Namespace sowie die innere Klasse, erkennbar am trennenden $, die offensichtlich aus der Funktion say-hello hervorgegangen ist. Die verbleibende Klasse enthält Clojure-spezifischen Initialisierungscode und kann hier weitgehend ignoriert werden.

Reverse

Der Bytecode in den erzeugten Klassen entspricht in etwa folgendem Java-Code, der durch einen Decompiler zurückübersetzt und zur besseren Übersicht verkürzt wurde, da diese Darstellung vermutlich verständlicher ist als die Ausgabe von javap.

  public class demo__init {
    // ...
    public static final Var c_mol =
        (Var)RT.var("demo", "meaning-of-life");
    public static final Object c_42 =
        Integer.valueOf(42);
    public static final Var c_hello =
        (Var)RT.var("demo", "say-hello");
  
    public static void load() {
      new demo.loading__6309__auto__();
      Var tmp35_32 = c_mol;
      tmp35_32.bindRoot(c_42);
      tmp35_32.setMeta(...);
      Var tmp82_79 = c_hello;
      tmp82_79.bindRoot(new demo.say_hello());
      tmp82_79.setMeta(...);
    }
    // ...
  }

Diese Klasse enthält die Deklaration und Initialisierung der Vars und des Namespace „demo“, der implizit durch das Erzeugen der ersten Var angelegt wird. Die Methode load wird in einem statischen Initialisierungsblock, der hier nicht gezeigt wird, aufgerufen, wenn die Klasse durch den Java-Classloader geladen wird.

Von Interesse sind hier die verwendeten Klassen und Methoden der Sprachimplementation; so wird durch die Methode var der Klasse RT ein Symbol mit dem gewünschten Namen angelegt und die entsprechende Variable zurückgegeben. Anschließend werden in den Var-Instanzen mittels bindRoot die initial zugeordneten Werte gespeichert sowie Metadaten wie der Name und die Zeile der ursprünglichen Quelldatei hinzugefügt.

Die zweite Klasse enthält die Implementation der Funktion. Auch hier werden Objekte vom Typ Var verwendet, um auf die Inhalte von verschiedenen Symbolen zugreifen zu können. Zudem erweitert die Klasse die Clojure-interne Klasse AFunction und überschreibt die durch die Parameteranzahl identifizierte Methode invoke.

  public class demo$say_hello extends AFunction {
    public static final Var v_println =
             (Var)RT.var("clojure.core", "println");
    public static final Var v_str =
             (Var)RT.var("clojure.core", "str");
    public static final Var v_mol =
             (Var)RT.var("demo", "meaning-of-life");
  
    // ...
  
    public Object invoke(Object user)
             throws Exception {
      return ((IFn)v_println.get()).invoke(
        ((IFn)v_str.get()).invoke(
          "Hello ", user, ": ", v_mol.get()));
    }
  }

Der Inhalt des Methodenrumpfes besteht lediglich aus dem Zugriff auf die Variableninhalte (get), einigen Casts sowie den dadurch ermöglichten Aufrufen der invoke-Methoden. Auch hier ist sichtbar, dass mit einer relativ kleinen Anzahl an Klassen verhältnismäßig viel Funktionalität erreicht wird.