Dieses Posting setzt unsere Clojure-Einführung (hier Teil 1,
Teil 2) fort. Dieses Mal geht es um
Fallunterscheidungen.
Zur Erinnerung: In Teil 1 steht, wie Sie eine
einfache IDE installieren und betreiben können.
Nehmen wir, an, wir wollten für Verkehrssünder aus dem
Flensburg-Punktestand berechnen, welche
Maßnahmen daraus
folgen. Dazu schreiben wir das Gerüst einer Funktion:
(defn flensburg-massnahme
"Maßnahme für eine gegebene Flensburg-Punktezahl berechnen."
[p]
...)
Hier wird gleich ein weiteres Clojure-Feature deutlich, nämlich die
Möglichkeit, einen Docstring für die Funktion mit einer kurzen
Beschreibung bereitzustellen. Nun müssen wir irgendwie zwischen den
vier verschiedenen Stufen des „Punkte-Tachos“ unterscheiden, der so
aussieht:
| 1 bis 3 Punkte | Vormerkung |
| 4 bis 5 Punkte | Ermahnung |
| 6 bis 7 Punkt | Verwarnung |
| 8 Punkte | Entziehung der Fahrerlaubnis |
Die Tests dafür sehen so aus:
(<= 1 p 3)
(<= 4 p 5)
(<= 6 p 7)
(>= p 8)
Nun müssen wir diese Tests benutzen, um eine Verzweigung
vorzunehmen, also abhängig davon, welcher Test zutrifft, die
entsprechende Maßnahme zuordnen. In Clojure geht das mit
cond
(für „Conditional“) und sieht so aus:
(defn flensburg-massnahme
"Maßnahme für eine gegebene Flensburg-Punktezahl berechnen."
[p]
(cond
(<= 1 p 3) ...
(<= 4 p 5) ...
(<= 6 p 7) ...
(>= p 8) ...))
In einer cond-Form befinden sich abwechselnd Tests und Ausdrücke.
(Die Kombination von Test und Ausdruck heißt Zweig.)
Bei der Auswertung des cond werden nacheinander alle Tests
ausgewertet, bis einer zutrifft - in dem Fall wird dann der
zugehörige Ausdruck zum Wert der cond-Form gemacht.
Es fehlen im Beispiel noch die Ausdrücke, welche die jeweiligen
Maßnahmen liefern. Die verschiedenen Maßnahmen bilden eine
Aufzählung, also eine feste Menge von Möglichkeiten. In Clojure
kommen für Aufzählungen Keywords zum Einsatz. Das sieht dann so aus:
(defn flensburg-massnahme
"Maßnahme für eine gegebene Flensburg-Punktezahl berechnen."
[p]
(cond
(<= 1 p 3) :vormerkung
(<= 4 p 5) :ermahnung
(<= 6 p 7) :verwarnung
(>= p 8) :entziehung))
(Wer Scheme oder einen anderen Lisp-Dialekt kennt, muss sich merken,
dass in Clojure nicht jeweils ein Klammernpaar um jeden
Zweig steht.)
Aufpassen muss man, wenn es möglich ist, dass keiner der Tests
zutrifft. Zum Beispiel:
(flensburg-massnahme 0) => nil
Um dies zu verhindern, können wir dem cond einen Zweig hinzufügen,
der immer zutrifft, wenn alle anderen Tests fehlschlagen.
Prinzipiell ist es möglich, einfach true als Test zu verwenden:
(defn flensburg-massnahme
"Maßnahme für eine gegebene Flensburg-Punktezahl berechnen."
[p]
(cond
(<= 1 p 3) :vormerkung
(<= 4 p 5) :ermahnung
(<= 6 p 7) :verwarnung
(>= p 8) :entziehung
true :nichts))
In Clojure ist allerdings Konvention, stattdessen das Keyword :else
zu verwenden:
(defn flensburg-massnahme
"Maßnahme für eine gegebene Flensburg-Punktezahl berechnen."
[p]
(cond
(<= 1 p 3) :vormerkung
(<= 4 p 5) :ermahnung
(<= 6 p 7) :verwarnung
(>= p 8) :entziehung
:else :nichts))
Das funktioniert deshalb, weil in Clojure jeder Wert, der nicht
false oder nil ist, als „logisch wahr“ gilt.
Oft ist eine Verzweigung binär, hat also nur einen „richtigen“
Test und einen :else-Zweig, wie etwa diese Funktion:
(defn absolute
"Absolutbetrag berechnen."
[n]
(cond
(< n 0) (- n)
:else n))
Binäre Verzweigungen können wir etwas kompakter mit
if
schreiben:
(defn absolute
"Absolutbetrag berechnen."
[n]
(if (< n 0)
(- n)
n))
Der Umstand, dass alles außer false und nil als „wahr“ gilt,
machen sich Clojure-Programme oft zunutze, zum Beispiel beim Zugriff
auf Maps. Nehmen wir an, eine Funktion solle eine Adresse in einer Map
nachschlagen und, falls diese nicht vorhanden ist, :unbekannt zurückliefern:
(def adressbuch
{"Mike Sperber" "Hornbergstraße 49, 70794 Filderstadt"
...
})
(defn adresse
"Adresse nachschauen."
[name]
(if-let [a (get adressbuch name)]
a
:unbekannt))
Die
if-let-Form
akzeptiert eine Bindung der Form [<name> <exp>] als ersten
Operand, wobei <name> ein Name und <exp> ein Ausdruck ist. Wenn
der Ausdruck einen wahren Wert liefert, wird er an <name> gebunden
und der erste Zweig der if-let-Form gebunden - ansonsten der zweite
Zweig.
Auch dieses Beispiel können wir noch kürzer schreiben, weil
or
nicht einfach true liefert, wenn ein Operand „wahr“ ergibt, sondern
stattdessen den Wert des ersten Operanden, der „wahr“ ergibt:
(defn adresse
"Adresse nachschauen."
[name]
(or (get adressbuch name)
:unbekannt))
Soweit so gut für heute!
Weiter geht‘s mit zusammengesetzten Daten in Teil 4.