Datentypen

Gesamtübersicht: Datentypen


Einfache Typen

Clojure bietet alle gewöhnlichen einfachen Typen, wie Zeichen, Zeichenketten, ganze Zahlen und Fließkommazahlen. Diese Typen sind identisch mit denen, die auch in Java verwendet werden. Eine Besonderheit ist jedoch, dass die ganzen Zahlen einen unbegrenzten Wertebereich haben. Dies wird dadurch gelöst, das intern automatisch zwischen den Typen java.lang.Integer, java.lang.Long und java.math.BigInteger gewechselt wird. Möchte man dieses Verhalten (und die damit verbundene Laufzeit) nicht nutzen, so kann auch der primitive Javatyp int verwendet werden.

Zusätzlich zu den Fließkommazahlen bietet Clojure noch Rationale Zahlen. Diese werden automatisch gekürzt und können in allen Rechnungen verwendet werden. Werden jedoch Fließkommazahlen verwendet, so ist das Ergebnis auch eine Fließkommazahl.

;java.lang.String
"String"

;java.lang.Character
\c

;java.lang.Integer ... java.lang.Long ... java.math.BigInteger
1000

;clojure.lang.Ratio
1/2

(* 1000 1/2)
=> 500  ;type: java.lang.Integer

;java.lang.Double
0.5

(* 1000 0.5)
=> 500.0  ;type: java.lang.Double

In Clojure gibt es weiterhin den Wert nil. Dieser hat in etwa die Bedeutung von null in Java, steht also für nichts. Dies ist ein Unterschied zu Lisp, wo nil auch die Bedeutung von false hat. Clojure nutzt den boolean Typen von Java, dieser wird jedoch etwas aufgeweicht, denn alles was nicht false oder nil ist, wird als true gewertet.

Neben diesen Typen gibt es noch Keywords, welche auf sich selbst auswerten und zusätzlich Funktionen sind. Als Funktion erwarten sie ein Argument (eine Tabelle) und liefern den zugeordneten Wert aus der Tabelle. Mit Keywords können weiterhin Vererbungsbeziehungen aufgebaut werden. Sie dienen häufig als Typen (s. Multimethoden), so wie in Java die statische Klassenhierarchie.

:keyword
=> :keyword

(:k (hash-map :k "value"))
=> "value"

Zusammengesetzte Typen

Gemeinsame Eigenschaften

Alle Clojure Datenstrukturen sind unveränderlich. Dies hat den Vorteil, dass keine Probleme bei nebenläufigem Zugriff entstehen können. Lesende Operationen sind immer möglich, verändernde (also schreibende) gibt es nicht. Wird ein Wert in eine Struktur eingefügt, so wird in Wirklichkeit eine neue Struktur erzeugt, in welcher alle alten Werte, sowie der neue, enthalten sind.

Die Datenstrukturen sind ebenfalls persistent, was bedeutet, dass bei diesen Erzeugungsoperationen immer möglichst viel aus der alten Struktur wiederverwendet wird, wodurch die O-Eigenschaften weitesgehend so gelten, wie man sie erwartet. Der Schreibzugriff auf einen Vektor wird z.B. konstant erwartet, würde die Struktur komplett neu erzeugt werden, so würde ein lineares Laufzeitverhalten eintreten.

Weiterhin gemein ist ihnen, dass sie die Schnittstelle ISeq implementieren und darüber auf ihre Elemente zugegriffen werden kann. Das Auslesen geschieht mittels first und next (oder auch get), das Einfügen mittels conj.

Für alle Strukturen bis auf die Liste werden Bit-partitionierte Hash-Bäume verwendet, bei denen jeweils ein 32Bit Hash über den Schlüssel (oder bei Mengen den Wert) gebildet wird. Dieser wird in 5Bit Blöcke aufgeteilt. Jeder dieser Blöcke beschreibt eine Ebene im Baum. Dadurch wird ein Zugriff auf die Elemente in schlechtestenfalls O(6), also konstanter Zeit erreicht.

Bit-Partitionierte Hash-Bäume

Listen

Listen sind die wichtigsten Strukturen eines Lisp-Programms und bilden auch in Clojure die Grundlage des Programms. Es handelt sich um einfach verkettete Listen.

Das Einfügen geschieht am Anfang der Liste, wodurch die gesamte restliche Listenstruktur übernommen wird.

Vektoren

Mit Vektoren wird das Laufzeitverhalten von Arrays angestrebt, also ein konstanter Zugriff sowohl beim Schreiben als auch beim Lesen an beliebiger Position. Die Positionen werden dabei durch ganzzahlige Werte ab 0 beschrieben.

Die Funktion vector erzeugt einen neuen Vektor, bestehende Sequenzen oder Collections lassen sich mit vec in einen Vektor überführen.

Eingefügt wird am Ende des Vektors.

Tabellen

Tabellen assoziieren einen Schlüssel mit einem Wert. Beim Einfügen muss daher immer ein Schlüssel-Wert-Paar angegeben werden. Die first Operation liefert ein solches Paar, während get nur den assoziierten Wert, oder nil liefert.

Für Tabellen ist eine Funktion assoc definiert, welche eine neue Assoziation einfügt. Neue Tabellen werden mit der Funktion hash-map oder dem Reader-Makro {} erzeugt.

Mengen

Clojure-Mengen haben die typischen Eigenschaften mathematischer Mengen. Relationale Operationen auf Mengen sind in clojure.set verfügbar. Unter anderem werden Vereinigungs-, Differenz- und Schnittmengen angeboten.

Eine Menge wird mittels der Funktion hash-set oder des Reader-Makros #{} erzeugt. Um bestehende Sequenzen oder Collections in eine Menge zu wandeln steht die Funktion set zur Verfügung.

Ein einfaches Beispiel zum Prüfen, ob in einer Eingabe nur gültige Zeichen enthalten sind:

(def *allowed-chars*
  (set "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-+*/=."))

(defn valid-word? [s]
  "checks validity of input against allowed-chars"
  (let [coll (seq s)]
    (every? *allowed-chars* coll)))

Zunächst wird das Symbol *allowed-chars* auf ein Var gebunden, welches auf eine Menge verweist, die alle erlaubten Buchstaben enthält. Die Funktion valid-word? bindet das Symbol coll neu. Es enthält eine Sequenz von Buchstaben, welche in der Zeichenkette s enthalten waren.
every? prüft anschließend, ob eine Prädikat (die *allowed-chars* Funktion), nacheinander angewendet auf alle Elemente der Sequenz, immer gilt.

Hier wird deutlich, das Mengen auch Funktionen sind, welche ermitteln, ob ein Element in der Menge selbst vorhanden ist.