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 PunkteVormerkung
4 bis 5 PunkteErmahnung
6 bis 7 PunktVerwarnung
8 PunkteEntziehung 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.