Die Welt der Programmiersprachen hat seit zwei Jahren eine neue Bewohnerin: Die funktionale Sprache Clojure spezialisiert sich auf Programme mit Multithreading und bietet dank Java-Verwandtschaft trotz ihres jugendlichen Alters eine umfangreiche Bibliotheken-Landschaft .
Mit Intels Core-Mikroarchitektur sind Rechner mit mehreren CPUs oder Prozessorkernen in Firmen und Privatwohnungen massenhaft angekommen. Dieser Entwicklung muss sich die aktuelle Generation von Programmierern stellen. Programme, die in mehreren Threads laufen, sind die Antwort. Derzeit entstehen Bibliotheken, die notwendige Funktionen in bekannten Programmiersprachen nachrüsten, es tauchen aber auch ganz neue Programmiersprachen auf, die diese Fähigkeit mitbringen.
Ein neuer Spieler auf dem Feld der Werkzeuge ist die Programmiersprache Clojure [1]. Die Grundzüge, die Sprachdesign und Motivation gleichermaßen beschreiben, sind schnell zusammengefasst: Concurrent Programming und die Java Virtual Machine. Concurrent Programming nennt man das Programmieren in einem Umfeld, in dem separate Programme oder Threads zusammenwirken und auf Ressourcen zugreifen, die sie sich teilen müssen. Dabei dürfen keine inkonsistenten Zustände durch die konkurrierenden Zugriffe entstehen. Clojure begegnet dem durch einen funktionalen Ansatz, spezielle Datenstrukturen und sichere Zugriffsmechanismen auf die Ressourcen.
Eine Plattform, die das Ausführen mehrerer Threads explizit unterstützt, ist die Java Virtual Machine (JVM), die es in Versionen für viele Betriebssysteme gibt. Zudem existiert bereits eine stattliche Menge an Bibliotheken in Java, die in Clojure sofort und ohne Integrationsaufwand verwendbar sind. Clojure erzeugt beim Kompilieren Java-Bytecode, sogar ».class«-Dateien lassen sich erstellen. Die JVM als Zielplattform ist kein Implementations-Detail, sondern ein Feature.
Sprache samt Zubehör
Der Erfinder von Clojure, Rich Hickey, stellt Clojure unter der Eclipse Public License 1.0 zum Download bereit. Wer Clojure unkompliziert testen möchte, lädt »clojure_1.0.0.zip« von [2] herunter, entpackt es in einem beliebigen Verzeichnis und startet zum interaktiven Programmieren die Read Eval Print Loop (siehe Kasten “REPL”). Aus Komfortgründen empfiehlt sich die Integration von Clojure in eine IDE. Lösungen für Netbeans [3], Emacs ([4], [5]), Vim [6] und Eclipse [7] sind verfügbar. Wer die aktive Entwicklung von Clojure verfolgen möchte, begibt sich zu Github [8].
Erste Experimente
Clojure startet nach der Eingabe von »java -jar clojure.jar clojure.lang.main« in einem Terminal und präsentiert dem Anwender einen Prompt, an dem er Befehle eingeben kann, die er mit der [Return]-Taste abschickt. Größere Mengen an Quellcode schreibt der Programmierer besser in einer separaten Datei, deren Inhalt er dann an der REPL mit Copy & Paste einfügt. Es gibt auch einen Befehl, um eine ganze Datei zu kompilieren und ihren Inhalt zu laden: »(load-file “file.clj”)«.
In alter Tradition ist “Hello World” das erste Beispiel:
user=> (println "Hello World") Hello World nil user=>
In diesem kurzen Codestück zeigen sich bereits wichtige Eigenschaften von Clojure. Zum einen ist das die Lisp-artige Syntax. Neueinsteigern sei versichert, dass die Klammern in der Regel nach wenigen Tagen nicht mehr stören und später unverzichtbar erscheinen. Befehle in Clojure werden in runden Klammern eingefasst, das erste Element einer Liste von Elementen in einem Klammernpaar ist der Befehl, alle folgenden Elemente sind dessen Argumente. In diesem Beispiel lautet der Name des Befehls »println«, was »System.out.println« in Java entspricht, und das einzige Argument ist der String »Hello World«. Darunter ist die Ausgabe zu sehen. In der dritten Zeile gibt Clojure »nil« aus, was »NULL« in Java entspricht, um danach wieder einen neuen Prompt anzubieten. Dabei ist »nil« der Rückgabewert des Befehlsaufrufs.
Nebenwirkungen
Wer bisher vor allem mit Sprachen wie C, Perl, Java oder Python entwickelt hat, ist gewohnt, die meisten Anweisungen wegen ihres Seiteneffekts zu verwenden. Ein »printf(“Hello”);« in C erzeugt als Seiteneffekt eine Ausgabe, hat aber auch einen Rückgabewert: die Anzahl der ausgegebenen Zeichen. In der funktionalen Programmierung interessiert nur der Rückgabewert; eine Funktion bekommt Argumente übergeben und bestimmt ausschließlich anhand dieser Argumente ihr Resultat. Das folgende Beispiel zeigt die Definition einer Funktion (»defn«) mit Namen »add«, die zwei Argumente (»x« und »y«) entgegennimmt und als Resultat deren Summe zurückgibt.
|
Listing 1: |
|---|
001 (ns de.linuxmagazin.cpu-heizung
002 (:import (java.awt Color Dimension)
003 (java.awt.image BufferedImage)
004 (javax.swing JPanel JFrame)))
005
006 (def param {:size 500
007 :max-abs-val 100
008 :max-iter 2000
009 :xmin -1.5
010 :ymin -1.0
011 :xmax 0.5
012 :ymax 1.0})
013
014 (defn scale-range [x min max steps]
015 (+ min (* x (/ (- max min) steps))))
016
017 (defn size-range []
018 (range 0 (param :size)))
019
020 (defn color-log-scale
021 "Skalierung des Iterations-Wertes."
022 [n max-n]
023 (max (* 255
024 (/ (Math/log (* n (/ 255.0 max-n)))
025 (Math/log 255)))
026 0.0))
027
028 (defn color-value
029 "Berechnung des Farbwertes."
030 [iter]
031 (int
032 (- 255
033 (color-log-scale iter (param :max-iter)))))
034
035 (defn mandelbrot-iterations
036 "Naive Implementation der Mandelbrot-Iteration.
037 Enthält Typ-Hinweise für den Compiler."
038 [x0 y0 max-abs max-iterations]
039 (let [x0 (float x0)
040 y0 (float y0)
041 max-iter (int max-iterations)]
042
043 (loop [x (float x0)
044 y (float y0)
045 iter (int 0)]
046 (if (== iter max-iter)
047 0
048 (let [x1 (+ x0 (- (* x x) (* y y)))
049 y1 (+ y0 (* (float 2) (* x y)))
050 abs (+ (* x1 x1) (* y1 y1))]
051 (if (< abs max-abs)
052 (recur x1 y1 (+ iter (int 1)))
053 iter))))))
054
055 (defn mandelbrot-line
056 "Berechne eine Zeile der Mandelbrot-Menge.
057 Achtung das Resultat ist lazy."
058 [x]
059 (map (fn [pxy]
060 (let [y (scale-range
061 pxy
062 (param :ymin)
063 (param :ymax)
064 (param :size))]
065 (mandelbrot-iterations
066 x y
067 (param :max-abs-val)
068 (param :max-iter))))
069 (size-range)))
070
071 (def world (ref (vec (repeat (param :size) []))))
072
073 (defn make-world-image []
074 (let [img (new BufferedImage
075 (inc (param :size))
076 (inc (param :size))
077 BufferedImage/TYPE_INT_ARGB)]
078 (doseq [pxy (size-range)]
079 (let [line (world pxy)]
080 (when (not (empty? line))
081 (doseq [pxx (size-range)]
082 (.setRGB img
083 pxx
084 pxy
085 (.getRGB
086 ( Color.
087 (color-value (line pxx))
088 (color-value (line pxx))
089 255)))))))
090 img))
091
092
093 (defn repaint-world-img [g]
094 (.drawImage g (make-world-image) 0 0 nil))
095
096 (def world-agent (agent {}))
097
098 (defn mandelbrot-parallel []
099 (let [frame (JFrame. "Fractal Multi-Threaded")
100 panel (proxy [JPanel] []
101 (paintComponent
102 [g]
103 (proxy-super paintComponent g)
104 (repaint-world-img g)
105 ))]
106 (dosync
107 (ref-set world
108 (vec (repeat (param :size) []))))
109
110 (.setPreferredSize
111 panel
112 (Dimension. (param :size) (param :size)))
113
114 (doto frame
115 (.add panel)
116 (.pack)
117 (.setVisible true))
118
119 ;; The Meat
120 (dorun
121 (pmap (fn [pxx]
122 (let [x (scale-range
123 pxx
124 (param :xmin)
125 (param :xmax)
126 (param :size))
127 newline (vec (mandelbrot-line x))]
128
129 (dosync
130 (alter world
131 (fn [state]
132 (assoc state pxx newline))))
133
134 (send-off world-agent
135 (fn [_]
136 (.repaint panel)))))
137 (size-range)))
138
139 panel))
user=> (defn add [x y]
(+ x y))
#'user/add
user=> (add 2 3)
5
user=> (add (add 1 2) 3)
6
|
Offensichtlich verwendet diese Funktion keine globalen Variablen bei der Berechnung, sie erzeugt keine Ausgabe während ihrer Arbeit und schreibt auch nicht in Dateien oder Datenbanken – sie ist rein funktional. Solche Seiteneffekt-freien Funktionen erzeugen auch bei parallelen Aufrufen aus mehreren Threads mit Sicherheit keine Konflikte.
Rein funktionale Sprachen werden oft mit dem Argument geschmäht, sie könnten allenfalls zum Aufheizen der CPU dienen, aber nicht bei der Bewältigung realer Aufgaben helfen. Ein Computerprogramm muss Seiteneffekte erzielen, beispielsweise Ausgaben anzeigen oder in eine Datenbank schreiben, sonst hat es für den Anwender keinen Effekt.
|
REPL |
|---|
|
Das Akronym REPL steht für Read Eval Print Loop. Es bezeichnet eine interaktive Sitzung, in der der Anwender Befehle eintippt, ähnlich wie bei einer Shell. Die Reader-Komponente nimmt diese entgegen und baut daraus interne Codestrukturen, die im Evaluations-Schritt ausgeführt werden. Das Resultat des eingegebenen Ausdrucks erscheint dann in der Sitzung (Print), und das Ganze beginnt von vorn (Loop). |
Diesen Anspruch erfüllt Clojure mit mehreren Mitteln. Zunächst interagiert es direkt mit Java. Clojure kann einfache alte Java-Objekte instanziieren und auf ihnen Methoden aufrufen. Die Syntax dafür beschränkt sich im Wesentlichen auf einen Punkt. Des Weiteren gestattet Clojure auch nicht-funktionale Bestandteile, der Programmierer muss lediglich an manchen Stellen darauf achten, dass er keine Seiteneffekte erzeugt.
Wesentlich sind dabei die von Clojure bereitgestellten Datenstrukturen: Ohne weitere Angabe bleiben Daten in Clojure unveränderlich. Sind jedoch sich ändernde Zustände erwünscht, existieren vier verschiedene Arten von indirekten Referenzen, die in unterschiedlichen Anwendungsfällen das Ändern erlauben. Es gibt Referenztypen für Daten, die pro Thread eindeutig sind, für Daten mit nur einem Wert und synchroner Änderung sowie für Daten mit nur einem Wert und asynchroner Änderung. Zudem gibt es ein Transaktionssystem, das den synchronen Zugriff auf mehrere Daten im Speicher steuert, ähnlich, wie es eine Transaktion für ein Datenbanksystem leistet. Von diesen vier Typen verwendet das Beispiel, das dieser Artikel weiter unten beschreibt, drei: Vars, Refs und Agents.
Apfelmännchen
Listing 1 enthält den Code zum Berechnen einer Mandelbrot-Menge mit mehreren Threads und zum Anzeigen des Resultats. Abbildung 1 zeigt eine Emacs-Sitzung mit diesem Beispielcode. Zunächst definiert der Befehl »ns« einen neuen Namespace und importiert einige Java-Klassen. Namespaces in Clojure sind Java-Namespaces.
Der zweite Ausdruck definiert einige Parameter, die dafür sorgen, dass das Programm eine hübsche Anzeige erstellt. Die Verwendung von »def« in Zeile 6 sorgt dafür, dass Clojure eine globale Variable, hier mit dem Namen »param«, anlegt. Solche Typen heißen in Clojure Var, sie sind die ersten speziellen Typen im Beispiel. An diese Var bindet Clojure eine (Hash)-Map in geschweiften Klammern. Diese Bindung ist der Root-Wert, jeder Thread kann eine eigene Bindung verwenden. In der Map benutzt das Programm Keywords, erkennbar am führenden Doppelpunkt, als besonders effiziente Schlüssel.
Anschließend definiert das Listing die Funktionen »scale-range«, »size-range«, »color-log-scale«, »color-value« und »mandelbrot-iterations«. Die Argumente einer Funktion folgen, nach einem optionalen Docstring, dem Funktionsnamen in eckigen Klammern – die Syntax für einen Vektor. Der Befehl »range« liefert eine Sequence zurück, deren Inhalte aber nicht sofort festliegen, sondern erst in dem Moment entstehen, in dem ein Programmteil tatsächlich darauf zugreift. Sequences sind eine Abstraktion in Clojure, die den Zugriff auf verschiedenste Datenstrukturen vereinheitlichen, beispielsweise auf Vektoren und Maps, aber auch auf Dateien, XML-Dateien oder Datenbank-Ergebnisse. Für Sequences gilt Lazy Evaluation, sodass sie ihre Werte erst im Moment des Zugriffs realisieren. Auf diese Weise sind auch unendliche Sequences möglich, solange das Programm nur endlich viele Werte anfordert. Die beiden Color-Funktionen sorgen für eine gute Darstellung.
Die Funktion »mandelbrot-iterations« enthält die einfache Rechnung der iterativen Funktion zur Berechnung der Mandelbrot-Menge im Raum der komplexen Zahlen. Sie besteht aus grundlegender Mathematik, expliziten Typumwandlungen für eine höhere Geschwindigkeit und einem Schleifenkonstrukt »loop … recur«. In einer solchen Schleife definiert Clojure an der Stelle von »loop« einen Einstiegspunkt mit Variablenwerten und springt bei »recur« mit den dort angegebenen neuen Werten wieder zurück. Das entspricht einer Rekursion, schützt aber vor Stack-Überläufen.
Schließlich berechnet die Funktion »mandelbrot-line« ab Zeile 55 die Werte der Mandelbrot-Menge für eine Zeile von Bildpunkten. Beachtenswert sind hier der Befehl »let«, der lokale Variablen anlegt, sowie der Befehl »map«, der in der funktionalen Programmierung häufig auftaucht. Der Aufruf von »map« sorgt dafür, dass Clojure eine Funktion, im Beispiel anonym erzeugt mit »fn«, auf jedes Element einer Sequence, hier generiert durch »size-range«, anwendet.
Referenzen
Der in Zeile 71 folgende Ausdruck enthält eine weitere spezielle Datenstruktur: eine Ref. Der Befehl »repeat« wiederholt sein zweites Argument (den leeren Vektor »[]«) so oft, wie im ersten Argument angegeben. Auf dieses Resultat erzeugt dann »ref« eine besondere Referenz, und Clojure bindet sie dann an die Var »world«. In »world« speichert das Programm später die errechneten Werte. Für Clojure sind Daten aber wie gesagt unveränderlich. Es stellt sich die Frage, wie das Programm die Werte dort speichern kann.
Refs sind eine Besonderheit von Clojure, die kompliziertes Locking der Daten durch ein elegantes Transaktions-System ersetzt. Eine Transaktion verändert den Zustand mehrerer Refs in einem Thread, andere Threads bekommen immer einen konsistenten Zustand zu sehen.
Ab Zeile 73 folgen zwei Funktionen, die direkt Java-Klassen verwenden und das Abbild der »world« in einen Grafik-Kontext zeichnen. Die Funktion »make-world-image« demonstriert die Instanziierung eines Objekts der Klasse »BufferedImage« mit dem Befehl »new« – für Java-Programmierer vertraut.
Der Befehl »inc« liefert sein Argument um 1 erhöht zurück, »doseq« realisiert alle Werte der übergebenen Sequence, die ja lazy sein kann, um die hier gewollten Seiteneffekte zu bewirken. »when« funktioniert wie ein »if«, nur ohne »else«. Einen Aufruf einer Java-Methode auf dem Objekt »img« zeigt der Befehl ».setRGB«. Das ist fast das Gleiche wie in nativem Java-Code, nur dass die Methode vor dem Objekt steht. Bei der Erzeugung eines Color-Objekts zeigt das Beispiel etwas syntaktischen Zucker: Statt »(new Objekt)« erlaubt Clojure auch das kürzere »(Objekt.)« mit einem Punkt hinter dem Namen der Klasse.
Agents
Der Ausdruck in Zeile 96 zeigt die dritte besondere Datenstruktur von Clojure in diesem Beispiel: den Agent. Clojure erzeugt Agents, wenn es den Befehl »agent« findet, in diesem Falle einen Agent auf eine leere Map. Agents sind ein anderer Referenztyp und erlauben es, einen Wert asynchron zu ändern. An den Agent schickt ein Programmteil eine Funktion, die dann zu einem späteren Zeitpunkt und in einem separaten Thread ausgeführt wird. In diesem Beispiel verwendet das Programm einen Agent, um asynchron das Neuzeichnen der Welt zu signalisieren.
Malerei
Nach dieser Vorbereitung folgt nun die Funktion, die alles zusammenbaut. In der Funktion »mandelbrot-parallel« erzeugt das Programm zunächst einen Jframe und ein Jpanel, für das es die Methode »paintComponent« implementiert, was der Befehl »proxy« ermöglicht. Die Welt, gespeichert in der Ref »world«, darf das Programm nicht direkt manipulieren, also erzeugt es mit »dosync« eine Transaktion, in der der Befehl »ref-set« der Welt eine neue Liste von leeren Vektoren zuweist (Zeile 107). Danach bekommt das Panel die im Parameter-Satz definierte Größe zugewiesen. Maps sind Funktionen ihrer Schlüssel, das macht den Quelltext kompakt. Eine weitere kompakte Schreibweise erlaubt »doto« (Zeile 114): Es ruft auf dem ersten Argument, einem Java-Objekt, alle darauf folgenden Methoden auf.
Ab Zeile 119 folgt das Herzstück dieses Programms. Mit »dorun« bewirkt der Programmierer Seiteneffekte. Die komplette Parallelisierung des Algorithmus steckt in dem p von »pmap« in Zeile 121. Hier handelt es sich um den oben beschrieben Befehl »map«, der sich aber automatisch auf die vorhandenen CPUs oderKerne verteilt. Wie das auf einem Notebook mit zwei Cores (Intel Core2 Duo 1,8 GHz, 2 GByte RAM) während der Berechnung aussieht, zeigt die Abbildung 2. Obwohl durch den zweiten Kern kein großer Gewinn zu erwarten ist, da ja das wiederholte Zeichnen des Jpanel schon einen zweiten Thread beschäftigt, sinkt die Zeit für die Berechnung durch »pmap« von rund 5 Sekunden auf etwa 4.
Die anonyme Funktion (»fn«), die hier in »pmap« zur Anwendung kommt, verändert »world« in einer Transaktion (»dosync«), indem sie »alter« aufruft. Dieses »alter« nimmt eine Funktion entgegen, die keine Seiteneffekte hat. Dafür muss der Programmierer sorgen. Hintergrund dieser Anforderung ist das Transaktionsmodell, das die Funktion unter Umständen mehrfach ausführt. Der Rückgabewert dieser Funktion ist der neue Wert der Ref. In diesem Falle schreibt »assoc« dazu die vorher neu berechnete Zeile (»newline«) in den aktuellen Zustand (»state«) der Welt und liefert diese neue Welt zurück.
Nach dieser Manipulation von »world« in einer Transaktion kann der Agent das Signal bekommen, die Welt neu zu zeichnen (»send-off«). Das Resultat zeigt die Abbildung 3.
Neues und Bewährtes
Clojure ist neu und bringt zwei wesentliche Features mit: Unterstützung für Concurrency direkt in der Sprache und eine enge Verwandschaft mit Java. Das erste macht die Sprache interessant und gut geeignet für die nächsten Generationen von Prozessoren, das zweite gibt Clojure die Chance, produktiv eingesetzt zu werden. Der Einstieg scheint nicht leicht, ist aber einfacher, als mancher denkt ([9], [10]). Die interaktive Entwicklung an der REPL hilft dabei.
Während der Erstellung dieses Artikels hat Clojure ihren zweiten Geburtstag gefeiert. In diesen beiden Jahren hat die Sprache sowohl in der Java- als auch in der Lisp-Community einige Aufmerksamkeit erregt. Ob sie sich durchsetzen kann, wird sich bis zum vierten Geburtstag sicherlich zeigen. Herzlichen Glückwunsch, Clojure! (mhu)
|
Infos |
|---|
|
[1] Clojure-Webseite: [http://www.clojure.org] [2] Clojure-1.0-Download: [http://clojure.googlecode.com/files/clojure_1.0.0.zip] [3] Enclojure für Netbeans: [http://www.enclojure.org/] [4] Clojure-Mode, Swank-Backend: [http://github.com/jochu/] [5] SLIME: [http://common-lisp.net/project/slime/] [6] Vim Clojure: [http://kotka.de/projects/clojure/vimclojure.html] [7] Counterclockwise: [http://code.google.com/p/counterclockwise/] [8] Clojure und Contrib auf Github: [http://github.com/richhickey] [9] Ausführlicher Artikel für Einsteiger: [http://java.ociweb.com/marks/clojure/article.html] [10] Clojure-Vorträge als Videos: [http://clojure.blip.tv] |
|
Der Autor |
|---|
|
Stefan Kamphausen ist Physiker und hat eine Vorliebe für Sprachen der Lisp-Familie. Derzeit arbeitet er an einem Buch zum Thema Clojure. |








