Clojure unter der Lupe

Listen als Funktionsanwendung

Ohne „‚“ vor der Liste wird diese auf besondere Weise ausgewertet: Zuerst wird der erste Ausdruck in der Liste ausgewertet. Ist das Ergebnis eine Funktion, werden alle weiteren Ausdrücke der Liste ausgewertet und die Funktion mit diesen Werten als Parameter aufgerufen. Ist das Ergebnis der Auswertung des ersten Ausdrucks keine Funktion (oder Sonderform), tritt ein Fehler auf.

Dem Ausdruck in 1 + 2 * 3 in Infix-Notation entspricht in Clojure >> (+ 1 (* 2 3)) ;; => (+ 1 6) => 7. In diesem Fall ist der erste Ausdruck in der Liste das Symbol +, das standardmäßig an die Additionsfunktion gebunden ist. Dann folgen als Argumente ein konstanter Ausdruck und wieder ein Funktionsaufruf. Die Prefix-Schreibweise ist vielleicht ungewohnt, hat aber den Vorteil, dass die Operatorreihenfolge eindeutig durch die Klammerung vorgegeben ist.

Funktionen erzeugen

Eine Funktion wird mit (fn [<argument>*] <ausdruck>*) erzeugt. Jede Funktion wertet die Ausdrücke im Rumpf nacheinander aus und liefert den Wert des zuletzt ausgewerteten Ausdrucks zurück. Funktionen können mit einer variablen Argumentzahl definiert werden. Dazu ist vor den letzten Parameter der Argumentliste ein „&“ zu stellen. Der letzte Parameter wird dann an die Liste aller verbleibenden Argumente gebunden: >> ((fn [x & xs] xs) 0 1 2 3 4) ;; => (1 2 3 4).

Neben fn existiert eine kürzere (und eingeschränktere) Syntax für die Erzeugung von Funktionen. Mit #(<ausdruck>) wird eine Funktion erzeugt, die *einen* Ausdruck auswertet, bei dem die Symbole %1 … %n die impliziten Parameter der Funktion sind. Sie sollten diese Syntax nur für kurze Funktionen verwenden.

Funktionen können wie andere Werte mit def an Symbole gebunden werden: >> (def summe (fn [a b] (+ a b))) und mit defn existiert eine Kurzform, die beide Schritte vereinigt: >> (defn summe [a b] (+ a v)).

In Clojure können Funktionen wie Werte behandelt werden, d. h. sie können an Symbole gebunden (s. defn), als Parameter an Funktionen übergeben und als Ergebnis von Funktionen zurückgegeben werden (siehe fn). Das hört sich zunächst unspektakulär an, ermöglicht aber eine elegante Formulierung von Programmen, die ohne dieses Sprachmittel nur umständlich zu erreichen wären.

Der Clojure-Sprachkern im Namensraum clojure.core stellt eine große Menge von Funktionen zur Verfügung, die eine Funktion als Parameter erhalten und so in ihrer Funktionsweise angepasst werden können (so genannte „Higher Order Functions“: Funktionen höherer Ordnung). map ist ein Beispiel für eine HOF. Sie wendet eine Funktion auf alle Elemente einer Collection-Instanz an und liefert eine neue Liste mit den Ergebnissen:

>> (def zahlen1-7 (range 1 8)) ;; (range i i+n) => (i,i+1,...,i+n-1)
>> (map #(* %1 %1) zahlen1-7) ;; => (1 4 9 16 25 36 49)

map kann auch mit Funktionen angewendet werden, die mehr als ein Argument übernehmen. Für jedes Argument der Funktion f wird map eine eigene Collection übergeben. Das i-te Element des Ergebnis ist dabei die Anwendung von f auf die i-ten Elemente der Collections. Die Ergebnismenge ist nur so lang wie die kürzeste Argumentliste: >> (map list ‚(1 2 3 4 5) ‚(:a :b)) ;; => ((1 :a) (2 :b)).

Ein weiteres Beispiel für eine Higher Order Function aus clojure.core ist reduce. Es erhält als ersten Parameter eine Funktion f mit zwei Argumenten, als zweiten Parameter einen Wert r0 und dritten Parameter eine Liste von Werten e0 … en. Die Auswertung geschieht nach:

(reduce f r0 '(e0 ... en)) => (f ... (f (f r0 e0) e1) ... en)
>> (reduce conj '() '(1 2 3)) ;; => (conj (conj (conj '() 1) 2) 3) => (3 2 1)

Diese Funktion ist in anderen Sprachen auch als „left fold“ bekannt.

Namensräume

Um Namenskollisionen zu vermeiden, ist jede Definition Mitglied in einem Namensraum. Symbole können innerhalb des Namensraums, in dem sie definiert wurden, ohne den vorangestellten Namensraum benutzt werden. Definitionen aus anderen Namensräumen müssen entweder durch Angabe des Namensraums referenziert oder im eigenen Namensraum durch :refer oder :use bekanntgemacht werden. Der Unterschied von :use zu :refer ist, dass :use eventuell noch nicht geladene Namensräume aus Ressourcen im Klassenpfad von Clojure lädt:

>> (ns innoq.a)
>> (def frage "Das Leben, das Universum, alles ...")
>> (def antwort 42)
>> antwort ;; => 42
>> (ns innoq.b)
>> antwort ;; => Unable to resolve symbol: antwort in this context
>> (ns innoq.b (:refer innoq.a :only [antwort]))
>> antwort ;; => 42
>> frage ;; => Unable to resolve symbol: frage in this context. 
         ;; Durch ":only [antwort]" im :refer wurden die importierten
         ;; Elemente auf antwort beschränkt.
Datenstrukturen

Datenstrukturen sind wichtige Konstrukte einer Programmiersprache. Clojure besitzt neben der schon vorgestellten Liste weitere Datenstrukturen. Eine gemeinsame Besonderheit aller Clojure Collections ist, dass diese unveränderlich sind (sog. persistente Datenstrukturen). Operationen, die z. B. in den analogen Klassen java.util.* das Objekt ändern, erzeugen in Clojure eine neue Instanz. Clojure Collections können wegen dieser Unveränderbarkeit wie Werte behandelt werden, z. B. bei Vergleichen mit „=“. Clojure nutzt effiziente Implementierung für die persistenten Datenstrukturen, bei denen identische Teile von Strukturen gemeinsam verwendet werden.

Assoziative Strukturen

Eine assoziative Datenstruktur („Map“) verknüpft beliebige Schlüsselwerte mit beliebigen Werten. Eine „Map“ wird durch die Syntax {<schlüssel_1> <wert_1> … <schlüssel_n> <wert_n>} erzeugt.

Der Zugriff auf ein Element der Map hat die Form einer Funktionsanwendung auf einen Schlüssel, die Map übernimmt die Rolle der Funktion. Optional kann noch ein weiterer Parameter übergeben werden, der als Wert zurückgegeben wird, wenn zum Schlüssel kein Wert in der Map zugeordnet ist:

>> (def a-map {:erster 1, :zweiter 2, :dritter 3})
>> (a-map :erster) ;; => 1
>> (a-map :sonstige) ;; => nil 
>> (a-map :sonstige 100) ;; => 100

Als Beispiel für die Nutzung von Maps dient die Definition eines endlichen Zustandsautomaten. Die Schlüssel sind die Zustände das Automaten, die Werte Maps, die Ereignisse auf Zielzustände abbilden:

>> (def fsm {:StateA  {:EventX :StateB, :EventY :StateC, :EventZ :StateA}
             :StateB  {:EventX :StateA, :EventZ :StateC}
             :StateC  {:EventX :StateX, :EventY :StateA}})

Die Funktion fsm-run liefert für den Startzustand und eine beliebige Zahl von Ereignissen die eingenommenen Zustände, wobei ein ungültiger Übergang zum Zustand :ILLEGAL führt:

>> (defn fsm-run[state & events] 
     (reverse  ;; Liste mit umgekehrter Elementreihenfolge erzeugen
      (reduce (fn [history event] 
                (cons ((fsm (first history) {}) event :ÌLLEGAL)
                      history))
              (list state)
              events)))
>> (fsm-run :StateA :EventX :EventY :EventY);;=>(:StateA :StateB :ÌLLEGAL :ÌLLEGAL)

Clojure besitzt eine recht umfangreiche Sammlung von Funktionen für den Umgang mit Map. Hier zwei Beispiele:

Durch (assoc <map> <key_1> <val_1> <key_n> <val_n>) wird eine Map erzeugt, die <map> um die Schlüssel-/Wertpaare <key_i>/<val_i> erweitert bzw. schon vorhandene Assoziationen ersetzt. Wichtig: Die ursprüngliche Map wird nicht verändert, sondern eine neue zurückgegeben:

>> (assoc fsm :NewState {:EventX :StateA :EventY :StateB})
>> (fsm :NewState) ;; => nil

Die Funktion (zipmap <keys> <values>) erzeugt aus zwei Collections eine Map, die das n-te Element der <keys> mit dem n-ten Element der <values> verknüpft. Überzählige Elemente in einer Collection werden ignoriert: >> (zipmap ‚(:1ter :2ter :3ter) ‚(1 2 3 4 5)) => {:3ter 3, :2ter 2, :1ter 1}.

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.