Open Source im professionellen Einsatz
Linux-Magazin 01/2010
1186

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:
»mandelbrot.clj«

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.

Abbildung 1: Emacs mit Quellcode (oben) und einer REPL mit einigen Befehlen (unten).

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.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 5 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • Clojure

    Clojure sieht aus wie Lisp und läuft überall, wo Java installiert ist. Dank praktischer Tools und ausgereifter Bibliotheken ist mit der Sprache auch rasch eine moderne Webanwendung programmiert.

  • Unabhängigkeit: Clojure will ohne Spenden auskommen

    Der Erfinder des Lisp-Dialekts Clojure will sein Projekt künftig ohne Spenden weiterführen.

  • Spenden für Clojure

    Rich Hickey, Erfinder und Hauptentwickler der funktionalen Programmiersprache Clojure, bittet um Spenden, um das Projekt weiter verfolgen zu können.

  • Tux liest

    Das Linux-Magazin nimmt ein englischsprachiges Buch unter die Lupe, das sich mit Ubuntu Server beschäftigt und das Etikett "offizielles Handbuch" trägt. Der zweite Titel ist in Deutsch abgefasst und widmet sich der funktionalen Programmiersprache Clojure.

     

  • Clojure 1.6 stabilisiert Alpha-Features

    Die funktionale, Java-basierte Programmiersprache Clojure, die sich an Lisp orientiert, ist in Version 1.6 erschienen. Einige vormalige Alpha-Features sind nun den Kinderschuhen entwachsen.

comments powered by Disqus

Stellenmarkt

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.