Clojure: Ein pragmatisches Lisp für die JVM

Man kann das Gefühl bekommen, Programmiersprachen auf Basis der Java Virtual Machine gebe es im Dutzend billiger.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 20 Min.
Von
  • Stefan Tilkov
Inhaltsverzeichnis

Man kann das Gefühl bekommen, Programmiersprachen auf Basis der Java Virtual Machine (JVM) gebe es im Dutzend billiger: JRuby, Groovy, Scala, Jython und das gute alte Java – alle ringen um des Programmierers Gunst. Vielen erschien Scala als der bereits gesetzte "Gewinner", aber in den letzten Monaten konkurriert zumindest in Sachen öffentlicher Aufmerksamkeit deutlich eine andere Sprache – Clojure.

Clojure ist ein speziell für die JVM entwickelter Lisp-Dialekt, der insbesondere durch seine Unterstützung für die Entwicklung von Anwendungen für Multicore-Plattformen glänzt. War Lisp nicht diese in der Praxis völlig irrelevante Sprache mit den unsäglich vielen Klammern, mit der man im Informatik-Studium gequält wurde oder wird? Unter dem Ruf leidet Lisp seit einigen Jahrzehnten, und die recht zersplitterte und teilweise nicht eben demütige Lisp-Gemeinde hat es bislang nicht geschafft, das zu ändern. Clojure ist anders, denn es ist vor allem eines: praktisch. Es lohnt sich daher, eine eventuelle initiale Scheu vor der Syntax zu überwinden: Es gibt einen guten Grund dafür, und zumindest der Autor dieser Zeilen hat sich mittlerweile nicht nur daran gewöhnt, sondern empfindet sie als die eleganteste denkbare Variante.

Neben Literalen für Zahlen, Strings und reguläre Ausdrücke unterstützt Clojure als wichtigste Datentypen Listen, Vektoren, Mengen und assoziative Arrays (Maps). Listen sind in allen Lisp-Sprachen das wichtigste Element, und darin unterscheidet sich Clojure nicht von seinen Ahnen (obwohl auch Vektoren eine große Rolle spielen, wie der Artikel gleich zeigt). Sie werden als geklammerter Ausdruck dargestellt, können Elemente beliebiger Art enthalten und lassen sich ineinander verschachteln:

'(1 2 3)
'(1 "Ein String" 2.45)
'(1 2 ("a" :b "c") ("d" "e" (:x "y")))

Vektoren unterstützen einen effizienten Indexzugriff und werden mit eckigen Klammern notiert. Maps werden mit {...} dargestellt, Sets (Mengen) mit #{...}:

[1 2 3]
[1 2 [4 5 6] ["a" "b"]]
{1 "Eins", 2 "Zwei"}
#{1 2 3 4}
Mehr Infos

Clojure

Clojure 1.0 wurde im Mai 2009 freigegeben, die aktuell stabile Version 1.1 stammt aus dem Dezember 2009. Die Version 1.2 steht unmittelbar vor ihrer Freigabe. Allen Versionen ist gemeinsam, dass sie vor allem Erweiterungen mit sich brachten beziehungsweise bringen: Bis auf die Änderung einiger Namensräume waren in der Regel kaum Auswirkungen auf bestehenden Code zu befürchten. In Clojure 1.2 sind vor allem Performanceoptimierungen und Ergänzungen enthalten, die mittelfristig eine vollständige Implementierung von Clojure in Clojure ermöglichen sollen.

Clojure-Vater Rich Hickey legt in seinen Präsentationen nach der Folie zu Datentypen gerne als Nächstes eine mit dem Titel "Syntax" auf und sagt dazu "You've just seen it" – und tatsächlich hat die Sprache fast keine anderen Elemente. Clojure-Code wird in den Datenstrukturen abgelegt, vor allem in Listen und Vektoren: Code und Daten sind letztlich das Gleiche. Diese besondere  Eigenschaft spielt eine große Rolle, wenn es darum geht, wiederkehrende Muster zu automatisieren: Lisp braucht hierzu keinen externen Mechanismus wie einen Code-Generator, sondern bringt ihn als Bestandteil der Sprache mit – dazu später mehr.

Da es sich bei Clojure um eine funktionale Programmiersprache handelt, ist das wichtigste Code-Konstrukt der Funktionsaufruf. Dazu interpretiert ein Lisp das jeweils erste Element einer Liste als eine Funktion (genauer: etwas, das zu einer Funktion evaluiert) und die restlichen Elemente als deren Parameter. Die Funktion str zum Beispiel verkettet ihre Parameter zu einem String. Ein Aufruf sieht wie folgt aus (das Ergebnis steht hinter dem ->-Symbol in der nächsten Zeile):

(str "a" "b" "c")
->  "abc"

Will man die Auswertung (Evaluation) des Listenausdrucks verhindern, kann man die Funktion "quote" oder ihre Abkürzung ' (ein einfaches Hochkomma) benutzen:

'(str "a" "b" "c")
->  (str "a" "b" "c")

Funktionsnamen können aus praktisch beliebigen Zeichen bestehen, bekommen in vielen Fällen eine variable Anzahl von Parametern und lassen sich beliebig schachteln. Sonderregeln für Operatoren erübrigen sich damit. Das folgende Beispiel zeigt die Funktionen '*', '+' und '-' im Einsatz:

(str "Das Ergebnis ist: " (* (+ 2 4) (- 10 8)))
->  "Das Ergebnis ist: 12"

Die Syntax von Funktionsdefinitionen zeigt, wie eng die Datenstrukturen und der Code zusammenhängen: Eine Funktionsdefinition ist eine Liste, die aus dem Schlüsselwort defn, einem Namen, einem Vektor mit den Parametern besteht, gefolgt von der Implementierung selbst (die wiederum eine Liste ist, die den oben genannten Regeln folgt):

(defn multiply [a b] (* a b))
(multiply 3 6)
->  18

defn als Schlüsselwort zu bezeichnen ist genau genommen falsch: Es handelt sich um ein Makro, eine weitere Besonderheit von Lisp-Sprachen. Die Funktion multiply von oben lässt sich auch als Zuweisung einer Funktion an eine Variable definieren:

(def multiply (fn [a b] (* a b)))

In Lisp sind zwar fast alle Sprachkonstrukte selbst Funktionen. Aber es gib eine Grenze, einen kleinen Sprachkern, den die sogenannten "Special Forms" bilden. Zu ihnen zählen auch def und fn: Ersteres definiert eine neue Variable, Letzteres eine neue (anonyme) Funktion.

Wie der Name defn suggeriert, werden damit die Schritte def und fn zusammengefasst – man definiert eine Funktion und gibt ihr im selben Schritt einen Namen. Dass der Entwickler einer Programmiersprache eine solche Konstruktion einbauen kann, verwundert nicht. Das Besondere an Lisp-Dialekten wie Clojure ist jedoch, dass sich solche Vereinfachungen nicht nur durch eine Änderung des Sprachkerns, sondern auch mit den Mitteln der Sprache erstellen lassen. Genau dazu verwendet man Makros: Ein solches Makro ist ein Stück Code, das Code schreibt. Auch defn ist als ebensolches implementiert. Das Ergebnis des Makros kann man sich mit der Funktion macroexpand ansehen:

(macroexpand '(defn multiply [a b] (* a b)))
->  (def multiply
 (.withMeta (fn multiply ([a b] (* a b))) (.meta #'multiply)))

Das clojure.core/ vor dem fn ist der Namespace, in dem die Form definiert ist. In anderen Programmiersprachen würde man für eine derartige Automatisierung einen
zusätzlichen Code-Generator benötigen, bei Lisp ist dieser Mechanismus eingebaut. Eine (stark vereinfachte) Version des defn-Makros ließe sich wie folgt definieren:

(defmacro defn2 [name & fdecl] (list 'def name (cons 'fn fdecl)))

Die Funktion list erzeugt eine neue Liste aus einzelnen Elementen, cons fügt ein Element am Anfang einer Liste ein – das Makro generiert also aus seinen Argumenten eine neue Datenstruktur, die den zu erzeugenden Code enthält. (In der Praxis würde man syntax-quote, eine Art Templating-Mechanismus verwenden.) Ein Makro verhält sich ähnlich wie eine Funktion, wird allerdings während der Kompilierung und nicht erst zur Laufzeit des Programms ausgewertet. Zur Überprüfung lässt sich wieder macroexpand verwenden:

(macroexpand '(defn2 multiply [a b] (* a b)))
->  (def multiply (fn [a b] (* a b)))

Der Zusammenhang zwischen Makros, Funktionen und Special Forms lässt sich zudem am Beispiel von Kontrollstrukturen zeigen. Andere Sprachen haben für Aspekte wie eine Bedingung ein eigenes Schlüsselwort, bei Clojure und anderen Lisp-Dialekten sieht auch eine Bedingung aus wie ein Funktionsaufruf. Das illustriert die folgende einfache Funktion:

(defn gleich3 [v]
  (if (== v 3)
    (str v " ist durch 3 teilbar")
    (str v " ist nicht durch 3 teilbar")))
(gleich3 3)
 "3 ist durch 3 teilbar"
(gleich3 4)
 "4 ist nicht durch 3 teilbar"

Makros sind eines der interessantesten Merkmale von Lisp-basierten und -inspirierten Sprachen. Steht etwas Vergleichbares nicht zur Verfügung (wie bei Java), greifen Entwickler häufig auf externe Code-Generatoren zurück.

Eine weitere wesentliche Zutat für ein Verständnis der Lisp-Besonderheiten sind Funktionen höherer Ordnung, also Funktionen, bei denen ein oder mehrere Parameter selbst wieder Funktionen sein können. Ein Beispiel für eine solche Funktion ist map:. Diese Funktion transformiert eine Liste in eine neue, indem sie auf jedes Element eine als Parameter übergebene Funktion anwendet:

(defn double-number [v] (* 2 v))
(map double-number '(1 2 3 4 5))
->  (2 4 6 8 10)

Eine etwas komplizierte Funktion ist die folgende, die für durch 3 und 5 teilbare Zahlen "FizzBuzz", für durch 3 teilbare Zahlen "Fizz", für durch 5 teilbare Zahlen "Buzz" und für alle anderen die Zahl selbst zurückliefert:

(defn fizzbuzz [n]
  (cond
    (and (= (rem n 3) 0) (= (rem n 5) 0)) "FizzBuzz"
    (= (rem n 3) 0) "Fizz"
    (= (rem n 5) 0) "Buzz"
    :else n))

An fizzbuzz lässt sich ein weiterer Clojure-Mechanismus erläutern: unendliche Sequenzen, die sich erst verspätet ("lazy") evaluieren lassen. So liefert (iterate inc 1) eine Sequenz zurück, die in jedem Schritt die Funktion inc (inkrementieren) aufruft und dabei den jeweils letzten Wert übergibt (mit 1 als Startwert).

Das Ergebnis ist die Menge der natürlichen Zahlen. Diese komplett zu verarbeiten wäre keine sonderlich gute Idee; daher hilft die Funktion take, die ersten n Elemente aus einer solchen Sequenz abzurufen. In Kombination mit map lässt sich damit elegant die fizzbuzz-Folge für die Zahlen von 1 bis 25 generieren:

(map fizzbuzz (take 25 (iterate inc 1)))
->  (1
  2
  "Fizz"
  4
  "Buzz"
  "Fizz"
  7
  8
  "Fizz"
  "Buzz"
  11
  "Fizz"
  13
  14
  "FizzBuzz"
  16
  17
  "Fizz"
  19
  "Buzz"
  "Fizz"
  22
  23
  "Fizz"
  "Buzz")