Dieses Posting setzt unsere Clojure-Einführung (hier Teil 1) fort. Clojure war ein Pionier bei der Bereitstellung von effizienten rein funktionalen Datenstrukturen (siehe hier für eine Einführung). Dieses Posting erklärt, was es damit auf sich hat.

Übrigens …

Auf unserer Konferenz BOB 2015 gibt es ein Clojure-Tutorial!

Beispiele nachvollziehen

Um die Beispiele in diesem Posting nachzuvollziehen, reicht eine einfache Clojure-REPL. Entweder können Sie - wie in Teil 1 beschrieben - Nightcode hochfahren und die dortige REPL benutzen, oder Sie begeben sich in einem Kommandozeilen-Fenster in das Projektverzeichnis vom letzten Mal und tippen ein

lein repl

… und schon erscheint ein Prompt fp1.core==> (oder so ähnlich), an dem Sie Clojure-Ausdrücke eintippen können und sichten, was dabei herauskommt.

Einen guten Referenz für die eingebauten Clojure-Funktionen - nach Datentyp sortiert - gibt es zu Beispiel bei Grimoire.

Primitive Datentypen

In diesem Abschnitt kümmern wir uns erst einmal um „primitive“ Datentypen, die nicht wesentlich strukturiert sind. Booleans zum Beispiel sind true und false:

fp1.core=> true
true
fp1.core=> false
false

Booleans werden meist mit der if-Form weiterverarbeitet:

fp1.core=> (if true 1 2)
1
fp1.core=> (if false 1 2)
2

Neben false gibt es in Clojure noch einen weiteren Wert, der als boolesch „falsch“ durchgeht, nil:

fp1.core=> (if nil 1 2)
2

nil entspricht Javas null und ist mit der gleichen Vorsicht zu behandeln: Pragmatisch verwenden wir nil als Platzhalter dafür, dass etwas nicht da ist.

Außer false und nil zählen alle anderen Werte als „wahr“:

fp1.core=> (if 1 1 2)
1
fp1.core=> (if "foo" 1 2)
1

Die erste Überraschung zeigt, dass Clojure beliebig große ganze Zahlen verarbeiten kann:

fp1.core=> 98234723894735984357439543534
98234723894735984357439543534N
fp1.core=> (+ 2137812634 1214723657843564375384)
1214723657845702188018N

Zeichenketten sind allerdings wie in Java:

fp1.core=> "foo"
"foo"

Clojure ist ein Lisp-Dialekt, entsprechend gibt es dort auch Symbole. Diese kommen aber - anders als in anderen Lisps - fast ausschließlich bei der Programmierung von Makros zum Einsatz. Diese sparen wir uns deshalb für ein zukünftiges Posting auf.

Für den täglichen Einsatz hat Clojure stattdessen Keywords, die wir benutzen können, um Aufzählungen zum Beispiel für Status-Werte und ähnliches zu repräsentieren. Keyword-Literale sind durch den Doppelpunkt am Anfang zu erkennen:

fp1.core=> :ok
:ok
fp1.core=> :fail
:fail
fp1.core=> :delayed
:delayed

Folgen

Für Folgen kennt Clojure gleich drei verschiedene Datentypen: Listen, Vektoren und sogenannte Seqs. Sie sind allesamt funktionale Datenstrukturen - es wird also niemals an eine Folge „in situ“ etwas hinzugefügt, wie etwa bei den Java-Collections üblich.

Die drei Folgentypen unterscheiden sich - grob - folgendermaßen:

  • Listen sind die klassischen einfach verketteten Listen, wie in Lisp und anderen funktionalen Sprachen. Der primitive Konstruktor cons hängt an eine bestehende Liste vorn ein neues Element dran. Hinten an eine Liste etwas dranhängen ist teuer.

  • Vektoren bilden einen allgemeiner verwendbaren Datentyp. Insbesondere ist es billig, an einen Vektor hinten etwas dranzuhängen. (Vektoren sind durch hochverzweigte Bäume effizient repräsentiert.)

  • Seqs sind nicht-strikte („lazy“) Datenstrukturen, die erst wirklich konstruiert werden, wenn Elemente aus der Folge abgerufen werden.

Die folgenden Abschnitte gehen etwas mehr ins Detail:

Listen

Der Listentyp entspricht der klassichen Datendefinition:

Eine Liste ist eins der folgenden …

  • die leere Liste (Literal '())
  • eine Paar bestehend aus erstem Element und dem Rest, ebenfalls eine Liste (konstruiert mit cons)
; einelementige Liste aus 1
fp1.core=> (cons 1 '())
(1)
; zweielementige Liste
fp1.core=> (cons 1 (cons 2 '()))
(1 2)

Vektoren

Vektoren entstehen komplementär zu Listen durch Anhängen hinten mit der Funktion conj. Der leere Vektor wird durch das Literal [] gebildet:

fp1.core=> []
[]
fp1.core=> (conj [] 1)
[1]
fp1.core=> (conj (conj [] 1) 2)
[1 2]

Außerdem können wir Vektoren aus Elementen direkt mit den eckigen Klammern bilden:

fp1.core=> [1 2 3]
[1 2 3]

Vektoren unterstützen außerdem noch sehr schnelles Umdrehen mit rseq und außerdem die Extraktion von Teilfolgen mit subvec.

Wegen der praktischen []-Notation und der effizienten Repräsentation sind Vektoren in Clojure eher der Typ der Wahl für Folgen, nicht Listen wie in anderen Lisps.

Seqs

Schließlich sind da noch die Seqs, die „lazy“ konstruiert werden. Aus jedem Folgen-Datentyp wird ein Seq mit Hilfe der Funktion seq:

fp1.core=> (seq [1 2 3])
(1 2 3)

(Die runden Klammern sind also nicht - wie bei anderen Lisps - den Listen vorbehalten.)

Seqs entstehen im Alltag am häufigsten durch die Verwendung von eingebauten Folgenkombinatoren wie map:

fp1.core=> (map (fn [x] (+ x 1)) [1 2 3])
(2 3 4)

map produziert also - obwohl die Eingabefolge ein Vektor ist - als Ausgabe eine Seq, keinen Vektor. Das ist praktisch, weil die Funktion, die bei map benutzt wurde, erst aufgerufen wird, wenn das entsprechende Element aus dem Ergebnis herausgezogen ist. Das bedeutet aber auch, dass Seiteneffekte in diesen Funktionen problematisch sind, weil sie möglicherweise erst viel später nach der Anwendung von map zur Ausführung kommen, wie das folgende Beispiel zeigt:

fp1.core=> (def x (map println [1 2 3]))
#'fp1.core/x
fp1.core=> x
(1
2
3
nil nil nil)

Hier wird das println erst dann angewendet, wenn der Inhalt von x ausgedruckt wird, nicht schon bei der Definition von x. (Clojure druckt erst die öffnende Klammer, holt dann die drei Elemente heraus - aus Effizienzgründen sind es meist mehrere auf einmal - diese werden dabei ausgedruckt, und das Ergebnis ist dann die Liste aus den drei nils, die println zurückgegeben hat.)

Seqs merken sich die produzierten Elemente, so dass eine wiederholte Auswertung von x nicht dazu führt, dass alle Elemente erneut berechnet werden.

Wer strikte Ausführung benötigt, ist oft mit spezialisierten Operationen wie mapv (map auf Vektoren) besser bedient. Außerdem gibt es die Funktion doall, die alle Elemente einer Seq auswertet.

Gemeinsame Operationen

Viele Operationen in Clojure funktionieren auf allen drei Repräsentationen gleichermaßen. Zum Beispiel extrahiert first immer das erste Element und rest die Restfolge nach dem ersten Element.

Ein paar dieser Funktionen sind allerdings auch etwas verwirrend: conj zum Beispiel fügt ein Element einer beliebigen Folge hinzu, allerdings hängt es vom Folgentyp ab, an welchem Ende:

fp1.core=> (conj [1 2 3] 4)
[1 2 3 4]
fp1.core=> (conj (list 1 2 3) 4)
(4 1 2 3)
fp1.core=> (conj (seq [1 2 3]) 4)
(4 1 2 3)

Mengen

Clojure hat auch einen Typ für Mengen eingebaut. Mengenliterale sind von #{...} um umschlossen:

fp1.core=> #{1 2 3}
#{1 3 2}
fp1.core=> #{:red :blue :green}
#{:green :red :blue}

Aus anderen Folgen können wir Mengen mit der Funktion set konstruieren:

fp1.core=> (set [1 2 3])
#{1 3 2}

Elemente können zu Mengen mit conj hinzugefügt und mit disj entfernt werden:

fp1.core=> (conj #{1 2 3} 4)
#{1 4 3 2}
fp1.core=> (disj #{1 2 3} 2)
#{1 3}

Um festzustellen, ob ein Wert Element einer Menge ist, können wir das Mengenobjekt wie eine Funktion behandeltn:

fp1.core=> (def s #{1 2 3})
#'fp1.core/s
fp1.core=> (s 1)
1
fp1.core=> (s 4)
nil

Etwas lesbarer ist es aber vielleicht, die Funktion contains? zu verwenden:

fp1.core=> (contains? s 1)
true
fp1.core=> (contains? s 4)
false

Maps

Schließlich gibt es in Clojure noch Maps, also Tabellen aus Schlüsseln und Werten. Literale werden mit {...} umschlossen, innen wechseln sich Schlüssel und Werte ab. (Manche Clojure-Programmierer schreiben noch ein Komma zwischen Schlüssel-/Wert-Paare.)

fp1.core=> {:foo 1 :bar 2}
{:bar 2, :foo 1}

Maps können mit assoc erweitert und mit dissoc verkleinert werden:

fp1.core=> (def m {:foo 1 :bar 2})
#'fp1.core/m
fp1.core=> (assoc m :baz 3)
{:baz 3, :bar 2, :foo 1}
fp1.core=> (assoc m :foo 3)
{:bar 2, :foo 3}
fp1.core=> (dissoc m :bar)
{:foo 1}

Um an den Wert zu einem Schlüssel zu kommen, können Maps - wie Mengen

  • als Funktion verwendet werden:
fp1.core=> (m :foo)
1
fp1.core=> (m :baz)
nil

Verwirrenderweise können Schlüsselwörter auch als Zugriffsfunktion auf Maps dienen:

fp1.core=> (:foo m)
1
fp1.core=> (:baz m)
nil

Auch hier ist es unter Umständen lesbarer, auf die explizite get-Funktion zurückzugreifen:

fp1.core=> (get m :foo)
1
fp1.core=> (get m :baz)
nil

So weit, so gut …

Das war also der Turbo-Streifzug durch die eingebauten Clojure-Datenstrukturen. Ich hoffe, er war erhellend. Den nächsten Teil der Clojure-Einführung gibt es bald auf diesem Blog.