5  Protocols und Datatypes

Die Einführung der Sprache Clojure in diesem Buch beginnt mit dem Satz: „Clojure ist eine Programmiersprache, deren Sprachkern in Java implementiert ist.“ Gerade die Lisp-Familie der Programmiersprachen hat aber eine stolze Tradition von self-hosted Implementationen. In diesen Implementationen ist die Sprache in sich selbst geschrieben: Lisp in Lisp. In ähnlicher Weise strebt auch Clojure eine Implementation in Clojure an. Die Clojure-Gemeinde spricht in diesem Zusammenhang von „Clojure in Clojure“, mittlerweile häufig verkürzt zu „CinC“ oder auch „cinc“.

Warum Clojure in Clojure?

Dieses Ziel ist jedoch kein Selbstzweck; es braucht eine gute Begründung. Warum sollte Clojure in Clojure geschrieben werden? Inwiefern unterscheidet sich Clojure von den zahlreichen guten Bibliotheken, die in Java entwickelt werden?

Im Gegensatz zu diesen Bibliotheken ist Clojure vor allem eine Programmiersprache. Die Entwicklung einer neuen Programmiersprache entsteht meist aus der Unzufriedenheit mit den vorhandenen Sprachen. Clojure strebt also an, auch eine bessere Programmiersprache als Java zu sein. Die Zielplattform ist die Java Virtual Machine, doch der Weg dorthin führt nicht über den Java-Compiler. Clojures Kombination von Eigenschaften führt zu besser wartbaren und stabileren Programmen, zumindest nach Ansicht der Entwickler von Clojure. Diese Eigenschaften würden einer Implementation in Clojure zugute kommen. CinC würde auch die Möglichkeit der Unterstützung weiterer Host-Plattformen – etwa Microsofts CLR oder Perls Parrot – mit verringertem Aufwand eröffnen. Zu guter Letzt ist der Compilerbau eine klassische Anwendung für funktionale Programmierung.

Interfaces?

Als Version 1.1 von Clojure aktuell war, fragten sich die Entwickler von Clojure, was nötig sei, um dieses Ziel zu erreichen. Die Möglichkeit der Implementation von Java-Klassen in Clojure – wie in Abschnitt 4.2.2 beschrieben – reichte ihnen nicht aus. Für Clojure und Rich Hickey sind Abstraktionen von großer Bedeutung. Clojure sollte es erlauben, eigene Abstraktionen zu definieren. Vererbung, potenziell sogar mit schon vorhandenen Basisimplementationen, gehört dabei aus Sicht der Entwickler von Clojure nicht zu den sauberen Konzepten. Javas Interfaces sind einer erstrebenswerten Lösung näher, lösen aber vor allem das Expression Problem nicht, auf das im folgenden Abschnitt näher eingegangen wird. Offensichtlich brauchte Clojure in dieser Hinsicht etwas Neues.

Klassen?

Parallel zu Abstraktionen, deren Aufgabe es ist, Verhalten zu beschreiben, existieren Typen, deren Aufgabe es ist, Daten zu halten. Sobald Abstraktionen in Clojure definiert werden, besteht die Notwendigkeit, dazu passende Konkretisierungen programmieren zu können. Für diese Anforderung sind die Entwickler von Clojure ebenfalls zu dem Schluss gekommen, dass das Vorhandene – das abstrakte Konzept der Klassen – nicht ihrer Vorstellung entspricht und sie eine eigene Lösung wünschen. Klassen haben aber den großen Vorteil, von der Plattform optimal unterstützt zu werden. Vor allem die Performance und die Möglichkeiten der Optimierung durch HotSpot sind erstrebenswert. Clojure muss unter der Haube auf das Klassensystem der Host-Plattform aufsetzen; für seine Anwender kann es aber sich davon unterscheidende Konzepte anbieten. Auf diese Weise kann die auf der Plattform größtmögliche Geschwindigkeit erreicht werden. Eine hohe Verarbeitungsgeschwindigkeit ist bei Kernfunktionen einer Programmiersprache absolut notwendig.

Für Anwender

Die notwendigen Erweiterungen an der Sprache kommen nicht nur den Entwicklern von Clojure zugute, sondern auch denjenigen, die Clojure einsetzen. Sie stehen als eine weitere Ausdrucksmöglichkeit bei der Programmierung zur Verfügung.
 5.1  Expression Problem
 5.2  Abstraktionen: Protocols
 5.3  Konkretisierungen: Datatypes
  5.3.1  Types
  5.3.2  Records
  5.3.3  Anonyme Typen
 5.4  Fazit

5.1  Expression Problem

Im Jahre 1998 gab Philip Wadler, der unter anderem für seine Arbeiten an Haskell und XQuery bekannt ist, einem damals schon bekannten Problem einen neuen Namen, der bis heute Bestand hat: das Expression Problem [74]. Dieser Name ist sowohl eine Anspielung auf fehlende Ausdrucksmöglichkeiten als auch auf Probleme mit den Ausdrücken – im Sinne von Anweisungen, die einen Rückgabewert haben – von Programmiersprachen. Inhaltlich handelt es sich dabei aber eher um ein Erweiterungsproblem.

Erweitern von Bibliotheken

Gute Bibliotheken sind in Abstraktionen und Konkretisierungen geschrieben. Java-Programmierer können diese Begriffe gedanklich durch Klassen und Interfaces ersetzen. Eine Bibliothek definiert einige abstrakte Konzepte sowie verschiedene Datentypen, die diese Konzepte umsetzen. Wenn jetzt ein Entwickler eine solche Bibliothek verwenden möchte, wird er sich häufig in der Situation wiederfinden, entweder die Abstraktionen der Bibliothek in eigenen Typen unterstützen zu müssen oder aber die Datentypen der Bibliothek in eigene Abstraktionen einzubetten. Dabei gibt es aber häufig die Einschränkung, dass die Bibliothek selbst nicht verändert werden darf.

Beide Erweiterungen

Beide Ziele gleichzeitig zu erreichen, stellt häufig ein Problem dar. Tendenziell haben objektorientierte Programmiersprachen ein Problem mit der Unterstützung neuer Abstraktionen, während funktionale Sprachen eher dazu neigen, bei der Einführung neuer Datentypen auf Schwierigkeiten zu stoßen.

Beispiel

Verständlich wird das Problem an einem simplen Beispiel. Eine Bibliothek stelle Funktionen für die Verwaltung von Farben bereit. Als Abstraktion definiert sie die Ermittlung des RGB-Werts einer Farbe, als Konkretisierung enthält die sparsame Bibliothek lediglich Rot:

  Package: FarbenLib
  
  Abstraktion: RGBWert
  Konkretisierung: Rot

Kompositum

Der weitere Verlauf dieses Beispiels hängt davon ab, wie die Bibliothek geschrieben ist. Wenn sie dem Entwurfsmuster Kompositum folgt, wird die Konkretisierung eine Methode zur Implementation der Abstraktion „RGBWert“ enthalten:

  Package: FarbenLib
  
  Konkretisierung: Rot
    Methode: getRGBWert: return F,0,0

Für ein Programm, das diese Bibliothek verwenden möchte, ist es nun leicht, eine neue Klasse hinzuzufügen: Es muss lediglich darauf geachtet werden, ebenfalls die notwendige Methode bereitzustellen:

  Package: MeinProgramm
  
  Konkretisierung: Blau
    Methode: getRGBWert: return 0,0,F

Diese Konkretisierung kann in einem anderen Teil des Quelltexts, in einem anderen Paket erfolgen, was hier durch ein anderes Package angedeutet wird; die Bibliothek muss dafür nicht angepasst werden. Allerdings führt die Einführung einer neuen Abstraktion zu einem Problem: Die Konkretisierungen aus der Bibliothek unterstützen sie nicht. Alle Konkretisierungen – auch die der Bibliothek – müssten angepasst werden.

Besucher

Umgekehrt verhält es sich bei Verwendung des Entwurfsmusters Besucher. Hier muss die Bibliothek zuvor etwas mehr Infrastruktur zur Verfügung stellen. Alle Konkretisierungen verfügen über eine Methode, die einen Besucher akzeptiert und dessen Methode „visit“ aufruft. Somit findet eine Verzweigung in den Besucher statt. Dieser kann nun anhand des Typs der Konkretisierung entscheiden, was zu tun ist. Das kann implizit durch Überladen oder explizit durch Typprüfungen geschehen. In der Bibliothek findet sich also etwa folgende Situation:
  Package: FarbenLib
  
  Konkretisierung: Rot
    Methode: accept(besucher):
      besucher.visit(this)
  
  Konkretisierung: RGBWertBesucher
    Methode: visit:
      Verzweige auf Konkretisierung
        Wenn Rot:  return F,0,0

Das Hinzufügen einer neuen Methode ist in diesem Falle einfach: Es muss lediglich ein neuer Besucher geschrieben werden, der dann die notwendigen Aktionen auslöst. Beispielsweise könnte die Assoziation, die eine Farbe hervorruft, gefragt sein. Das Programm kann dann folgenden Besucher schreiben:

  Package: MeinProgramm
  
  Konkretisierung: AssoziationBesucher
    Methode: visit:
      Verzweige auf Konkretisierung
        Wenn Rot:  return "Gefahr"

In diesem Falle ist jedoch die Unterstützung neuer Datentypen aufwendig. Wenn das Programm eine neue Farbe verwenden soll, müssen auch die Besucher, die die Bibliothek enthält, angepasst werden, da sie den neuen Typen nicht kennen.

Lösungen

Mats Torgersen beschreibt in seinem Paper „The Expression Problem Revisited“ [70] sowohl das Problem als auch einige mögliche Lösungen aus dem Java- und C#-Umfeld. Clojure hingegen ist auch ein Nachkomme von Common Lisp, und dort wurde bereits eine mögliche Lösung geschaffen. Dazu wurden die Methoden gewissermaßen aus den Klassen geholt und an ihrer Stelle generische Funktionen eingeführt. Diese erlauben jederzeit neue Implementationen je nach Konkretisierung und Abstraktion. Auch Clojure verfügt über diese Möglichkeit mit den Mehrfachmethoden, die in Abschnitt 2.12.4 beschrieben wurden. Diese haben allerdings das Problem, dass sie nicht so schnell sind, wie die Plattform es erlauben würde.

5.2  Abstraktionen: Protocols

Die Definition einer Abstraktion umfasst im einfachsten Falle eine Menge von Funktionen. Sowohl die einzelnen Funktionen als auch ihre Gesamtheit benötigen ferner einen Namen und sollen dokumentierbar sein. Das leisten Protocols in Clojure. Wir verwenden die englische Bezeichnung „Protocols“ als Fachterminus im Zusammenhang mit Clojure. Im Gegensatz zu etwa Interfaces in Java sind sie aber ausdrücklich nicht Bestandteil einer hierarchischen Ordnung.

Definition

Protocols werden mit dem Befehl defprotocol angelegt. Dessen formale Syntax lautet:

  (defprotocol name docstring?
    (function-name [args*]? function-docstring?)*)

Somit lässt sich beispielhaft eine einfache Abstraktion definieren, die verschiedene Zeichen zählen kann:

  (defprotocol ZeichenZaehler
    "Zaehle verschiedene Zeichen"
    (zaehle-leerzeichen [this] "Zaehle Leerzeichen")
    (zaehle-ziffern [this] "Zaehle Ziffern 0-9"))

Auf den Namen folgt der Docstring des Protocol. Alle weiteren Ausdrücke bestehen aus Deklarationen von Methoden. Auch diese können einen Docstring enthalten. Die Argumentenliste deutet durch die Verwendung von „this“ bereits an, dass den Methoden ein geeignetes Objekt übergeben wird.

Vars

Nach diesem Schritt sind im aktuellen Namensraum drei neue Vars zu finden. Es ist wichtig zu beachten, dass die Funktionen eines Protocol zwar vollständig mit Namespace qualifiziert sind, aber innerhalb eines Namespace eindeutig sein müssen. Es können also nicht zwei Protocols in einem Namensraum die gleiche Methode enthalten. Die neuen Vars sind einerseits das definierte Protocol und andererseits die dort enthaltenen zwei Funktionen. Das Protocol verrät mit Hilfe von type Details seiner Implementation als PersistentArrayMap und kann mit print an der REPL ausgegeben werden:

  user> (resolve ’ZeichenZaehler)
  #’user/ZeichenZaehler
  user> (type ZeichenZaehler)
  clojure.lang.PersistentArrayMap
  user> ZeichenZaehler
  {:on-interface user.ZeichenZaehler,
   :on user.ZeichenZaehler,
   :doc "Zaehle verschiedene Zeichen",
  ;; Ausgabe gekuerzt
  #<user$eval2320$fn__2332 user$eval2320$fn__2332@7ad3f189>
  }

Auch die Funktionen lassen sich auflösen und ihre Dokumentation wie gewohnt mit doc nachschlagen. Sie werfen aber beim Aufruf noch eine Exception, wie das folgende Beispiel zeigt:

  user> (resolve ’zaehle-leerzeichen)
  #’user/zaehle-leerzeichen
  user> (type zaehle-leerzeichen)
  user$eval2320$fn__2321$G__2311__2326
  user> (doc zaehle-leerzeichen)
  -------------------------
  user/zaehle-leerzeichen
  ([this])
    Zaehle Leerzeichen
  nil
  user> (zaehle-leerzeichen)
  java.lang.IllegalArgumentException:
    No single method: zaehle_leerzeichen of interface:
    user.ZeichenZaehler found for function: zaehle-leerzeichen
    of protocol: ZeichenZaehler

Fehlende Konkretisierung

Diesem Protocol fehlt bislang eine Konkretisierung. Der folgende Abschnitt 5.3 behandelt die Erzeugung eigener Typen, aber eine Motivation von Clojures Protocols ist es ja, das Expression Problem zu lösen. Auf der JVM bedeutet das, dass es möglich sein muss, Java-Klassen um die Unterstützung eines Protocol zu erweitern. Wäre dies nicht gegeben, so ließe sich das Expression Problem nur für in Clojure definierte Typen lösen, aber die Vielfalt in Java geschriebener Bibliotheken bliebe außen vor.

Erweitern von Typen

Die Unterstützung eines Protocol für einen Datentyp übernimmt das Makro extend-type. Es stellt einen leichten Weg dar, einen Datentyp zu erweitern, und expandiert zu einem Aufruf von extend, das hier nicht weiter beschrieben wird.
  (defn zaehle-ziffern-in-string [s]
    (count (filter (set "0123456789") s)))
  
  (extend-type
   java.lang.String
   ZeichenZaehler
   (zaehle-ziffern [s]
     (zaehle-ziffern-in-string s))
   (zaehle-leerzeichen [s]
     (count (filter #(= % \space) s))))
  
  user> (zaehle-ziffern "C3P0")
  2
  user> (zaehle-leerzeichen "Landkreis Leer")
  1

Zunächst definiert das Beispiel eine Hilfsfunktion, die die Anzahl von Ziffern in einem String ermittelt. Als Filterfunktion kommt dabei ein Set zum Einsatz, das – als Funktion verwendet – darauf prüft, ob ein Wert im Set enthalten ist. Der Aufruf von extend-type erwartet als erstes Argument den Namen des Typs, hier java.lang.String, gefolgt vom Namen des Protocol, ZeichenZaehler. Darauf werden alle Funktionen definiert, die das Protocol vorgibt. Die einzelnen Funktionsdefinitionen bestehen aus dem Namen der Funktion, einer Argumentenliste und der Implementation selbst. Sollen weitere Protocols implementiert werden, können diese im Anschluss an das im Beispiel gezeigte Protocol in gleicher Form geschrieben werden: der Name des Protocol gefolgt von den Implementationen. Die letzten beiden Aufrufe des Beispiels zeigen, dass die Funktionen des Protocol nun für Strings verfügbar sind, und entlarven einen Tippfehler, bei dem eine Null statt des Buchstabens „O“ eingegeben wurde.

Mit extend-protocol existiert ein verwandtes Makro, das es erlaubt, zu einem Protocol in einem Aufruf die Unterstützung mehrerer Typen anzugeben.

Unterstützende Typen

Ein solchermaßen erweiterter Datentyp wird im Protocol selbst vermerkt. Somit ist es dem Protocol möglich, eine Liste von unterstützenden Datentypen zu liefern. Diese Aufgabe erfüllt die Funktion extenders:
  user> (extenders ZeichenZaehler)
  (java.lang.String)

Zusammenfassung

Protocols erlauben eine einfache Definition von Schnittstellen in Form von mehreren Funktionen. Sie ähneln den Interfaces von Java, führen aber unter anderem nicht zu einer hierarchischen Ordnung der implementierenden Typen. Die erzeugten Funktionen sind anhand des Typs des Arguments überladen, was die am häufigsten verwendete Form der in Abschnitt 2.12.4 beschriebenen Mehrfachmethoden ist.

5.3  Konkretisierungen: Datatypes

Die Definition einer Abstraktion ist ohne eine konkrete Implementation wirkungslos. Der vorige Abschnitt hat gezeigt, dass Javas Typen in die Lage versetzt werden können, Clojures Protocols zu unterstützen. Damit kann ein Entwickler in Clojure neue Abstraktionen definieren und deren Unterstützung für Java-Klassen implementieren. Zur Erzeugung neuer Datentypen hat er bislang nur die Entwicklung von kompilierten Java-Klassen zur Hand. Aber dieses auf Kompilieren ausgerichtete Entwicklungsmodell ist nicht so dynamisch, wie es ein Clojure-Entwickler erwartet.

Datatypes

Um diese Lücke zu füllen, treten drei verschiedene Mechanismen an, die in diesem Abschnitt beschrieben werden: Types, Records sowie ad hoc erzeugte, anonyme Klassen. Wir verwenden das englische „Datatypes“ als Sammelbegriff für diese drei Varianten sowie die englischen Formen „Type“ und „Record“ in ihrer speziellen Bedeutung in dem hier geschilderten Zusammenhang. Die deutsche Form „Datentyp“ steht nach wie vor für die Gesamtheit aller Typen, also auch für Maps, ganze Zahlen und andere. Das Ziel ist in jedem Falle ein von der Host-Plattform ideal unterstützter Datentyp, der ein oder mehrere Protocols implementieren kann.

5.3.1  Types

In seiner einfachsten Form ist ein Datatype eine Kollektion von Werten, die beim Erzeugen angegeben werden können; in der Datenmodellierung oft als „Entität“ bezeichnet. Diese Werte stehen wie gewöhnliche Felder zur Verfügung.

Definition

Die Definition eines solchen Types erfolgt mit Hilfe des Makros deftype, das hier zunächst in der Form

  ;; noch nicht vollstaendig
  (deftype name [felder*])

zur Anwendung kommt. Nach der Definition kann eine Instanz eines solchen Types angelegt werden, indem der Name wie der Name einer Java-Klasse verwendet wird. Es steht genau ein Konstruktor zur Verfügung, der die definierten Felder in der angegebenen Reihenfolge akzeptiert. Das folgende Beispiel zeigt die Definition und Erzeugung eines Types zusammen mit dem Zugriff auf die Felder.

  user> (deftype Planet [name position])
  user.Planet
  user> (def venus (Planet. "Venus" 2))
  #’user/venus
  user> (.name venus)
  "Venus"
  user> (.position venus)
  2

Wie gewohnt sind die Feldinhalte unveränderlich. Lediglich am Rande sei bemerkt, dass es auch möglich ist, veränderliche Felder anzulegen. Davon ist jedoch dringend abzuraten, wie auch die Dokumentation von deftype erklärt. Daher wird dieser Fall hier nicht behandelt.

Unterstützung von Protocols

Ein solchermaßen definierter Type kann wie zuvor mit extend-type um die Unterstützung eines Protocol erweitert werden. Dazu definiert das folgende Beispiel zunächst ein sehr einfaches Protocol und verwendet dann extend-type für die schon bekannte Erweiterung des Types.

  user> (defprotocol Entfernung
   (sonnen-entfernung [this]))
  user> (extend-type Planet
   Entfernung
   (sonnen-entfernung [this]
     (condp = (.name this)
         "Venus" "108e6 km")))
  nil
  user> (sonnen-entfernung venus)
  "108e6 km"

Zusammengefasst

Da bei Verwendung von deftype jedoch der Entwickler direkten Zugriff auf den neuen Type hat, liegt es nahe, die Unterstützung für ein Protocol oder ein Interface von Java gleich in der Definition des Types vorzunehmen. Dazu versteht deftype auch weitere Argumente:

  (deftype name [felder*] specs*)

Die specs bestehen aus den Namen von Protocols oder Interfaces, jeweils gefolgt von den Definitionen der Methoden.

  (defprotocol FormatiereAdresse
    (fmt-email [this])
    (fmt-anschrift [this]))
  
  (deftype LoginUser [vname nname alter strasse plz stadt]
  
    FormatiereAdresse
    (fmt-email [this]
      (str "<" (.vname this) " " (.nname this) ">"
           " " (.vname this) "." (.nname this) "@"
           "clojure-buch.de"))
    (fmt-anschrift [this]
      (str (.vname this) " " (.nname this) "\n"
           (.strasse this) "\n"
           (.plz this) " " (.stadt this) "\n"))
  
    java.lang.Comparable
    (compareTo [this other]
      (compare (.alter this) (.alter other))))

Nach dieser Definition, die sowohl das Protocol FormatiereAdresse als auch das Interface java.lang.Comparable mit Implementationen versorgt, steht der Type wie eine Java-Klasse mit dem Konstruktor zur Verfügung. Die Funktionen des Protocol können mit diesem Type verwendet werden. Die Funktion compare aus Clojures Kern kann Objekte vergleichen, die java.lang.Comparable implementieren. Somit lassen sich Instanzen von LoginUser miteinander vergleichen.

  user> (def emil (LoginUser. "Emil" "Egal" 21
                              "Einsteinstr. 1" 111 "Eselei"))
  #’user/emil
  user> (fmt-email emil)
  "<Emil Egal> Emil.Egal@clojure-buch.de"
  user> (fmt-anschrift emil)
  "Emil Egal\nEinsteinstr. 1\n111 Eselei\n"
  user> (def anja (LoginUser. "Anja" "Auchegal" 28
       "An der Ampel" 8 "Achso"))
  #’user/anja
  user> (compare anja emil)
  1
  user> (fmt-email anja)
  "<Anja Auchegal> Anja.Auchegal@clojure-buch.de"

Prädikate

Mit den Funktionen satisfies? und extends? stehen zwei Mittel bereit, um zur Laufzeit zu ermitteln, ob ein Objekt oder ein Type ein Protocol oder ein Interface unterstützt:

  user> (satisfies? FormatiereAdresse emil)
  true
  user> (extends? FormatiereAdresse LoginUser)
  true
  user> (extends? FormatiereAdresse java.lang.String)
  false

Nach dieser Erweiterung von Clojure sind sowohl Clojures Anwender als auch Clojures Entwickler in der Lage, zur Laufzeit Datentypen zu erzeugen und mit ihnen die Methoden eines Protocol zu implementieren. Diese in Clojure definierten Abstraktionen und Konkretisierungen stehen in Form von Klassen und Interface auch Java-Programmierern zur Verfügung.

5.3.2  Records

Anwendungsentwickler in einer objektorientierten Programmiersprache verwenden Klassen für zwei unterschiedliche Aufgaben. Auf der einen Seite stehen die Klassen, die die Programmiersprache und ihre Standardbibliothek mitbringen. Sie sind oft genereller Natur und dienen als Grundlage eigener Klassen oder Typen in der Anwendung. Die eigenen Typen hingegen dienen der Umsetzung des Domänenmodells der Anwendung, repräsentieren also Elemente der spezifischen Aufgabenstellung.

Datentypen in Programmen

In schwach typisierten Programmiersprachen – wie auch in Clojure – ist es üblich, für solche Informationsträger Datenstrukturen wie Maps oder Vektoren zu verwenden. Das erlaubt vor allem eine schnellere Entwicklung. Die Vorteile, die sich durch eine stärkere Formalisierung der Daten ergeben, rechtfertigen oft nicht den zusätzlichen Aufwand, der durch dedizierte Klassen entsteht. Diese Vorgehensweise hat noch einen weiteren günstigen Aspekt, der gerade in der Domäne der funktionalen Programmierung zum Tragen kommt: Clojure kann Funktionen mitliefern, die auf die Datenstrukturen spezialisiert sind. Wäre jeder Informationscontainer ein separater Typ, wie es in objektorientierten Sprachen die Regel ist, kann keine Bibliothek für die Datentypen des Anwendungsprogrammierers bereitgestellt werden, da deren Zugriff bei jedem Typen unterschiedlich ausfallen kann. Beispielsweise macht sich die Funktion get-in die Eigenschaft von Maps zunutze, dass der Zugriff auf die Elemente mit Keywords gewährleistet wird. Sollte ein ähnliches Ziel in einer objektorientierten Sprache erreicht werden, so müsste eine Abstraktion – in der Regel in Interface – den Zugriff regeln. Dies würde etwa eine Methode getElementByKeyword vorschreiben, die jeder Datentyp, der mit get-in funktionieren soll, implementieren muss.

Performance, Polymorphismus

Andererseits sprechen aber auch Fakten für die Definition eigener Datentypen: Sie werden vom Host mit bestmöglicher Performance verarbeitet und erlauben Polymorphismus.

Records

Clojure erlaubt die Definition von Records, die beide Vorteile vereinen: Sie führen in der JVM zu einer eigenen Klasse und implementieren zudem das Protocol von Clojures Maps.

Eigenschaften

Records gewähren Keyword-basierten Zugriff auf ihre Elemente und sind somit für alle Funktionen geeignet, die Maps verarbeiten können. Zusätzlich erhalten Records auch Unterstützung für Metadaten. Allerdings sind sie im Gegensatz zu Maps keine Funktionen ihrer Schlüssel.

Beispiel mit Definition

Das folgende Beispiel beginnt mit der Definition eines einfachen Protocol und eines Records. Die Definition von Records unterscheidet sich nicht von der Definition von Types, es kommt lediglich das Makro defrecord zum Einsatz.

  (defprotocol TypischerSatz
    (sag-deinen-satz [this]))
  
  (defrecord BuchCharakter
    [name typ-satz autor]
  
    TypischerSatz
    (sag-deinen-satz [this]
      (println (:typ-satz this))))

Nach diesen Definitionen steht der Konstruktor für BuchCharakter zur Verfügung, und Instanzen von diesem Typ erlauben den Zugriff auf ihre Elemente mit Schlüsselwörtern. Die Instanz kann jedoch nicht als Funktion ihrer Schlüsselwörter verwendet werden, wie es bei Maps sonst der Fall ist.

  user> (def generalticktack
      (BuchCharakter.
       "General Ticktack"
       (str "Mein Name [tic] ist General Ticktack\n"
    "Und dies sind [tac] meine Kupfernen Kerle")
       "Moers"))
  #’user/generalticktack
  user> (def gaunab
      (BuchCharakter.
       "Gaunab der 99."
       "Ich lefehbe dir, mir zu chenhorge"
       "Moers"))
  #’user/gaunab
  user> (:autor gaunab)
  "Moers"
  user> (generalticktack :name)
  java.lang.ClassCastException:
    user.BuchCharakter cannot be cast to clojure.lang.IFn

Die Funktionen des Interface werden von Records unterstützt:

  user> (sag-deinen-satz gaunab)
  Ich lefehbe dir, mir zu chenhorge
  nil
  user> (sag-deinen-satz generalticktack)
  Mein Name [tic] ist General Ticktack
  Und dies sind [tac] meine Kupfernen Kerle

Mit assoc wird eine neue Instanz erzeugt, bei der einer der Werte überschrieben wird:

  user> (assoc gaunab :name "Gaunab der Letzte")
  #:user.BuchCharakter{:name "Gaunab der Letzte",
   :typ-satz "Ich lefehbe dir, mir zu chenhorge",
   :autor "Moers"}

Zerlegende Variablenbindung

Durch ihre Eigenschaften als Map haben Records den Vorteil, dass sie zerlegende Variablenbindung unterstützen. Diese Form scheint sich durchaus zu etablieren. Vorteilhaft dabei ist, dass die Funktionssignatur signalisiert, welche Elemente verwendet werden. Das folgende Beispiel demonstriert diese Verwendung anhand einer separaten Funktion.

  user> (defn print-name [{n :name}]
   (println "Name: " n))
  #’user/print-name
  user> (print-name gaunab)
  Name:  Gaunab der 99.
  nil

Da beim Aufruf einer Funktion aus einem Protocol das jeweilige Objekt immer als erstes Argument übergeben wird, kann die Definition einer Methode des Protocol gleich in der Definition der Argumente die benötigten Informationen aus dem Typ extrahieren. Diese Form kann dann auch in der Definition mit defrecord verwendet werden.

Implementierte Interfaces

Welche Interfaces aus Clojures Bereich Records genau unterstützen, lässt sich mit Hilfe von supers ermitteln:

  user> (doseq [k (supers BuchCharakter)]
   (when (re-find #"clojure" (str k))
     (println k)))
  clojure.lang.IObj
  clojure.lang.IMeta
  clojure.lang.ILookup
  clojure.lang.Counted
  clojure.lang.IKeywordLookup
  clojure.lang.Seqable
  clojure.lang.ILookupHost
  clojure.lang.IPersistentCollection
  clojure.lang.Associative
  clojure.lang.IPersistentMap

Dazu gesellen sich noch einige Schnittstellen aus dem Sprachumfang von Java:

  user> (doseq [k (supers BuchCharakter)]
   (when (re-find #"java" (str k))
     (println k)))
  java.lang.Iterable
  java.io.Serializable
  java.util.Map
  java.lang.Object

5.3.3  Anonyme Typen

Gelegentlich sieht sich ein Programmierer mit einer Situation konfrontiert, in der er einen Datatype benötigt, der ein bestimmtes Protocol implementiert, aber nur lokal relevant ist. Ähnlich wie der funktionale Programmierstil häufig anonyme Funktionen verlangt, scheint in solchen Fällen eine anonyme Ad-hoc-Definition einer Klasse, die an keiner anderen Stelle wieder benötigt wird, angebracht. Solche anonymen Typen haben auch eine gewisse Ähnlichkeit zu Proxy-Objekten, wie sie in Clojure mit proxy erzeugt werden (vgl. Abschnitt 4.2.1).

Begriffe

In Clojure bewirkt der Befehl reify die Erzeugung eines solchen dynamischen Konstrukts. In der Informatik ist im deutschen Sprachraum der Begriff „Reifikation“ oder „Reifizierung“ bekannt, er wird allerdings selten gebraucht. Die direkte Übersetzung „Vergegenständlichung“ deutet jedoch darauf hin, dass es um die Konkretisierung eines mentalen oder abstrakten Konzeptes geht.

Definition

Beispielsweise könnte eine Programmentwicklung ein Objekt verlangen, das Runnable implementiert. Würde dieses Objekt aber nur lokal und vielleicht auch nur einmal verwendet werden, wäre die explizite Definition einer eigenen Klasse für diesen Anwendungsfall unnötig aufwendig. Die Verwendung von reify erlaubt die Definition und gleichzeitige Instantiierung solcher Objekte. Der Aufruf von reify gleicht dabei dem von deftype und defrecord bis auf den fehlenden Namen.

  (reify specs+)

Das folgende Beispiel erzeugt eine Klasse und eine Instanz, die Runnable implementiert. Auf diesem Objekt kann die Methode run aufgerufen werden oder das Objekt kann an einen Thread übergeben werden, der auch für den Aufruf von run sorgt.

  (def Walker (reify Runnable
                     (run [this] (println "I’m walking."))))
  
  user> (.run Walker)
  I’m walking.
  nil
  user> (.start (Thread. Walker))
  nil
  I’m walking.

Diese Methode unterscheidet sich im Detail von proxy. Erstens beschränkt sich reify auf die Implementation von Interfaces und erlaubt nicht das Ableiten von anderen Klassen. Zweitens ist das Resultat etwas performanter, weil es von der Host-Plattform direkt unterstützt wird.

5.4  Fazit

Die Einführung von Protocols, Types, Records und Ad-hoc-Typen erfolgte erst mit Clojure 1.2. Das Konzept erscheint zu diesem Zeitpunkt schlüssig, und im Sprachkern werden diese Konstrukte auch bereits verwendet. Die Eigenschaften dieser Technologien reflektieren aktuell vor allem die Meinung von Rich Hickey zu den Themen Polymorphismus, Vererbung und Kapseln von Informationen. Diese sind in der Dokumentation auf der Webseite von Clojure [27] wiedergegeben.

Die Akzeptanz bei den Clojure-Entwicklern bleibt abzuwarten. Erste Erfahrungen deuten bereits darauf hin, dass dieses Thema erfolgreich sein könnte. Wenn es sich so entwickelt, ist davon auszugehen, dass die hier noch in einem separaten Kapitel vorgestellten Konstrukte eine zentralere Rolle spielen werden. Die meisten Verwendungen von proxy werden durch reify ersetzt, StructMaps werden weitestgehend durch Records abgelöst werden.