Ein zweiter Blick auf JVM-Programmiersprachen

Seite 3: Clojure

Inhaltsverzeichnis

Die nächste Sprache verfolgt allein der Syntax nach einen anderen Ansatz: Clojure ist ein Lisp-Dialekt und wurde erstmals 2007 von Rich Hickey veröffentlicht. Die funktionale, dynamisch typisierte Programmiersprache kennt neben der JVM noch JavaScript und .NET als Zielplattformen. Die Syntax von Lisp-Programmen ist vergleichsweise einfach: Sie bestehen aus Listen in Präfix-Notation, das heißt, der Operator steht vor den Operanden. Im Code-Beispiel unten definiert defn eine Funktion mit einem Parameter. Das Minuszeichen wird per Konvention zur Worttrennung verwendet, statt der Camel-Case-Notation, die in Java üblich ist.

(defn say-hello [name]
(println (format "Hallo %s!" name)))

(say-hello "JavaLand")

Zur Notation der verschachtelten Listen kommen S-Expressions (Symbolic Expression) zum Einsatz. Sie lassen sich durch einen Binärbaum darstellen, wie in Abbildung 1 gezeigt. Die Besonderheit hierbei ist, dass Code und Daten dieselbe Repräsentation verwenden, was auch als Homoikonizität (Selbstabbildbarkeit) bezeichnet wird. Im folgenden Quelltextauszug repräsentieren Listen sowohl die Daten (die literale Liste) als auch den Code (die evaluierte Liste). Mit eval lassen sich die Daten ausführen. Die Funktion gibt es zwar auch in anderen Sprachen, allerdings wird in den meisten Fällen ein anderes Darstellungsformat für die Daten verwendet (z.B. als String), das erst einmal zu interpretieren ist.

Darstellung eines Ausdrucks als Binärbaum (Clojure). (Abb. 1)


'(println "Eine literale Liste")

(println "Eine evaluierte Liste (Form)")

(eval '(println "Eine literale Liste"))

Clojure bietet Kurzformen für oft genutzte Collections an. So lassen sich Vektoren, Sets und Maps schneller verwenden. Auch hier gilt, dass die eigentliche Darstellung anhand einer Liste erfolgt. Liste und Vektor unterscheiden sich anhand ihres Laufzeitverhaltens, folglich entscheidet der Anwendungsfall darüber, welche Variante der Entwickler einsetzen sollte. Im Zweifel wird empfohlen, einen Vektor zu verwenden – schon wegen der einfacheren Syntax gegenüber einer literalen Liste. Das folgende Beispiel zeigt die Collections und wie sich Symbole mit let an Datenstrukturen binden lassen. Die Bindung ist unveränderlich und nur im gegebenen Scope sichtbar.

["Ein" "Vektor"]
(vector "Ein" "Vektor")

#{"Ein" "Set"}

{:schluessel "Eine Map"}

(let [name "JavaLand"] (println name))

Rekursive Funktionen wie die unten gezeigte kommen in funktionalen Programmiersprachen häufig zum Einsatz. Im vorliegenden Fall soll sie alle Zahlen ab einem gegebenen Anfangswert verdoppeln und ruft sich dazu mit der nächsten Zahl selbst auf. Mit cons wird eine neue Liste mit dem verdoppelten Element am Anfang erzeugt. Auffällig ist, dass kein Abbruchkriterium angegeben ist. Beim Aufruf der Funktion sollen die Zahlen ab vier verdoppelt und mit take die ersten vier Stellen des Ergebnisses abgerufen werden.

Die etwas naive Implementierung einer rekursiven Funktion bricht allerdings beim Aufruf mit einem Stack-Overflow-Fehler ab, da, wie schon angedeutet, die Rekursion nicht beendet wird. Um den Fehler zu beheben, ließe sich der Funktion als zusätzlicher Parameter der Endwert übergeben, damit die Rekursion abbricht.

(defn double-from [n]
(cons (* n 2) (double-from (inc n))))

(println (take 4 (double-from 4)))

In Clojure gibt es allerdings eine viel elegantere Möglichkeit: Die Rekursion erfolgt erst dann, wenn tatsächlich auf das Element zugegriffen wird (lazy). Nachfolgend wurde der Aufruf von lazy-seq eingefügt und nun werden nur die Zahlen von vier bis sieben verdoppelt.

(defn double-from [n]
(cons (* n 2)
(lazy-seq (double-from (inc n)))))

(println (take 4 (double-from 4)))

Eine Besonderheit von Lisp-Dialekten, die viele interessante Möglichkeiten eröffnet, sind Makros. Durch das Code-as-Data-Prinzip sehen sie (fast) genauso aus wie Funktionen:

(defmacro dbg [body]
`(let [x# ~body]
(println "dbg:" '~body "=" x#)
x#))

(dbg (+ 1 2))
(println (macroexpand-1 '(dbg (+ 1 2))))

Das Makro gibt einen Ausdruck und das Ergebnis aus, was beim Debuggen hilfreich sein kann. Eine umfangreiche Beschreibung findet sich in einem entsprechenden Artikel. Hier sollte nur deutlich werden, dass Makros relativ einfach zu implementieren sind und einen großen Mehrwert bieten.

Eine so einfache Syntax wie Clojure kann keine der hier vorgestellten Programmiersprachen bieten. Einfachheit und Klarheit ziehen sich durch die gesamte Sprache, die wenigen Konzepte und die Standardbibliothek. Wer sich auf Clojure einlässt, hat die Möglichkeit, eine neue Sichtweise auf Programmiersprachen zu bekommen. Inwieweit sie dann von den Arbeitskollegen geteilt wird, bleibt abzuwarten. Haben sich aber erstmal alle auf Clojure geeinigt, ist dies eine gute Wahl und einer erfolgreichen Entwicklung steht, zumindest aus Sicht der Programmiersprache, nichts entgegen.