Clojure unter der Lupe

Spiel das Lied noch einmal, Sam

Für den Fall, dass ein Schleifenkonstrukt benötigt wird, gibt es die Form (loop [<sym1> <expr1> … <symn> <exprn>] <expr>* ) loop ist ein let mit einer Erweiterung. Wie let erzeugt loop lokale Symbol-Wert-Bindungen für die im Rumpf ausgewerteten Ausdrücke. Zusätzlich kann innerhalb der Ausdrücke noch der Ausdruck (recur <val1> . <valn>) stehen, wenn dieser der letzte ausgewertete Ausdruck im loop ist. Durch recur werden die Bindungen der lokalen Symbole sym1 . symn mit den übergebenen Werten belegt und die Ausdrücke im Rumpf nochmals ausgeführt:

(defn bereich "Erzeugt eine Lister der Zahlen (0.n)"
 [n]
  (loop [i 0     ;; Zähler i mit 0 initialisieren
         r '()]  ;; Resultat r mit leerer Liste initialisieren
    (if (

Schleifen sind in imperativen Sprachen ein häufig verwendetes Konstrukt. In Clojure werden Schleifen weniger oft benötigt, da mit map, filter und ähnlichen Funktionen die gleichen Aufgaben in vielen Fällen kürzer ausgedrückt werden können. Für die Funktion bereich im Beispiel oben ist z. B. bereits die Funktion range im Sprachkern enthalten.

Die Form recur kann auch innerhalb von Funktionen angewendet werden, wenn sie der letzte ausgewertete Ausdruck in der Funktion ist. Das Verhalten ist hier ähnlich wie bei loop: Die Parameter der Funktion werden an die Werte im recur gebunden und der Funktionsrumpf ausgewertet. Als Beispiel die bekannte Fakultät:

(defn fak "fak n berechnet die Fakultät."
  ([n] (fak n 1))       ;; Definition von fak für ein Argument
  ([n r] (if (= n 0)    ;; Definition mit zweitem Argument für Teilergebnisse
           r 
           (recur (dec n) (* n r))))) ;; 

Clojure erlaubt normale Rekursion durch Aufruf der Funktion über ihren Namen. Das kann im Gegensatz zu recur an beliebigen Stellen geschehen, nicht nur als letzter ausgewerteter Ausdruck. Im Beispiel oben könnte die letzte Zeile auch als (fac (dec n) (* n r)) geschrieben werden. Der Unterschied zu recur besteht darin, dass bei Rekursionsaufruf über den Funktionsnamen der Stack verwendet wird und daher die Rekursionstiefe durch die Stack-Tiefe der VM beschränkt ist. Bei Verwendung von recur wird die Rekursion in eine Schleife ohne Verwendung des Stacks umgewandelt, die Rekursionstiefe ist daher unbeschränkt.

Veränderungen mit Atomen und Agenten

Es gibt Probleme, in denen eine einfache Lösung die Veränderung eines Werts erfordert. Für diesen Fall gibt es in Clojure „Atome“. Ein Atom ist in Clojure eine Referenz auf einen Wert, die von verschiedenen Threads geändert werden kann. Ein Atom wird mit (atom <wert>) angelegt. Der aktuelle Wert eines Atoms kann mit @<atom> abgefragt und mit (swap! <atom> <fun>) geändert werden. swap! wendet die Funktion <fun> auf den Wert von <atom> an und ersetzt den aktuellen Wert durch das Ergebnis. Die Zuweisung ist Thread-sicher, auch wenn mehrere Threads parallel auf dem gleichen Atom arbeiten, ist der Vorgang atomar:

>> (def zahl (atom 2))
>> @zahl ;; => 2
>> (swap! zahl #(* % %)) ;; => 4
>> @zahl ;; => 4 

Ein Clojure-Atom ermöglicht die synchrone Änderung eines Werts durch verschiedene Threads. Mit Clojure-Agenten existiert eine Möglichkeit, asynchrone Änderungen an einem Zustand durchzuführen. Agenten besitzen einen Zustand, der durch „senden“ einer Funktion an den Agenten verändert werden kann. Mit (agent <startwert>) wird ein Clojure-Agent erzeugt und dessen Anfangszustand mit <startwert> belegt, der aktuelle Zustand wird durch @<agent> zurückgeliefert und durch (send <agent> <funktion> <parameter>*) wird der Zustand des Agenten aktualisiert. Dabei wird <funktion> mit dem Agentenzustand als erstem Argument und den weiteren Parametern ausgewertet und das Ergebnis als neuer Zustand gesetzt. Entscheidend ist, dass der send aufrufende Thread nicht auf die Auswertung wartet. Die Funktion wird in einem eigenen Thread gestartet. Der Agent, der die Funktion auswertet, ist innerhalb der Funktion an das Symbol *agent* gebunden.

Als Beispiel zwei Agenten, die als Zustand einen Vektor mit dem Namen und einem Zähler halten. Die Funktion „ping-pong“ erhält als erstes Argument den Zustand des Agenten, dann den Agenten, an den eine Antwort gesendet werden soll und dann die verbleibende Anzahl von zu sendenden Nachrichten:

>> def a0 (agent [:a0 0]))
>> (def a1 (agent [:a1 0]))
>> (defn ping-pong [state sender n] 
      (let [[name counter] state]
        (if (> n 0)
          (do
           (println "Hello " name)
           (send sender ping-pong *agent* (dec n))
           [name (+ 1 counter)])
         state)))
>> (send a0 ping-pong a1 10) 
>> @a0 ;; => [:a0 5]
Java-Integration

Clojure wurde ausdrücklich als ein Lisp für die Java-VM konzipiert und so ist die Nutzung von Java-Klassen recht problemlos. Alle Klassen, die im Klassenpfad der Java-VM der Clojure-REPL liegen, können im Clojure-Code genutzt werden. Alle Sprachelemente von Clojure sind als Java-Klassen implementiert bzw. werden in Java-Klassen übersetzt. Viele der Clojure-Datentypen sind Standardtypen von Java. Die Java-Klasse eines Clojure-Ausdrucks kann mit der Funktion class abgefragt werden:

>> (class true) ;; => java.lang.Boolean
>> (class 1)  ;; => java.lang.Integer
>> (class "Text")  ;; => java.lang.String

Methoden von Objekten können in Clojure mit (.<methode> <object> <parameter>* aufgerufen werden, Instanzen von Objekten können mit der Form (<klasse>. <parameter>*) erzeugt werden. Statische Methoden sind durch (<klasse>/<methode> <parameter>* zugänglich:

>> (def java (String. "JAVA"))
>> (.toLowerCase java) ;; => "java"
>> (Math/sin (Math/PI)) ;; => 1.2246467991473532E-16 ;; Fast richtig 

Zum Abschluss ein Programm, das ein Schiebepuzzle mit einer Swing-GUI implementiert. Das Programm wird in einem eigenen Namensraum implementiert, in dem durch :import Java-Klassen durch ihren unqualifizierten Namen genutzt werden können:

 (ns innoq.schiebung
  [:import [javax.swing JFrame JPanel JButton]]
  [:import [java.awt GridLayout]])

Zunächst werden Konstanten und zwei Hilfsfunktionen implementiert. spalte-idx rechnet einen Index aus einem eindimensionalen Array in einen Vektor mit Zeile und Spalte um:

(def LAENGE 4)
(def ANZAHL (* LAENGE LAENGE))
(defn zeile-spalte [idx] 
     [(int (/ idx LAENGE)) (rem idx LAENGE)]) 

Die Funktion beweglich? testest, ob die Indizes idx und frei direkte Nachbarn in derselben Spalte oder Zeile sind:

(defn beweglich? [idx frei] 
  (let [[idx-zeile idx-spalte] (zeile-spalte idx)
        [frei-zeile frei-spalte] (zeile-spalte frei)]
    (or (and (=  idx-zeile frei-zeile) 
             (or (= frei-spalte (inc idx-spalte))
                 (= frei-spalte (dec idx-spalte))))
        (and (=  idx-spalte frei-spalte) 
             (or (= frei-zeile (inc idx-zeile))
                 (= frei-zeile (dec idx-zeile)))))))

Die Funktion action-listener erzeugt durch die eingebaute Clojure-Funktion proxy ein Java-Objekt, das das ActionListener-Interface implementiert. Die Werte, die action-listener übergeben werden, sind innerhalb des konstruierten Objekts verfügbar, es handelt sich um einen sog. „Funktionsabschluss“ oder „Closure“:

(defn action-listener 
 "idx ist der Feldindex des Elements, dem der ActionListener zugeordnet ist,
   frei ist ein Clojure-Atom, das den Index des freien Feldes im Puzzle hält
   buttons ist ein Vektor mit allen Buttons des Puzzles."
  [idx frei buttons] 
  (proxy [java.awt.event.ActionListener][]
    (actionPerformed [e] 
       (if (beweglich? idx @frei)  ;; @frei : der Index des freien Feldes 
         (let 
           (.setText (buttons @frei) (.getText  source))
           (.setText source "")
           (swap! frei (constantly idx))))))) 

Zusammengebaut und angezeigt wird alles in der Funktion schiebepuzzle. Im let wird ein Clojure-Atom angelegt. Dieses dient als Zeiger auf einen veränderlichen Wert. Im Schiebepuzzle wird in diesem Wert der Index des freien Felds abgelegt. Es folgen die GUI-Elemente: der Frame frame, ein Panel mit GridLayout und ein Vektor mit Buttons. Den Buttons werden dann ActionListener zugewiesen. Diese haben Zugriff auf die Position des Buttons, dessen Ereignisse sie behandeln, auf das Atom mit der Position des freien Felds sowie auf alle anderen Buttons des Puzzles:

(defn fak "fak n berechnet die Fakultät."
  ([n] (fak n 1))       ;; Definition von fak für ein Argument
  ([n r] (if (= n 0)    ;; Definition mit zweitem Argument für Teilergebnisse
           r 
           (recur (dec n) (* n r))))) ;; 

Viel Spaß beim Schieben!

Burkhard Neppert ist Senior Consultant bei der innoQ Deutschland GmbH. Er beschäftigt sich dort mit modellgetriebenen Entwicklungsmethoden und dem Entwurf von IT-Systemen.
Kommentare

Schreibe einen Kommentar

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