Spezifikation von Clojure-Datentypen mit clojure.spec
Clojure ist eine dynamische Programmiersprache, was unter anderem
bedeutet, dass auch die Datentypen erst dynamisch zur Laufzeit
feststehen. Clojure kennt zwar sogenannte
type hints,
die sollen aber dem Compiler helfen, effizienteren Code zu generien
und sind nicht dafür da, Typeigenschaften von Datenstrukturen zu
erzwingen. Die
clojure-spec
-Bibliothek füllt diese
Lücke: Damit ist es möglich, die Struktur der Daten als Programmcode
zu spezifizieren und so die zur Laufzeit vorhandenen Daten zu
validieren. Das führt zu weniger unerwartetem Verhalten während der Programmlaufzeit
und im Fehlerfall zu besseren Fehlermeldungen. Zusätzlich ist sogar
möglich, Daten aus der Spezifikation zu generieren, zum Beispiel für
randomisiertes Testen.
Dieser Artikel gibt eine Einführung in clojure.spec
.
Mitmachen
Über die Programmiersprache Clojure haben wir in diesem Blog bereits viel geschrieben, zum Beispiel finden Sie den ersten Teil einer mehrteiligen Einführung hier.
Um die Beispiele in diesem Artikel nachzuvollziehen, reicht eine
Clojure-REPL mit einer Clojure-Version, in der clojure.spec
bereits
enthalten ist, also 1.9.0-alpha10
oder neuer. Das kriegen Sie mit
einer neuen Version der Clojure-IDE
Nightcode oder mit
Leiningen und einer Datei project.clj
mit
folgendem Inhalt:
Dann können Sie aus dem Verzeichnis, in dem die project.clj
-Datei
liegt, die Clojure-REPL mit lein repl
starten.
Um mit clojure.spec
loszulegen, müssen Sie die Bibliothek einbinden:
Spezifikationen
Jede Spezifikation beschreibt die Menge von erlaubten Werten. Eine
Spezifikation ist zum Beispiel das Prädikat odd?
, das bestimmt, ob
ein Wert ungerade ist. Ob ein Wert auf eine Spezifikation passt, sagt
s/valid?
:
s/valid?
akzeptiert eine Spezifikation und einen Wert. s/valid?
gibt zurück, ob der Wert mit der Spezifikation übereinstimmt.
Als Spezifikationen können alle Funktionen verwendet werden, die einen Wahrheitswert zurückgeben, insbesondere alle Konstrukte, die es bereits in Clojure gibt, wie zum Beispiel eingebaute Prädikate, anonyme Funktionen, Funktionalität aus importierten Java-Bibliotheken und Mengenprädikate:
Der Ausdruck #(> % 5)
im vorigen Beispiel ist eine kürzere Notation
für anonyme Funktionen, hier mit einem Parameter, der mit %
referenziert wird. #{...}
ist eine Notation für Mengen.
Namen geben
Spezifikationen können mit s/def
auch an globale Namen gebunden
werden, um sie einfach wiederverwenden zu können und auch um bessere
Fehlermeldungen zu erhalten. Um konfliktfreie Spezifikationen über
mehrere Bibliotheken und Namespaces zu verwenden, sollten die Namen
der Spezifikation namespaced keywords sein, also qualifizierte
Schlüsselwörter mit Namespace-Präfix. In Clojure erzeugt man
qualifizierte Schlüsselworter mit zwei Doppelpunkten:
Das obige Beispiel bindet die Spezifikation in Form eines
Mengenprädikats an das qualifizierte Schlüsselwort ::day-of-week
.
Die benannte Spezifikation kann dann so benutzt werden:
Zusammengesetzte Daten validieren
Auch für zusammengesetzte Datentypen können wir Spezifikationen schreiben, hier am Beispiel mit Gürteltieren und Papageien aus einem früheren Blogeintrag:
Ein Gürteltier hat ein Gewicht und ist lebendig oder tod, als Spezifikation wie folgt ausgedrückt:
Um Records zu spezifizieren, nutzen wir, dass Spezifikationen
komponierbar sind, sie können also nach belieben zusammengesetzt
werden. Ein Record ist gültig, wenn der Typ des Records stimmt und
wenn die Bestandteile des Records den entsprechenden Spezifikationen
entsprechen. Da beide Vorgaben gelten müssen, verknüpfen wir beide
Bedingungen mit s/and
. Die erste Bedingung überprüfen wir mit einer
anonymen Funktion, die die eingebaute Funktion instance?
nutzt. Für
die zweite Bedingungen nutzen wir aus, dass Clojure-Records auch als
Map funktionieren (siehe unsere
Einführung in Datentypen).
Die obige Spezifikation für Gürteltiere ist also tatsächlich eine
Spezifikation für eine Map, in der die Schlüsselwörter ::weight
und
::alive?
als unqualifizierte Schlüssel enthalten sein müssen. Das
Argument :req-un
für s/keys
legt fest, dass die Spezifikation
unqualifizerte Schlüssel akzeptieren soll, da Clojure Records mit
unqualifizierten Schlüsselwörtern implementiert (Gegenstück wäre das
Argument :req
, das qualifizerte Schlüssel erwartet, die von
clojure.spec
grundsätzlich empfohlen werden, um Namenskonflikte zu
vermeiden — die Clojure-Records passen aber nicht dazu).
Damit können wir Gürteltiere validieren:
Da die Datenstrukturen jetzt schon komplizierter sind, ist es auf den
ersten Blick gar nicht mehr so einfach, herauszufinden, warum die
letzte Validierung fehl schlägt. clojure.spec
kann uns bei der
Fehlersuche helfen:
Die Funktion s/explain
liefert eine hilfreiche Fehlerbeschreibung, aus
der sofort klar wird, dass :male
keine Zahl ist, die an der Stelle
eigentlich erwartet wird.
Gemischte Daten validieren
Wir können auch gemischte Daten spezifizeren, indem wir die Spezifikationen anders zusammensetzen. Um Tiere zu repräsentieren, die entweder ein Gürteltier oder ein Papagei sind, liefern wir zunächst die Spezifikation für Papageien nach, die ein Gewicht haben und einen Satz sagen können:
Um die Spezifikation für Tiere zu erhalten, können wir die
Spezifikationen von Gürteltieren und Papageien mit s/or
verknüpfen:
Die s/or
-Spezifikation erfordert eine Auswahl beim validieren. Jede
Möglichkeit wird mit einem Namen versehen um den Zweigen Namen zu
geben, hier :dillo
und :parrot
, damit im Fehlerfall die
Fehlerbeschreibung zielgerichtet helfen kann.
Der letze Aufruf erläutert, dass der Wert kein Tier ist, weil es kein Gürteltier ist und auch kein gültiger Papagei, da der Wert des Satzes keine Zeichenkette ist, wie in der Spezifikation gefordert. Diese detaillierten Fehlerbeschreibungen helfen weiter.
Nutzung zur Laufzeit
Wir haben inzwischen gesehen, wie Hilfreich clojure.spec
beim
interaktiven Entwickeln und Debuggen von Programmen sein kann.
Sinnvoll ist aber auch, die Fähigkeiten von clojure.spec
zur
Laufzeit des Programms zu nutzen, um zur Laufzeit die Gültigkeit der
Daten zu überprüfen. Dazu kann man zum Beispiel Prädikate mit Hilfe
des bereits bekannten s/valid?
implementieren:
Eine weitere Möglichkeit ist, mit s/assert
sicherzustellen, dass ein
Wert eine Spezifikation erfüllt. s/assert
akzeptiert wie s/valid?
eine Spezifikation und einen Wert. Passt die Spezifikation, gibt
s/assert
den Wert zurück, ansonsten wirft es eine Exception:
Das Überprüfen der Assertions ist standardmäßig deaktiviert, die
Funktion s/check-asserts
kann Assertions aktivieren.
Noch viel mehr…
In diesem Artikel haben wir nur einen kleinen Teil der Fähigkeiten von
clojure.spec
abgedeckt. clojure.spec
kann noch viel mehr: mit
Hilfe von Spezifikationen Eingaben parsen und destrukturieren,
Collections und Funktionen spezifizieren; die Code-Dokumentation
erleichtern und automatische Tests erzeugen. Mehr davon in einem
späteren Artikel.