Dieses Posting setzt unsere Clojure-Einführung (hier Teil 1, Teil 2, Teil 3) fort. Dieses Mal geht es um zusammengesetzte Daten.

In Teil 2 der Clojure-Einführung haben wir uns mit den eingebauten Datenstrukturen beschäftigt. Heute behandeln wir die Definition neuer zusammengesetzter Datentypen.

Als Beispiel gehen wir in einen Computerladen und stellen uns einen Individualrecher aus Einzelteilen zusammen. Um diese Zusammenstellung zu beschreiben (zum Beispiel für einen Online-Shop), entwerfen wir eine Repräsentation als Daten. Dieser Prozess fängt mit einer Datendefinition an:

; Ein Computer besteht aus:
; - Prozessor
; - Hauptspeicher-Kapazität in Gbyte
; - Festplatten-Kapazität in Gbyte

Die Datendefinition sagt klar aus, dass ein Computer aus mehreren Teilen besteht, wir brauchen also zusammengesetzte Daten. In Clojure sind für zusammengesetzte Daten Records zuständig, und wir können einen Record-Typ für zusammengesetzte Daten mit der defrecord-Form definieren:

(defrecord Computer
[processor ram hard-drive])

Diese Definition stellt eine Java-Klasse names Computer her, mit Feldern processor, ram und hard-drive. (Java-Programmierer würden Computer eine POJO-Klasse nennen. Da Java-Klassen in der Regel mit einem Großbuchstaben anfangen, übernehmen wir die Konvention für Record-Typen.) Damit können wir das in Clojure eingebaute new-Konstrukt verwenden, um Computer-Objekte herzustellen:

(def gamer (new Computer "Cell" 4 1000)) ; Cell, 4 Gbyte RAM, 1000 Gbyte Festplatte
(def workstation (new Computer "Xeon" 2 500)) ; Xeon, 2 Gbyte RAM, 500 Gbyte Festplatte

Wir können new Computer abkürzen mit Computer. (beachten Sie den Punkt):

(def workstation (Computer. "Xeon" 2 500))

Allerdings ist zu berücksichtigen, dass Computer. keine Funktion, sondern ein Makro ist. Wenn wir versuchen, sie als eigenständiges Objekt zu verwenden, passiert folgendes:

records> Computer.
CompilerException java.lang.ClassNotFoundException: Computer.

Clojure stellt aber glücklicherweise auch noch eine Konstruktor-Funktion namens ->Computer zur Verfügung:

(def workstation (->Computer "Xeon" 2 500))

Die einzelnen Teile eines Records können wir folgendermaßen extrahieren:

records> (:processor gamer)
"Cell"
records> (:ram gamer)
4
records> (:hard-drive gamer)
1000

Die Keywords mit den Feldnamen fungieren also als Selektoren - das liegt daran, dass in Clojure jeder Record auch als Map funktioniert. (Siehe unsere Einführung in Datentypen.)

Das reicht eigentlich schon, um mit Records in Clojure zu hantieren. Allerdings tauchen zusammengesetzte Daten oft auch als Fälle in gemischten Daten auf, wofür wir noch ein Prädikat für einen Record-Typ brauchen, also eine Möglichkeit, Objekte der Record-Definition von anderen Objekten zu unterscheiden.

Wir schreiben zur Illustration ein Programm zur Fütterung zweier Sorten Tiere: Gürteltiere und Papageien.

Hier ist eine Datendefinition für Gürteltiere, eine dazu passende Record-Definition und ein Beispiel-Gürteltier:

; Ein Gürteltier hat folgende Eigenschaften:
; - Gewicht (in g)
; - lebendig oder tot
(defrecord Dillo
[weight alive?])

(def d1 (->Dillo 55000 true)) ; 55 kg, lebendig

… und hier ist das gleiche nochmal für den Papagei:

; Ein Papagei hat folgende Eigenschaften:
; - Gewicht in Gramm
; - Satz, den er sagt
(defrecord Parrot
[weight sentence])

(def p1 (->Parrot 10000 "Der Gärtner war's.")) ; 10kg, Miss Marple

Ein Gürteltier zu füttern erhöht dessen Gewicht, zumindest wenn es noch lebt:

(defn feed-dillo
"Gürteltier füttern."
[d]
(if (:alive? d)
(->Dillo (+ (:weight d) 500) true)
d))

Bei einem Papagei klappt das immer:

(defn feed-parrot
"Papagei füttern."
[p]
(->Parrot (+ (:weight p) 50)
(:sentence p)))

Nun schreiben wir eine Funktion, die sich sowohl für Gürteltiere als auch für Papageien zuständig fühlt. Die Datendefinition dafür sieht so aus:

; Ein Tier ist eins der folgenden:
; - ein Gürteltier
; - ein Papagei

Um eine Funktion feed-animal zu schreiben müssen wir eine Verzweigung je nachdem machen, ob es sich bei einem Tier um ein Gürteltier oder einen Papagei handelt. Das funktioniert mit instance?. (instance? C x) testet, ob x eine Instanz der Klasse C ist, insbesondere also, ob x zum Record-Typ C gehört:

(defn feed-animal
"Tier füttern."
[a]
(cond
(instance? Dillo a) (feed-dillo a)
(instance? Parrot a) (feed-parrot a)))

Damit wären alle Bestandteile beisammen, die wir für den Umgang mit neuen Typen für zusammengesetzte Daten brauchen: Konstruktor, Selektoren und Prädikat. Genug für heute!