6  Bibliotheken

Clojure bedient sich der umfangreichen Sammlung von Bibliotheken aus dem Java-Umfeld. Dadurch bietet Clojure trotz seines jugendlichen Alters den Anwendern eine reiche Auswahl. Darüber hinaus entstehen mehr und mehr native Bibliotheken, oft als Wrapper um bestehende Bibliotheken und Funktionen von Java, um sie in Clojure leichter und idiomatischer verwenden zu können. ContribHier ist die Sammlung Clojure Contrib [35] besonders hervorzuheben. Diese kann als eine Art Staging-Area betrachtet werden, aus der besonders gute oder wichtige Bibliotheken unter Umständen den Sprung in den Sprachkern von Clojure schaffen. Clojars und weitereDaneben existiert mit Clojars [51] eine weitere Anlaufstelle für interessante Bibliotheken. Dabei ist Clojars selber bereits in Clojure geschrieben. Viele Entwickler, die ihre Leidenschaft für Clojure bereits entdeckt haben, stellen ihre Projekte auch auf den großen Git-Hostern wie github [20] oder Gitorious [63], um nur zwei willkürlich zu nennen, bereit.

Dieses Kapitel stellt einige Bibliotheken aus Clojure-Contrib und dem Sprachumfang von Clojure 1.2 vor.

 6.1  Input/Output
 6.2  XML
  6.2.1  Kernfunktionen
  6.2.2  Lazy XML
 6.3  Automatisierte Softwaretests
 6.4  Externe Programme
 6.5  Inspector
 6.6  Durchlaufen von Bäumen
 6.7  REPL-Utils
 6.8  Pretty Print
 6.9  Trace
 6.10  SQL
 6.11  Dataflow
 6.12  Abschluss

6.1  Input/Output

Operationen auf Dateien sind ein elementarer Baustein fast aller Programme; dementsprechend ist diese Funktionalität in jeder ernstzunehmenden Sprache enthalten. In Java geschieht dies durch Eingabe- und Ausgabe-Streams, die unabhängig vom eigentlichen Medium (Datei, Netzwerk, Arbeitsspeicher) das Lesen bzw. Schreiben auf der Basis von Bytes erlauben. Eine Menge von „Reader“-Klassen, die auf diesen Streams operieren, vereinfachen den Umgang mit textbasierten Dateien (zeilenweise Lesen, Zeichensatzkodierung).

In früheren Versionen von Clojure wurden diese Klassen durch die Contrib-Bibliothek duck-streams verfügbar gemacht. Einige Mehrfachmethoden (siehe Abschnitt 2.12.4) gestatteten es beispielsweise, einen Reader aus einem java.io.File, einer java.net.URL oder einem String, der als URL oder Pfadangabe interpretiert wird, zu erzeugen.

Ab Clojure 1.2 sind diese Funktionen in der Kernbibliothek im Namespace clojure.java.io enthalten. Die Implementation basiert nun auf Protokollen (siehe Abschnitt 5.2) und ist nebenbei bemerkt ein gut lesbares Beispiel für deren Einsatz.

Ein Beispiel für die Benutzung ist in Abschnitt 3.4.4 enthalten; dort wurden mit einem Reader die Zeilen einer Logdatei in eine Sequence von Strings umgewandelt.

Als zweites Beispiel folgt hier eine Funktion, die einerseits Dateien kopieren, aber darüber hinaus auch Dateien aus dem Internet herunterladen kann.

  (use ’[clojure.java.io :only
    (input-stream output-stream copy)])
  
  (defn wget [input output]
    (with-open [is (input-stream input)]
      (with-open [os (output-stream output)]
        (copy is os))))
  
  ;; Verwendung:
  user> (wget "http://example.com/test.zip"
              "test.zip")
  nil
  ;; oder auch
  user> (wget "/tmp/quelle" "/tmp/ziel")
  nil

In diesem Beispiel werden zwei Streams geöffnet (das Makro with-open übernimmt das Schließen) und der Inhalt per copy kopiert.

6.2  XML

Gerade im Umfeld von Java hat sich die Verarbeitung von XML-Daten als Standard etabliert. Erwartungsgemäß existieren qualitativ hochwertige Klassen, die diese Verarbeitung erleichtern.

6.2.1  Kernfunktionen

Clojure bringt in seinem Sprachumfang bereits einen einfachen Parser für XML-Dateien mit. Dieser findet sich im Namespace clojure.xml. Die beiden wichtigen Funktionen sind parse und emit. Erstere erwartet als Argument einen Dateinamen, einen Stream oder einen URI und liefert eine Map zurück. Die Funktion emit hingegen erwartet genau eine solche Map, wie sie parse liefert, und gibt den XML-Inhalt auf *out* aus.

  (def die-url
   (str "http://github.com/richhickey/clojure/raw/"
        "9694a92d84ddffb6794fa97efd429b5e23285553/"
        "pom-template.xml"))
  
  user> (clojure.xml/parse die-url)
  {:tag :project,
   :attrs {:xmlns "http://maven.apache.org/POM/4.0.0",
          ;; Ausgabe gekuerzt...
          },
   :content
     [{:tag :modelVersion,
       :attrs nil,
       :content ["4.0.0"]}
       ;; Ausgabe gekuerzt
       {:tag :artifactId, :attrs nil, :content ["clojure"]}
       ;; Ausgabe gekuerzt
     ]}

6.2.2  Lazy XML

Die Bibliothek clojure.contrib.lazy-xml ermöglicht es, die Baumstruktur einer XML-Datei als eine Lazy Sequence anzusprechen. Im Gegensatz zu anderen Parsern gibt es damit keine Eltern-Kind-Beziehung zwischen den enthaltenen Knoten; die Reihenfolge der Elemente in der Sequence entspricht einer Tiefensuche im Baum.

  user> (use ’(clojure [pprint :only (pprint)]))
  nil
  user> (use ’clojure.contrib.lazy-xml)
  nil
  user> (def xml-data
             "<root><foo><bar>hello</bar></foo><baz/></root>")
  #’user/xml-data
  user> (pprint (parse-seq
                 (java.io.ByteArrayInputStream.
                  (.getBytes xml-data))))
  ({:type :start-element, :name :root, :attrs {}, :str nil}
   {:type :start-element, :name :foo, :attrs {}, :str nil}
   {:type :start-element, :name :bar, :attrs {}, :str nil}
   {:type :characters, :name nil, :attrs nil, :str "hello"}
   {:type :end-element, :name :bar, :attrs nil, :str nil}
   {:type :end-element, :name :foo, :attrs nil, :str nil}
   {:type :start-element, :name :baz, :attrs {}, :str nil}
   {:type :end-element, :name :baz, :attrs nil, :str nil}
   {:type :end-element, :name :root, :attrs nil, :str nil})
  nil

Diese Funktionalität wird in Abschnitt 4.5.1 aus Java heraus verwendet. Zum Vergleich ein reines Clojure-Programm, das dieselbe Ausgabe liefert:

 
1(ns de.clojure-buch.lazy-reddit-rss) 
2 
3(use clojure.contrib.lazy-xml) 
4 
5(defn- match-key-val [m k v] 
6  (= (m k) v)) 
7 
8(defn- match-start-k [m k] 
9  (and (match-key-val m :type :start-element) 
10       (match-key-val m :name k))) 
11 
12(defn- skip-to-title [s] 
13  (loop [s s] 
14    (if (match-start-k (first s) :item) 
15      s 
16      (recur (next s))))) 
17 
18(defn- filter-els [e] 
19  (or (match-key-val e :type :characters) 
20      (or (match-start-k e :title) 
21          (match-start-k e :link)))) 
22 
23(defn- filter-groups [s] 
24  (and 
25   (match-key-val (first s) :name :title) 
26   (match-key-val (first (nnext s)) :name :link))) 
27 
28(defn parse-rss [src] 
29  (map 
30   (fn [s] [((nth s 1) :str) ((nth s 3) :str)]) 
31   (filter filter-groups 
32           (partition 4 1 
33                      (filter filter-els 
34                              (skip-to-title 
35                               (parse-seq src)))))))

Dessen Verwendung demonstriert das folgende Beispiel.

  user> (use ’de.clojure-buch.lazy-reddit-rss)
  nil
  user> (doseq [e (parse-rss
                   "http://www.reddit.com/r/clojure.rss")]
          (printf "%s\n%s\n\n" (first e) (fnext e)))
  
  ;; Ausgabe entfernt

In beiden Programmen müssen die Zeichenketten aus Elementen mit dem Typ :characters extrahiert werden, die unmittelbar auf die öffnenden Tags link bzw. title folgen. Im Java-Programm wurden Variablen verwendet, um die Position innerhalb der Sequence zu notieren und so die richtigen Werte zu selektieren. In einem funktionalen Stil ist ein anderer Ansatz wünschenswert.

Algorithmus

Die Kernidee des hier verwendeten Algorithmus ist, sich mit einer Art „sliding-window“ durch die Sequence zu bewegen. Die Funktion partition gibt in diesem Fall eine Sequence von vierelementigen Listen zurück, die jeweils um ein Element zueinander verschoben sind. Abschließend müssen nur noch diejenigen Listen herausgefiltert werden, in denen die gesuchten Tags link und title an der richtigen Position enthalten sind.

6.3  Automatisierte Softwaretests

In der modernen Softwareentwicklung ist die Verifikation bezüglich der Korrektheit einer Implementation ein zentraler Bestandteil des Entwicklungsprozesses geworden.

Framework

Clojure enthält als Kernkomponente ein Test-Framework zum Erstellen von Unit-Tests, das mittels Erweiterungen auch die Ausgabe der Ergebnisse in verschiedenen Formaten – etwa JUnit-kompatibles XML – gestattet.

In folgendem Beispiel ist ein Lindenmayer-System [55] als Clojure-Sequence implementiert. Ein solches System ist eine Variante einer formalen Grammatik, in der Zeichenketten aus einem Ausgangswert und einer Menge von Produktionsregeln erstellt werden. Die Funktionalität wird durch zwei Tests von konkreten L-Systemen überprüft, wobei einer der Tests zu Demonstrationszwecken ein falsches Ergebnis erwartet und daher immer fehlschlägt.

 
1;;; An L-System implementation in clojure 
2;;; see: http://en.wikipedia.org/wiki/L-system 
3(ns de.clojure-buch.lsystem 
4  (:require [clojure.test :as test])) 
5 
6;; the string rewrite functionality 
7(defn tokenize-str 
8  "Convert a string into a sequence of keywords." 
9  [s] (map #(keyword (str %)) s)) 
10 
11(defn process-rules 
12  "Convenience for production rules." 
13  [r] (into {} (map #(hash-map (keyword (first %)) 
14                               (tokenize-str (second %))) 
15                    r))) 
16 
17(defn lsystem-seq 
18  "String rewrite-fn as a lazy seq." 
19  [word rules] 
20  (let [t-seq (tokenize-str word) 
21        rules (process-rules rules)] 
22    (iterate (fn [in-seq] 
23               (mapcat #(if-let [r (rules %)] r [%]) 
24                       in-seq)) 
25             t-seq))) 
26 
27;; Several l-systems 
28(def algae  (lsystem-seq "A" {"A" "AB" "B" "A"})) 
29(def cantor (lsystem-seq "A" {"A" "ABA" "B" "BBB"})) 
30 
31; test-function body as a macro 
32(defmacro iterate-and-check [res s] 
33  ‘(loop [res# ~res s# ~s] 
34     (if (first res#) 
35       (do 
36         (test/is (= (tokenize-str (first res#)) 
37                     (first s#))) 
38         (recur (next res#) (next s#)))))) 
39 
40;; tests for lsystem sequence 
41(test/deftest algae-lsystem 
42  (let [res ["A" "AB" "ABA" "ABAAB" "ABAABABA" 
43             "ABAABABAABAAB" "ABAABABAABAABABAABABA" 
44             "ABAABABAABAABABAABABAABAABABAABAAB"] 
45        s algae] 
46    (iterate-and-check res s))) 
47 
48; note: incorrect result to demonstrate test failure 
49(test/deftest cantor-lsystem-failing 
50  (let [res ["A" "ABAX" "ABABBBABA" 
51             "ABABBBABABBBBBBBBBABABBBABA"] 
52        s cantor] 
53    (iterate-and-check res s)))

Ausführen der Tests

Die Tests werden ausgeführt, indem die in clojure.test definierte Funktion run-tests mit einem oder mehreren Namespaces aufgerufen wird.
  user> (use ’de.clojure-buch.lsystem)
  nil
  user> (use ’clojure.test)
  nil
  user> (run-tests ’de.clojure-buch.lsystem)
  
  Testing de.clojure-buch.lsystem
  
  FAIL in (cantor-lsystem-failing) (lsystem.clj:53)
  expected:
  ;; Ausgabe gekuerzt...
    actual: (not (clojure.core/= (:A :B :A :X) (:A :B :A)))
  
  Ran 2 tests containing 12 assertions.
  1 failures, 0 errors.
  {:type :summary, :test 2, :pass 11, :fail 1, :error 0}

Enge Bindung

Im produktiven Einsatz ist die enge Bindung zwischen Tests und Implementation unvorteilhaft, da dadurch beim Laden einer Datei Laufzeit und Speicher für die Testfälle verbraucht werden.

Diese Kosten können eingeschränkt werden, indem der Wert der Variablen *load-tests* auf false gesetzt wird; der Testcode muss zwar immer noch geparst werden, aber die Testfunktionen werden nun nicht mehr angelegt. Selbstverständlich ist es ebenso möglich, die Tests in einer separaten Datei zu definieren und die Problematik damit zu umgehen.

6.4  Externe Programme

In vielen Fällen lässt sich eine Aufgabenstellung am einfachsten durch das Aufrufen eines externen Programmes bewältigen. Für diesen Zweck gibt es in Clojure die Bibliothek clojure.java.shell. Einige einfache Anwendungsbeispiele sind bereits in früheren Kapiteln enthalten.

Optionale Parameter

Durch die Übergabe von optionalen Parametern können Daten an die Standardeingabe des aufgerufenen Programmes übergeben, das Arbeitsverzeichnis sowie die Umgebungsvariablen gesetzt und sogar die Zeichenkodierung der Standardausgabe beeinflusst werden.

C-Programm

Im folgenden Beispiel wird ein triviales C-Programm mittels des GNU C Compilers erstellt und anschließend ausgeführt. Die optionalen Parameter ermöglichen es, den zu kompilierenden Code ohne den Umweg über eine Datei direkt an den Compiler zu übergeben und das (Linux-spezifische) Temporärverzeichnis als Arbeitsverzeichnis festzulegen. Der Rückgabewert ist eine Map, die den numerischen Rückgabewert des Programms sowie die Ausgabe der Standardausgabe- und Standardfehler-Kanäle enthält.

  user> (use ’clojure.java.shell)
  nil
  user> (def code "main() { putchar(65); }")
  #’user/code
  user> (sh "gcc" "-x" "c" "-"
            :in code :dir "/tmp")
  {:in main() { putchar(65); }, :out UTF-8, :dir /tmp, :env nil}
  {:exit 0, :out "", :err ""}
  user> (sh "./a.out" :dir "/tmp")
  {:out UTF-8, :dir /tmp, :env nil}
  {:exit 65, :out "A", :err ""}

Sowohl der Name des Namespace als auch der Name der Funktion sh können bei UNIX-Benutzern eine Assoziation zu den dort üblichen Kommandozeileninterpretern auslösen, tatsächlich handelt es sich jedoch um einen Wrapper um die Methode exec der Runtime-Klasse.

In Clojure 1.1 war diese Funktionalität in der Contrib-Bibliothek unter dem Namen clojure.contrib.shell-out verfügbar, seit Version 1.2 ist sie in der Clojure-Kernbibliothek enthalten.

6.5  Inspector



Abbildung 6.1: Clojure Inspector
PIC

In Clojure bereits enthalten ist ein einfacher grafischer Inspector für baumartige Datenstrukturen. Er befindet sich im Namensraum clojure.inspect. Im Zusammenspiel mit der Funktion parse aus dem Namespace clojure.xml lassen sich so XML-Dateien betrachten. Das Resultat des folgenden Aufrufs zeigt Abbildung 6.1.

  user> (use ’clojure.inspector)
  ;; die-url wie beim XML-Beispiel
  user> (inspect-tree (clojure.xml/parse die-url))

6.6  Durchlaufen von Bäumen

Die Bibliothek clojure.walk enthält Funktionen, um einen Baum aus (nahezu) beliebigen Clojure-Datenstrukturen zu traversieren und optional sogar zu manipulieren.

Die Bibliothek beinhaltet einige Anwendungsbeispiele, anhand derer die Benutzung und auch das Potenzial deutlich werden. Da Clojure-Sourcecode vom Reader als ein Baum dieser Datenstrukturen geliefert wird, eröffnet sich die faszinierende Möglichkeit, zur Laufzeit auf dem aktiven Code zu operieren.

Das folgende Beispiel demonstriert, wie der Code aller Funktionen eines Namespace bezüglich der Verwendung eines bestimmten Symbols untersucht wird. Die Funktionalität des Auffindens aller Aufrufstellen einer Funktion ist Benutzern moderner IDEs wie zum Beispiel NetBeans vermutlich bekannt.

 
1(ns de.clojure-buch.api-search 
2  (use [clojure.walk :only (postwalk)]) 
3  (use [clojure.repl :only (source-fn)])) 
4 
5(defn source-as-sexpr [sym] 
6  (let [src (source-fn sym)] 
7    (binding [*read-eval* false] 
8      (if (nil? src) ’() (read-string src))))) 
9 
10(defn walk-source [sym-to-scan sym] 
11  (let [found-symbol (atom false)] 
12    (postwalk 
13     (fn [x] 
14       (if (= sym x) 
15         (swap! found-symbol #(or % true))) x) 
16     (source-as-sexpr sym-to-scan)) 
17    (when @found-symbol sym-to-scan))) 
18 
19(defn- symbol-usage [sym syms-to-scan-seq] 
20  (filter identity (pmap #(walk-source % sym) 
21                         syms-to-scan-seq))) 
22 
23;; Alternative ohne Parallelisierung 
24;; (defn- symbol-usage [sym syms-to-scan-seq] 
25;;   (filter #(walk-source % sym) syms-to-scan-seq)) 
26 
27(defn find-usage-in-ns 
28  ([^Symbol s] 
29     (symbol-usage s (keys (ns-publics clojure.core)))) 
30  ([^Symbol ns #^Symbol s] 
31     (require ns) 
32     (symbol-usage s (keys (ns-publics ns)))))

In den Zeilen 6–8 wird der Code einer Funktion als String geladen und mit read-string in eine durchsuchbare Datenstruktur umgewandelt; dazu wird mit Hilfe von source-fn aus clojure.repl der Quelltext ermittelt. Das Binding der Variable *read-eval* verhindert, dass der Code beim Lesen evaluiert wird. Diese Variable bezieht sich nur auf den seltenen Fall, wenn der in Tabelle 2.18.2 in Abschnitt 2.18.2 kurz erwähnte EvalReader – in Form des Reader-Makros #= – vorkommt. Da in dieser Funktion jedoch sehr viel Code gelesen wird, ist diese Vorsichtsmaßnahme angebracht.

Der erzeugte Baum, der den Quelltext widerspiegelt, wird in walk-source durchlaufen und dabei jedes Element in der anonymen Funktion (Zeile 13–14) auf Gleichheit mit dem gesuchten Symbol getestet. Das lokal gültige Atom zeigt hier, dass es prinzipiell möglich ist, Statusinformationen lokal zu speichern. Im Sinne funktionaler Programmierung ist das sehr selten guter Stil und hier auch mehr zu Demonstrationszwecken verwendet. Das Durchsuchen aller Funktionen eines Namespace geschieht in diesem Beispiel parallelisiert durch die Verwendung von pmap in Zeile 20, da dieser Vorgang relativ aufwendig ist und die einzelnen Suchvorgänge offensichtlich voneinander unabhängig sind. Wie groß der Performancegewinn ist, ist allerdings fragwürdig: Die Operation ist vermutlich I/O-gebunden, hängt also eher von der Geschwindigkeit des Speichermediums ab. Daher zeigt auch der Kommentar ab Zeile 23 eine alternative Implementation ohne Parallelisierung.

Abschließend erfolgt ab Zeile 27 die Definition der Hauptfunktion find-usage-in-ns, die mit einem oder zwei Argumenten aufgerufen werden kann. Die folgende REPL-Sitzung zeigt die Verwendung dieser neu definierten Funktion:

  user> (use ’de.clojure-buch.api-search)
  nil
  user> (use ’clojure.repl)
  nil
  user> (find-usage-in-ns ’filter)
  (find-protocol-impl filter ns remove flatten)
  user> (source remove)
  (defn remove
    ;; Ausgabe gekürzt
    [pred coll]
    (filter (complement pred) coll))
  nil

6.7  REPL-Utils

Die Hilfsfunktionen für den Einsatz an der REPL sind ein weiteres Beispiel für eine Bibliothek, die ihr Dasein in der Contrib-Sammlung begonnen hat. Seit Version 1.2 ist sie (zumindest teilweise) im Namespace clojure.repl Bestandteil von Clojure selbst. Je nach verwendeter IDE kann dieser Namespace an der REPL bereits geladen sein. Die interessanteste Funktion aus dieser Sammlung ist source, die den Quelltext von in Clojure definierten Vars ermittelt und anzeigt. Dazu greift sie auf die Metadaten an den Vars zurück, die während des Kompilierens angelegt wurden. Beispiele hierzu haben sich im Verlauf des Buches bereits ergeben.

Die bisher nur in der Contrib-Bibliothek verfügbare Funktion show listet (optional gefiltert) die Methoden und Variablen einer Klasse (beziehungsweise einer Objektinstanz) auf.

6.8  Pretty Print

Die Pretty-Print-Bibliothek – seit Version 1.2 von Clojure im Sprachstandard im Namespace clojure.pprint enthalten – erlaubt die Ausgabe von Daten und Datenstrukturen mit besserer Lesbarkeit. Die zentrale Funktion dabei ist pprint. In diesem Buch haben die Codebeispiele nur eine Breite von etwa 60 Zeichen, pprint formatiert aber auf eine Breite von 72 Zeichen. So kann der durch pprint zu erzielende Effekt ohne weitere Maßnahmen nicht gezeigt werden. Das Paket definiert aber mit *print-right-margin* eine Var, die dynamisch auf eine Breite gesetzt werden kann, so dass die Funktion von pprint auch hier sichtbar wird.

  user> (def mp {:a {:k "a" :g "A"}
                 :b ["b" "B"]})
  #’user/mp
  user> (print mp)
  {:a {:k a, :g A}, :b [b B]}nil
  user> (use ’clojure.pprint)
  user> (binding [*print-right-margin* 30]
          (pprint mp))
  {:a {:k "a", :g "A"},
   :b ["b" "B"]}
  nil

Ein gutes Hilfsmittel ist auch das Makro pp, das das letzte Resultat der REPL, das in der Var *1 vorliegt, mit pprint ausgibt.

  user> (map concat [[:a :b :c] [100 200 300]])
  ((:a :b :c) (100 200 300))
  user> (pp)
  ((:a :b :c) (100 200 300))
  nil

Common Lisp format

Zudem enthält dieses Paket eine fast vollständige Implementation der Formatfunktion von Common Lisp, als cl-format. Diese Formatierungen unterscheiden sich erheblich von denen, die Java- und C-Programmierer kennen. Es gibt zahlreiche Direktiven, unter anderem auch für die Ausgabe von Datenstrukturen, so dass die Ausgabe ohne ein Schleifenkonstrukt auskommen kann. Eine gute Einführung in diese Art der Formatierung findet sich in Kapitel 18 von Peter Seibels „Practical Common Lisp“ [60].

6.9  Trace

Wenn es während der Entwicklung eines Programms notwendig ist, die Weitergabe von Argumenten durch verschiedene Ebenen von Funktionsaufrufen zu verfolgen, hilft die Bibliothek clojure.contrib.trace. Zentral ist hier das Makro dotrace, das eine Liste von zu verfolgenden Funktionen sowie einen oder mehrere Ausdrücke akzeptiert. Die genannten Funktionen werden während der Ausführung der Ausdrücke an Varianten gebunden, die sowohl ihre Eingangsargumente als auch ihren Rückgabewert ausgeben.

Das folgende Beispiel demonstriert die Verwendung von dotrace mit zwei eigenen Funktionen.

  (defn ping [arg]
    (str "ping: " (apply str (reverse arg))))
  
  (defn pong [arg]
    (str "pong: " (ping (apply str (reverse arg)))))
  
  user> (use ’clojure.contrib.trace)
  nil
  user> (dotrace [ping pong]
          (pong "O Genie, der Herr ehre Dein Ego"))
  TRACE t1969: (pong "O Genie, der Herr ehre Dein Ego")
  TRACE t1970: |    (ping "ogE nieD erhe rreH red ,eineG O")
  TRACE t1970: |    => "ping: O Genie, der Herr ehre Dein Ego"
  TRACE t1969: => "pong: ping: O Genie, der Herr ehre Dein Ego"
  "pong: ping: O Genie, der Herr ehre Dein Ego"
  user> (dotrace [ping]
          (pong "O Genie, der Herr ehre Dein Ego"))
  TRACE t2852: (ping "ogE nieD erhe rreH red ,eineG O")
  TRACE t2852: => "ping: O Genie, der Herr ehre Dein Ego"
  "pong: ping: O Genie, der Herr ehre Dein Ego"

Eine Beschränkung hierbei ist, dass nicht alle Funktionen auf diese Weise verfolgbar sind. Das betrifft zum einen Funktionen, die aus Performancegründen inline ausgeführt werden, etwa die numerischen Funktionen, aber auch die Funktionen, die für dotrace verwendet werden. Offensichtlich führt das Verfolgen einer solchen Funktion zu einem Überlauf des Stacks. Ein Beispiel hierfür ist str. In der Regel wird man sich ohnehin auf die eigenen Funktionen beschränken wollen, dann fallen die Beschränkungen nicht mehr ins Gewicht.

6.10  SQL

Für die Kommunikation mit relationalen Datenbanken via SQL bietet das Paket clojure.contrib.sql vereinfachende Funktionen, die die Verwendung von JDBC kapseln. Einige der Funktionen stellen wir anhand eines Datei-Indexes vor.

Eine ganz einfache Tabelle nimmt lediglich Dateinamen auf und versieht sie mit einem eindeutigen Index:

  CREATE TABLE IF NOT EXISTS ‘FINDEX‘ (
    ‘id‘ bigint unsigned NOT NULL auto_increment,
    ‘fpath‘ varchar(4096) NOT NULL,
     PRIMARY KEY  (‘ID‘)
  ) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Eine reale Anwendung wird sicherlich noch weitere Eigenschaften an dieser Stelle speichern sowie indizierte Begriffe in anderen Tabellen ablegen, für dieses Beispiel reicht diese Tabelle aber aus.

Datenbankverbindung

Für die Verbindung zur Datenbank braucht die Bibliothek Angaben zu User, Passwort, Host, Datenbanknamen und Port. Diese werden zusammen mit den für JDBC typischen Angaben in einer Map gespeichert; die Vorbereitungen für die folgenden Beispiele werden durch das Laden der SQL-Unterstützung abgeschlossen. Selbstverständlich muss der JDBC-Treiber für die verwendete Datenbank im Classpath der JVM vorhanden sein.

  (def conn-parm
       {:user "jimknopf"
        :pass "molly"
        :host "localhost"
        :port "3306"
        :db   "cljtest"
        })
  
  (def db {:classname "com.mysql.jdbc.Driver"
           :subprotocol "mysql"
           :subname (str "//" (conn-parm :host)
                         ":"  (conn-parm :port)
                         "/"  (conn-parm :db))
           :user (conn-parm :user)
           :password (conn-parm :pass)})
  
  (use ’clojure.contrib.sql)

Inserts

Einen File-Index zu erstellen ist denkbar einfach. Clojures Funktion file-seq liefert die Dateien, und die Java-Methode getPath liefert den zu speichernden Pfad. Die Funktion, die die Datensätze in die Datenbank schreibt, ist insert-records. Sie erwartet den Namen der Tabelle (als Keyword) und die Werte als Maps, in denen die Schlüssel die Spalten angeben. Da insert-records die Werte nicht als eine Liste erwartet, sondern als einzelne Argumente, stellen wir ein apply voran.

  ;; index files
  user> (with-connection db
          (apply
           insert-records
           :FINDEX
           (map
            (fn [f] {:fpath (.getPath f)})
            (file-seq (java.io.File. "/etc/java-6-sun/")))))

Makro

Dieser Ausdruck verwendet das sehr wichtige Makro with-connection. Die Funktionen in clojure.contrib.sql erwarten, dass die Datenbank-Verbindung in der Variablen *db* vorliegt. Mit with-connection wird diese Var dynamisch gebunden.

Resultsets

Für die nächste Funktion, die alle Dateinamen aus dem so erstellten Index liest, setzen wir die Datenbank-Verbindung bereits voraus. Der Grund dafür ist, dass wir auf die Datensätze später außerhalb der Funktion zugreifen müssen, was nicht mehr möglich wäre, wenn die Datenbank-Verbindung schon geschlossen wäre.

Das SQL-Statement wird an with-query-results übergeben, das zudem einen Namen verlangt, unter dem die einzelnen Sätze angesprochen werden können.

  (defn files-in-db []
    (let [sql "select FPATH from FINDEX"]
      (with-query-results rows [sql]
        (into [] rows))))
  
  user> (with-connection db
          (dorun
           (map println
                (files-in-db))))
  {:fpath /etc/java-6-sun}
  {:fpath /etc/java-6-sun/content-types.properties}
  ;;  Ausgabe verkürzt
  {:fpath /etc/java-6-sun/net.properties}
  {:fpath /etc/java-6-sun/calendars.properties}
  nil

Da sich die Dateien auf einer Festplatte gelegentlich ändern, würde im Rahmen der Wartung eine Konsistenzprüfung auf Existenz der Dateien erfolgen. Zunächst erzeugen wir dafür mit Hilfe von insert-records zwei Datensätze, zu denen es keine Dateien gibt.

  ;; erzeuge zwei fehler
  user> (with-connection db
          (insert-records
           :FINDEX
           {:fpath "GibtEsNicht"}
           {:fpath "gibt.es.auch.nicht"}))
  nil

Die Prüfung auf Existenz einer Datei erledigt die exists-Methode von File. Mit Hilfe von complement wird dessen Ergebnis negiert und direkt an filter übergeben.

  (defn file-exists? [sysf]
   "pruefe files auf vorhandensein."
   (.exists (java.io.File. sysf)))
  
  user> (with-connection db
          (filter (complement #(file-exists? (:fpath %)))
                  (files-in-db)))
  ({:fpath "GibtEsNicht"}
   {:fpath "gibt.es.auch.nicht"})

6.11  Dataflow

Die Dataflow-Bibliothek ist eine geeignete Grundlage, um ein Netzwerk aus miteinander verknüpften Zellen zu erzeugen. Einigen dieser Zellen, den Quellzellen, können Werte direkt zugewiesen werden, in anderen wird der enthaltene Wert aus den als Abhängigkeit angegebenen Zellen errechnet. Dieses Netz ist inbesondere nützlich, um ein System aus voneinander abhängigen Variablen zu definieren, in dem sich die Veränderungen der Quellwerte automatisch auf ihre Abhängigkeiten übertragen.

  user> (use ’clojure.contrib.dataflow)
  nil
  
  (defn not-negative? [a]
    (if (< a 0)
      (throw (RuntimeException. "Value is not positive"))))
  
  (defn print-flow-values [df key-seq]
    (apply concat
           (map #(vector (keyword %) (get-value df %))
                key-seq)))
  
  (def rectangle-geometry-flow
       (build-dataflow
        [(cell :source width 0)
         (cell :source height 0)
  
         (cell area (* ?height ?width))
         (cell circumference (+ (* 2 ?height) (* 2 ?width)))
  
         (cell :validator (not-negative? ?height))
         (cell :validator (not-negative? ?width))
         ]))
  user> (update-values rectangle-geometry-flow
                       {’width 5 ’height 10})
  nil
  user> (print-flow-values rectangle-geometry-flow
                           ’(height width area circumference))
  (:height 10 :width 5 :area 50 :circumference 30)
  user> (try
         (update-values rectangle-geometry-flow {’width -5})
         (catch Throwable e (println e)))
  #<Exception java.lang.Exception:
    (cell :validator (not-negative? ?width))>
  nil

In diesem trivialen Beispiel wird ein „Flow“ mit Hilfe des Befehls build-dataflow angelegt. In diesem Flow existieren zwei Quellzellen (width und height), die mit dem Schlüsselwort :source erzeugt werden. Ihnen können mit der Funktion update-values Werte zugewiesen werden. Zwei Validierungszellen – erzeugt mit :validator – stellen mit der weiter oben definierten Funktion not-negative? sicher, dass keiner der Werte negativ ist. Im Falle eines negativen Wertes wird, wie demonstriert, eine Exception geworfen. Die Zellen area und circumference berechnen anhand der Quellwerte den Flächeninhalt bzw. den Umfang eines Rechtecks.

Quellzellen sowie abhängige Zellen werden durch ihren Namen identifiziert und können über ihn (mit einem vorangestellten Fragezeichen) beliebig zur Berechnung von weiteren abhängigen Werten sowie in Validierungsfunktionen verwendet werden.

Der Name einer Zelle muss allerdings nicht eindeutig sein. Falls mehrere Zellen mit identischem Namen existieren, können diese mit der Syntax ?*mehrfach selektiert werden. So kann wie im folgenden Beispiel eine Berechnung in Einzelteile zerlegt werden (hier veranschaulicht als Preisnachlässe in Abhängigkeit von verschiedenen Faktoren). Dies ist allerdings nur sinnvoll, wenn letztendlich die Gesamtheit der Werte der wiederholten Zellen verwendet wird, da es aufgrund der mehrdeutigen Namen nicht mehr möglich ist, eine der Zellen explizit anzusprechen.

  (def price-calc
       (build-dataflow
        [(cell :source item-price 10)
         (cell :source item-count 5)
         (cell discount (if (> ?item-count 3) 0.95 1))
         (cell discount (if (> ?item-price 5) 0.8 1))
         (cell total-price (* ?item-price ?item-count))
         (cell final-price (* ?total-price
                              (reduce * ?*discount)))]))

Der vermutlich interessanteste Aspekt ist, dass die Werte der abhängigen Zellen nicht bei jedem Zugriff (get-value) berechnet werden; vielmehr führt das Aktualisieren einer Quellzelle zur Neuberechnung der Werte in allen direkt oder indirekt beteiligten Zellen.

6.12  Abschluss

Insgesamt existiert bereits eine erstaunlich große Menge an Bibliotheken; alleine in Clojure selbst sowie der dazugehörigen Contrib-Sammlung sind Funktionen für diverse Anwendungszwecke vorhanden.

In einigen Fällen handelt es sich um relativ einfache Wrapper, die einen „funktionalen“ Zugriff auf bekannte Java-Schnittstellen ermöglichen; zum Beispiel die Funktionen in den Namespaces logging, swing-utils und server-socket (alle drei sind Teil der Contrib-Bibliothek).

Auf der anderen Seite gibt es von Java unabhängige Libraries, die teilweise nicht unerhebliche Komplexität aufweisen. Es ist im Rahmen dieses Buches unmöglich, auf alle einzugehen, einige der subjektiv wichtigsten sollen jedoch zumindest Erwähnung finden:

Viele weitere Bibliotheken sind auf der Seite „Clojure Libraries“ [33] oder aber auch verstreut im Internet zu finden. Die überwiegende Mehrzahl steht unter freien Lizenzen zum Download bereit, die meist auch die kommerzielle Nutzung gestatten.