Clojure

Transcription

Clojure
Java für Fortgeschrittene
Proseminar im Sommersemester 2009
Funktional programmieren auf der
JVM: Clojure
Florian Zacherl
Technische Universität München
19.05.2009
Zusammenfassung
Clojure ist eine dynamische, funktionale Programmiersprache, die auf
der Java Virtual Maschine“ von Sun läuft. Die Sprache ist noch sehr
”
neu (am 4. Mai 2009 wurde Version 1.0 veröffentlicht) und somit relativ
unbekannt. Dieser Artikel soll deshalb einen Einblick in die Möglichkeiten,
die Clojure bietet, geben.
1
Einleitung
Der interessanteste Aspekt von Clojure ist die Vereinigung von zwei völlig unterschiedlichen Programmieransätzen. Einerseits ist Clojure ein Dialekt von Lisp
und bietet somit die Vorteile der funktionalen Programmierung. Andererseits
ist Clojure in Java geschrieben, alle Datenstrukturen sind als Java-Objekte realisiert und es ist somit eine sehr enge Zusammenarbeit mit Java-Programmen
und -Bibliotheken möglich. Auch wird Clojure direkt in JVM-Code kompiliert,
was die Sprache prinzipiell genauso effizient wie Java macht.
Diese Arbeit gibt zuerest eine kurze Einführung in die funktionale Programmierung und die Syntax von Clojure. Nach einer kurzen Erläuterung der Sprachkonstrukte und elementaren Datentypen wird detailiert auf die wichtigsten Datenstrukturen und das Erstellen eigener Funktionen eingegangen. Nach einem
Überblick über die Intergration von Java werden zuletzt noch die Grenzen von
Clojure als funktionale Sprache aufgezeigt.
2
Funktionale Programmierung
Zu Beginn dieses Artikels wird ein kurzer Einblick in das Konzept funktionale
”
Programmierung“ gegeben.
1
In einer funktionalen Programmiersprache ist, wie der Name schon sagt, die
entscheidende Struktur die Funktion. Das heißt, es gibt keine Methoden, die
nur dazu dienen gewisse Operationen hintereinander auszuführen, sondern nur
Funktionen, die aus einer gewissen Eingabe einen gewissen Ausgabewert erzeugen.
Die strikte Einschränkung auf Funktionen schließt allerdings ein, dass Aspekte wie Ein-/Ausgabe und Benutzerinteraktion eigentlich nicht vorgesehen sind,
da sie ja nicht Ergebnis einer Berechnung sind. Sie werden als sogenannte Seiteneffekte betrachtet, die beim Aufruf von Funktionen auftreten. Clojure ist in
dieser Beziehung nicht strikt funktional: Funktionen mit Seiteneffekten werden
als Funktionen mit Rückgabewert nil (entspricht null ) betrachtet und nicht
gesondert behandelt.
Im weiteren werden kurz zwei Konzepte der funktionalen Programmierung, die
insbesondere für Clojure entscheidend sind, kurz angerissen:
• First-Order“ Funktionen: Funktionen werden als Variablen interpretiert.
”
Das heißt, sie können wie jede andere Variable anderen Funktionen übergeben oder von diesen zurückgegeben werden.
• Immutable Data“: Wird einer Variablen einmal ein Wert zugewiesen,
”
kann dieser nicht mehr verändert werden. Das hat den Vorteil, dass (wie
in Clojure) persistente Datenstrukturen möglich sind. Das bedeutet, dass
beispielsweise beim Erzeugen einer neuen Liste aus einer alten durch Hinzufügen eines neuen Elements nicht die alte Liste kopiert wird, sondern
diese weiterhin genutzt und nur mit dem neuen Element verlinkt“ wird.
”
Das spart einerseits Speicherplatz und ist andererseits sehr viel effizienter,
falls listenähnliche Strukturen kopiert“ werden.
”
3
Syntax
Clojure übernimmt von Lisp die sogenannte Präfix-Notation. Wie im Abschnitt
zuvor beschrieben, ist in Clojure alles eine Funktion und es gibt keine anderen
Konstrukte. (Anmerkung: Den Begriff Funktion verwende ich hier als Überbegriff für die drei unterschiedlichen Typen function“, macro“und special
”
”
”
form“. Dazu später mehr.)
Somit gibt es eine allgemeingültige Syntax, mit der man (fast) alles ausdrücken
kann:
(funktion argument1 argument2 ...)
An erster Stelle steht immer der Name der Funktion, auf den eine (beliebig
lange) Liste der Argumente folgt. Zu beachten ist dabei, dass der komplette
Ausdruck umklammert wird, d.h. die Klammer enthält sowohl den Funktionsnamen als auch die Argumente. Eine Addition zweier Zahlen wird beispielweise
folgendermaßen ausgedrückt:
(+ 1 2)
2
Ebenfalls möglich sind:
(+ 1 2 3)
(+ 1)
(+)
;Rückgabe 0
Die Funktion + hat also folgende Signatur: Number* → Number Anmerkung
zu Signaturen: Da Clojure eine dynamische Sprache ist, gibt es streng genommen keine Signaturen, da weder Argumente noch Rückgabewerte auf bestimmte
Datentypen beschränkt sind. Bei vielen Funktionen machen aber nur bestimmte Datentypen Sinn bzw. werden bei anderen Datentypen Exceptions geworfen.
Deswegen wird im weiteren eine symbolische Notation für Signaturen verwendet, die an reguläre Ausrücke angelehnt ist und zum Verständnis dienen soll.
Wenn jeder Datentyp Sinn macht, wird duch den Buchstaben a ein generischer
”
Datentyp“ angedeutet.
Es können auch beliebig viele Funktionsaufrufe geschachtelt werden:
(/ (+ 2 3 4) (- 6 2))
Ein weiteres Beispiel ist die Funktion println, die den übergebenen String gefolgt
von einer Leerzeile auf der Konsole ausgibt. Mehrere Argumente werden mit
Leerzeichen aneinander gehängt.
(println "Hello" "World!") gibt Hello World!“ aus.
”
Will man das nicht, muss man die Strings vorher selbst konkatenieren:
(println (str "Cloj" "ure")) gibt Clojure“ aus.
”
Der Rückgabewert der Funktion println ist nil, da die Funktion streng funktional
gesehen nichts berechnet, sondern die Textausgabe nur ein Seiteneffekt ist.
Kommas können beliebig eingebaut werden, da sie vom Compiler wie Leerzeichen behandelt werden. Die folgenden Ausdrücke sind äquivalent:
(+, 1, 2, 3) (+ 1,2,3) (+ 1 , , 2 , , 3)
Auch wenn man mit dieser Notation schon praktisch alles ausdrücken kann, gibt
es doch noch einige Abkürzungen, die das programmieren sehr erleichtern. So
werden beispielsweise Kommentare mit einem Semikolon begonnen, obwohl man
auch diese Notation verwenden könnte: (comment Kommentar)
4
Sprachkonstrukte
In Clojure gibt es drei verschiedene Typen von Funktionen: Funktionen, Makros
und sogenannte Special Forms“:
”
3
4.1
Funktionen
Funktionen werden wie folgt ausgewertet: Zuerst werden alle Argumente ausgewertet und dann daraus der Rückgabewert berechnet. Da Funktionen Daten
sind, können sie anderen Funktionen als Argumente übergeben werden.
4.2
Makros
Makros sind Funktionen, die nicht direkt ausgeführt werden, sondern vor dem
Kompilieren durch anderen Code ersetzt werden. Innerhalb des Makros wird
also nur festgelegt, durch welchen Code er vor der Ausführung ersetzt wird.
Damit können eigene syntaktische Konstrukte erstellt werden. Das einfachste
Beispiel für ein Makro ist comment, welches unabhängig von den Argumenten
immer durch nil ersetzt wird.
Makros können nicht direkt als Argumente für Funktionen übergeben werden.
Es ist allerdings möglich eine neue Funktion zu übergeben, die nur das Makro
ausführt.
4.3
Special Forms
”
”
Die sogenannten Special Forms“ sind Funktionen, die nicht in Clojure
”
selbst implementiert wurden. Es gibt nur sehr wenige davon und es können
keine eigenen erstellt werden. Special Forms“ werden anders ausgewer”
tet als reguläre Funktionen, so gibt es beispielsweise das special form“
”
(if bedingung if-zweig else-zweig), wobei zuerst die Bedingung ausgewertet wird und dann entweder das erste Argument ausgewertet und zurückgegeben wird oder das zweite.
5
Atomare Datentypen
Atomare Datentypen sind zuerst einmal Booleans (true, false) und nil. Dabei
ist zu beachten, dass nil in Abfragen genau wie false interpretiert wird.
Außerdem ist in Clojure das Symbol ein eigener Datentyp, der einem Variablennamen entspricht. Das ist nötig, da zum Beispiel das Special Form“ def,
”
das eine globale Variable erzeugt, als Argumente ein Symbol und einen Wert
erwartet.
(def x 5)
Diese globalen Variablen besitzen einen sogenannten Root-Value“, der theore”
tisch verändert werden kann. Das sollte aber vermieden werden, da sonst die
Vorteile von persistenten Datenstrukturen verloren gehen.
Einzelne Zeichen werden mit einem Backslash erzeugt: \C.
4
Strings werden in Anführungszeichen geschrieben, werden allerdings nicht mit
+, sondern mit str (a* → String) konkateniert.
(def s (str "Test" 1 2 3))
Zu beachten ist, dass die Funktion = (a* → Boolean) der Java-Funktion equals
entspricht. Diese kann also für den String-Vergleich benutzt werden:
(def s1 "Test")
(def s2 "Test")
(def s3 "Etwas anderes")
(= s1 s2) ;gibt true zurück
(= s1 s3) ;gibt false zurück
Außerdem gibt es sogenannte Schlüsselwörter. Diese werden durch einen vorangestellten Doppelpunkt ausgezeichnet und haben keinen Wert, d.h. sie werden
nur zu sich selbst ausgewertet. Somit können sie sehr effizient verglichen werden.
In Clojure erben alle Zahlen von java.lang.Number. Zusätzlich zu Integer, Long
und Double sind BigInteger und BigDecimal integriert, wobei die Typen automatisch umgewandelt werden, wenn der Wertebereich überschritten wird. Außerdem gibt es den speziellen Zahlentyp Ratio, der für Brüche verwendet wird.
Bei Division von ganzen Zahlen werden Brüche erzeugt, um Rundungsfehler zu
vermeiden. Fließkommazahlen werden nur dann erzeugt, wenn in der Berechnung schon eine Fließkommazahl vorkommt:
(/ 1 3)
;gibt 1/3 zurück
(/ 1.0 3) ;gibt 0.3333333333333333 zurück
Insgesamt muss man sich um den Typ von Zahlen kaum kümmern, da Clojure
selbst passende Datentypen wählt.
6
Sequences und Collections
In Clojure wird mit sogenannten Sequenzen gearbeitert. Eine Sequenz ist kein
konkreter Datentyp, sondern der Überbegriff für listenartige Strukturen, der
durch das Interface ISeq definiert wird. Konkrete Datentypen sind die Collections, wobei die Funktion seq für jede Collection die zugehörige Sequenz zurückgibt. Somit können immer Collections übergeben werden, wenn eine Funktion
Sequenzen als Argument benötigt (seq muss nicht explizit aufgerufen werden).
Diese implementieren teilweise das Java-Interface Collection. Clojure bietet verschiedene Collection-Klassen an, die alle persistent und unveränderlich sind.
Außerdem können sie sämtliche Datentypen (auch gemischt) enthalten.
Im folgenden werden die wichtigsten Collection-Typen kurz vorgestellt:
5
• Listen
Eine Liste enthält eine Menge von Werten in einer bestimmten Reihenfolge. Sie wird mit der Funktion list erstellt:
(list "a" 1.2 2 3 "Clojure" (+ 1 2 3))
• Vektoren
Ein Vektor entpricht einem Java-Array. Dies ist eine Liste von Werten,
auf die per Index zugegriffen werden kann. Dies geschieht entweder durch
Aufruf der Methode get oder aber durch Aufrufen des Vektors“ mit ei”
nem Index als Argument. Dies ist möglich, da Vektoren das Interface IFn
implemtieren, welches für alle Clojure-Funktionen genutzt wird. Vektoren
werden mit eckigen Klammern erstellt und das erste Element wird mit 0
indiziert.
(def v [1 2 "String"])
(get v 1)
(v 0)
• Maps
Maps
enthalten
Paare
aus
Schlüsseln
und
Werten
und
werden
mit
geschweiften
Klammern
erstellt:
(def m {:name "Meier" :vorname "Hans" :alter 99}) Für Schlüssel
werden meist Schlüsselwörter verwendet. Auf die einzelnen Werte kann
wiederum mit der get-Funktion ((get m :vorname)) oder direkt durch
Aufrufen der Map“ mit einem Schlüssel als Argument zugegriffen werden
”
((m :alter)). Falls der Schlüssel ein Schlüsselwort ist, kann auch das
Schlüsselwort mit der Map als Argument aufgerufen werden, um den
zughörigen Wert zu erhalten ((:name m)).
Die Schlüssel müssen dabei nicht zwingend Schlüsselwörter sein, es sind
auch beliebige andere Datentypen möglich:
(def m2 {"abc" 1 1 1.2})
Dem Schlüssel abc“ wird also der Wert 1 zugeordnet und dem Schlüssel
”
1 der Wert 1.2.
Zuätzlich zu den genannten Clojure-eigenen Collections können alle Funktionen,
die auf Sequenzen arbeiten auch auf Strings oder Java-Objekten, die Iterable
implementieren, arbeiten. In der Kernbibliothek von Clojure gibt es eine sehr
große Menge von solchen Funktionen, wovon einige wichtige hier kurz genannt
werden:
6
• first (seq → element): Gibt das erste Element der Sequenz zurück.
• rest (seq → seq|nil): Gibt den Rest einer Liste zurück, also eine Liste
aller Elemente, außer demjenigen, das durch first zurückgegeben wird.
Bei einem Element bzw. der leeren Liste wird nil zurückgegeben.
• cons (seq×element → seq): Gibt eine Sequenz zurück, bei der das neue
Element vorne an die Ursprungssequenz angefügt wurde
• count (seq → int): Gibt die Anzahl der Elemente zurück
• take (seq×int → seq): Gibt die ersten n Elemente einer Sequenz zurück
oder die gesamte Sequenz, wenn sie weniger als n Elemente enthält
Sehr oft werden sogenannte lazy sequences“ erzeugt, d.h. die einzelnen Elemen”
te der Sequenz werden nicht sofort sondern erst bei Bedarf erzeugt. Dies macht
es mögliche unendliche Sequenzen zu erzeugen, wozu bespielsweise die Funktion
repeat verwendet werden kann:
(def x (repeat 4))
(take 12 x) ;gibt die Sequenz (4 4 4 4 4 4 4 4 4 4 4 4) zurück
7
Eigene Funktionen definieren
Eigene Funktionen werden mit dem Special Form“ fn erstellt. Als Parameter
”
werden der Name der Funktion (optional), ein Vektor mit den Argumenten und
ein Ausdruck übergeben, der festlegt, was die Funktion berechnet:
(fn name? [params* ] exprs*)
Die folgende Funktion berechnet zum Beispiel die Summer zweier Zahlen:
(fn f [a b] (+ a b))
Zu Beachten ist, dass fn“ nur eine Funktion erzeugt, sie aber nicht unter
”
dem übergebenen Namen speichert. Dieser kann also auch nicht zum Aufruf der Funktion verwendet werden, sondern nur innerhalb der Funktion,
um beispielsweise einen rekursiven Aufruf zu starten. Um von außen auf die
Funktion zugreifen zu können, muss sie einer Variablen zugewiesen werden:
(def add (fn [a b] (+ a b))) und kann dann über deren Namen aufgerufen
werden: (add 1 2).
Eine abkürzende Schreibweise hierfür bietet der Makro defn. Der Ausdruck
(defn add [a b] (+ a b)) definiert wie oben eine Funktion, die zwei Zahlen addiert und über den Namen add aufgerufen werden kann. Dies entspricht
aber exakt der obigen Formulierung, da das Makro vor dem Kompilieren genau
duch eine Funktionserzeugung mit fn und eine anschließende Zuweisung mit def
ersetzt wird.
In manchen Fällen muss man allerdings eine Funktion gar nicht einer Variablen
zuweisen, es reicht auch eine anonyme Funktion zu definieren. Die Funktion map
7
(seq×fun → seq) wendet zum Beispiel die Funktion fun auf alle Elemente einer
Sequenz an. Anstatt erst eine Funktion zu erzeugen und sie dann zu übergeben,
kann man auch direkt eine anonyme Funktion übergeben.
Möglichkeit 1:
(defn f [x] (+ x 2))
(map f [1 2 3])
Möglickeit 2:
(map (fn [x] (+ x 2)) [1 2 3])
7.1
Lokale Variablen
Für lokale Variablen innerhalb einer Funktion gibt es das special form“ let“.
”
”
Es wird ein Vektor mit Variablenzuweisungen übergeben (z.B. [a 1 b 2 c 99])
und ein oder mehrere Ausrücke, die nacheinander ausgeführt werden, wobei der
Rückgabewert des letzten Ausdrucks zurückgegeben wird.
(defn f [a]
(let [a 5]
(println a))
(println a))
Beim Aufruf von (f 42) wird also zuerst 5 und dann 42 ausgegeben, da die
zweite Ausgabe nicht mehr innerhalb von let stattfindet und sich somit auf das
Argument a bezieht.
7.2
Überladen
Wie in Java auch, können Funktionen überladen werden, es kann allerdings nur
nach der Anzahl der Argumente unterschieden werden, nicht nach dem Typ.
Dies resultiert daraus, dass Clojure eine dynamische Sprache ist, dass also der
Typ der Argumente nicht festgelegt ist, sondern erst zur Laufzeit bestimmt wird.
(defn zaehle_argumente ([]
"Keine Argumente")
([x]
"Ein Argument")
([x y] "Zwei Argumente"))
Dies kann mit dem Zeichen &“ auf beliebig viele Argumente ausgedehnt wer”
den, indem alle Argumente nach dem &“ als Sequenz aufgefasst werden:
”
(defn zaehle_argumente ([]
"Keine Argumente")
([& rest] (str (count rest) " Argumente")))
8
Ein Aufruf von (zaehle_parameter "abc" 1 [1 2 3] :bah 7.5) liefert dann
als Rückgabewert 5 Parameter“.
”
Analog zu Java kann man also beispielsweise einer Funktion, die ohne Argumente aufgerufen wird, default-Argumente“ übergeben:
”
(defn f ([] (f 0 0))
([a]
(f a 0))
([a b] (println a b)))
Das Beispiel soll so interpretiert werden, dass die Funktion zwei Zahlen erwartet
und für nicht übergebene Argumente eine Null nimmt.
7.3
Multimethods“
”
Ein weiterer nützlicher Aspekt von Clojure sind sogenannte Multimethods“.
”
Diese werden mit den beiden Makros defmulti und defmethod erstellt. Mit Hilfe
von defmulti wird die eigentliche Multimethode bestimmt. Ihr wird außerdem
Namen noch eine Dispatch“-Funktion übergeben. Diese Funktion ist so ge”
dacht, dass sie auf die Argumente angewendet wird und nach den unterschiedlichen Ergebnissen unterschieden wird. Dazu wird der Makro defmethod benutzt.
Diesem werden folgende Argumente übergeben:
• Der Name der zugehörigen Multimethode
• Derjenige Rückgabewert, bei dem die Funktion aufgerufen werden soll.
• Ein Vektor für die (ursprünglichen) Argumente
• Der auszuführende Code
Man hat somit eine ähnliche Funktion wie das switch-Konstrukt in Java, nur
dass es sehr viel mächtiger ist, da die Dispatch“-Funktion beliebiges berech”
nen kann, nach dem dann unterschieden wird. Im einfachsten Fall kann also
dieses nachgebildet werden, indem man die Identität als Dispatch“-Funktion
”
verwendet:
(defmulti switch (fn [x] x))
(defmethod switch 0 [x] (tue_etwas 0))
(defmethod switch 1 [x] (tue_etwas 1))
(defmethod switch 2 [x] (tue_etwas 1))
(defmethod switch :default [x] (println "Ungültige Eingabe!")))
Für alle Fälle, die nicht abgefangen werden, kann das Schlüsselwort :default
benutzt werden.
Im folgenden Beispiel wird nach einigen Klassen unterschieden. Die Funktion class, die den Klassentyp zurückgibt, wird hierbei als Dispatch“-Funktion
”
benutzt. Außerdem ist zu beachten, dass bei der Abfrage der einzelnen Fälle
9
bei Multimethoden immer die Funktion isa? benutzt wird, die dann wahr
ist, wenn Gleicheit besteht oder das Objekt von der jeweiligen Klasse erbt.
Da in Clojure wie in Java alle Objekte von Object erben ist insbesondere
(isa? irgendeine_klasse Object) immer wahr.
(defmulti was class)
(defmethod was String [s] (println s "ist ein String!"))
(defmethod was Number [n] (println n "ist eine Nummer!"))
(defmethod was clojure.lang.Ratio [r] (println r "ist ein Bruch!"))
(defmethod was :default [x] (println x "ist etwas anderes!"))
(was 1/2)
(was 5)
(was [1 2 3])
;gibt "1/2 ist ein Bruch!" aus
;gibt "5 ist eine Nummer!" aus
;gibt "[1 2 3] ist etwas anderes!" aus
Wenn mehrere Fälle zutreffend sind (1/2 ist sowohl Number als auch clojure.lang.Ratio) wird, falls möglich, der präzisere ausgesucht. Falls das nicht
möglich ist, kann die Methode prefer-method genutzt werden. Das folgende
Codefragment würde eine Exception erzeugen, da nicht klar ist, welcher Fall
gewählt werden soll:
(defmulti was (fn [x y] [(class x) (class y)]))
(defmethod was [String Object] [x y] (println "Fall 1"))
(defmethod was [Object String] [x y] (println "Fall 2"))
(was "String1" "String2")
Durch Hinzufügen folgender Zeile wird dies verhindert, indem festgelegt wird,
dass der erste Fall wichtiger als der zweite ist:
(prefer-method was [String Object] [Object String])
7.4
Funktionen dokumentieren
In Clojure kann jede Funktion dokumentiert werden, indem man bei der Funktionsdefinition einen String übergibt:
(defn verdopple
"Diese Funktion verdoppelt den übergebenen Wert"
[x]
(* 2 x))
Diese Dokumentation kann dann zur Laufzeit über die Funktion doc aufgerufen
werden. Der Aufruf von (doc verdopple) liefert dann:
10
user/verdopple
([x])
Diese Funktion verdoppelt den übergebenen Wert
8
Java Integration
Clojure besitzt zwar eine sehr große Anzahl an Funktionen in der Kernbibliothek, aber keine anderen Bibliotheken, auf die man zugreifen kann. Das ist
allerdings kein Nachteil, da man stattdessen uneingeschränkt auf alle JavaBibliotheken zugreifen kann. Die java.lang-Bibliothek ist beispielsweise wie auch
in Java von Haus aus eingebunden und kann dementsprechend genutzt werden.
So können beispielsweise die Funktionen der Klasse Math direkt verwendet werden, die Clojure zum Großteil nicht selbst anbietet. Es gibt zwei äquivalente
Schreibweisen, um auf eine statische Methode zuzugreifen, die .“-Funktion oder
”
das Zeichen /“:
”
(. Math pow 2 5)
(Math/abs -10)
Auch Objektmethoden können ähnlich aufgerufen werden. So kann die von pow
zurückgelieferte Fließkommazahl mit Hilfe der Methode intValue in einen Integer umgewandelt werden.
(. (. Math pow 2 5) intValue)
Des weiteren können Bibliotheken importiert werden oder direkt über den Bibliothekspfad auf Klassen zugeriffen werden:
(def d (new java.util.Date))
;Pfadangabe
(import ’(java.util Date))
(def d (new Date))
;Import
Zu beachten ist bei der import-Funktion, dass der Pfad der Bibliothek nicht einfach übergeben, sondern zitiert wird (durch das Hochkomma vor der Klammer
oder alternativ durch den Aufruf von quote). Zitierte Ausrücke dagegen werden
nur als Symbol übergeben.
Es können auch mehrere Klassen einer Bibliothek auf einmal der importFunktion übergeben werden.
Am folgenden Beispiel sieht man die zwei äquivalenten Schreibweisen für das
Erstellen eines neuen Java-Objekts: Einmal wird die Funktion new verwendet
einmal die abkürzende Schreibweise mit dem nachgestellten Punkt. Außerdem
wird auf ein Attribut der Klasse JFrame zugegriffen:
11
(import ’(javax.swing JFrame JButton))
(do
(def fenster (new JFrame "Titel"))
(. fenster setBounds 100 100 200 200)
(. fenster setDefaultCloseOperation (. JFrame DISPOSE_ON_CLOSE))
(. fenster add (JButton. "Button1"))
(. fenster setVisible true))
Anmerkung: Im Beipiel wird das Special Form“ do verwendet, das nichts weiter
”
macht als eine Reihe von Ausdrücken auszuwerten und den Rückgabewert des
letzten zurückzugeben.
Zusammenfassend kann man also sagen, dass sämtliche Operationen auf JavaObjekten bzw. -klassen durch Nutzung des Punkt-Operators ausgedrückt werden können (auch wenn es meistens noch alternative Formulierungen gibt):
(. Klasse/Objekt Methode/Atrribut Argumente*)
Das obige Code-Fragment kann übrigens auch eleganter dargestellt werden, dies
soll allerdings nur als ein Beispiel dafür dienen, dass Clojure eine Reihe von
Funktionen zur Verfügung stellt, mit denen die Java-Integration in bestimmten
Fällen sehr komfortabel möglich ist. Es wird das Makro doto verwendet, das
mehrere Methoden auf dem selben Objekt aufruft und es danach zurückgibt:
(import ’(javax.swing JFrame JButton))
(doto (JFrame. "Titel")
(.setBounds 100 100 200 200)
(.setDefaultCloseOperation JFrame/DISPOSE_ON_CLOSE)
(.add (JButton. "Button1"))
(.setVisible true))
9
9.1
Grenzen von Clojure als funktionale Sprache
Endrekursion
Man kann in Clojure Rekursion einsetzen, indem man direkt eine Funktion sich
selbst aufrufen lässt. In den meisten funktionalen Sprachen wird allerdings ein
Konzept namens Endrekursion“ sehr effizient umgesetzt. Wenn der rekursive
”
Aufruf nämlich als letzes aufgerufen wird, ist es prinzipiell nicht nötig wie gewohnt für jeden einzelnen rekursiven Aufruf wieder einen eigenen Stack... usw
zu erzeugen, da weder die lokalen Variablen der momentane Funktion noch die
Rücksprungadresse noch benötigt werden. Die Rekursion muss so aufgebaut
sein, dass sobald die Abbruchbedingung ereicht wird, kein Rücksprung in die
jeweiligen vorherigen Aufrufe nötig ist. Eine endrekursive Funktion sieht also
zum Beispiel so aus:
12
(defn fac [n acc]
(if (= n 1)
acc
(fac (dec n) (* n acc))))
In anderen funktionalen Sprachen, würde der Compiler jetzt die Endrekursion
erkennen und dementsprechend umsetzen, da Clojure allerdings auf der JVM
läuft, ist das nicht so einfach möglich, da diese keine Unterstützung für Endrekursion anbietet. Es gibt allerdings eine Möglichkeit trotzdem diese effizientere
Methode der Rekursion zu nutzen und zwar mit dem Special Form“ recur :
”
(defn fac [n acc]
(if (= n 1)
acc
(recur (dec n) (* n acc))))
Das heißt es wird also ausschließlich beim rekursiven Aufruf statt des Funktionsnamens der Name recur verwendet.
Bemerkung: Der Compiler erkennt, ob der Aufruf von recur sich an einer sinnvollen Position befindet, d.h. falls eine Funktion recur aufruft, d.h. falls beim
Kompilieren keine Exception geworfen wird, kann man sich auch sicher sein,
dass der Aufruf endrekursiv ausgeführt wird. Da der Compiler erkennt, an welchen Stellen Endrekursion benutzt werden kann, sollte es auch möglich sein,
dass dieser sie automatisch umsetzt, was aber nicht der Fall ist. Da Clojure
aber noch eine sehr neue Sprache ist, ist es möglich, dass dies bei einer neueren
Version noch ergänzt wird.
9.2
Currying
Ein weiteres Konzept, das in vielen funktionalen Sprachen unterstützt wird, ist
Currying, d.h. bei einem Funktionsaufruf wird nicht die Funktion mit allen Argumenten ausgewertet, sondern erstmal nur mit dem ersten, was eine Funktion
zurückgibt, die mit einem Argument weniger ausgewertet werden kann usw. Es
wird also ein Funktionsaufruf mit mehreren Argumenten auf mehrere Funktionsaufrufe mit einem Argument zurückgeführt. Clojure setzt dieses Konzept
kaum um. Natürlich kann man immer eine anonyme Funktion definieren, die eine andere Funktion mit bestimmten festen Werten aufruft. Als Beispiel hier eine
Funktion, die aus den Argumenten a, b und c den Wert a ∗ b + c berechnet und
eine zweite Funktion, die diese benutzt, um den Wert von 2b + c zu berechnen:
(defn irgendwas [a b c] (+ (* a b) c))
(fn [b c] (irgendwas 2 b c))
Dafür gibt es auch eine abkürzende Schreibweise:
#(irgendwas 2 %1 %2)
13
Es müssen allerdings immer alle Argumente mit % durchnummeriert angegeben
werden (bzw. %& für eine Restsequenz), es kann also nicht wie in anderen
Sprachen die Funktion mit weniger Argumenten aufgerufen werden, sondern es
können nur bestimmte durch Platzhalter ersetzt werden.
10
Demoprojekt
Das Demoprojekt, das zu dieser Arbeit erstellt wurde, dient dazu verschiedene Fraktale (d.h. geometrische Formen, die durch rekursive Anwendung einer
Konstruktionsvorschrift entstehen) in verschiedenen Iterationsstufen aufzuzeichnen. Dabei wird die vollständige Berechnung von einem Clojue-Programm übernommen, welches einem zugehörigen Java-Programm eine Menge von Punkten
liefert. Dieses öffnet eine grafische Benutzeroberfläche und übernimmt die Zeichenaufgaben.
11
Schluss
Clojure ist eine vollständige Programmiersprache, d.h. diese Arbeit kann nur
einen kleinen Einblick in die meiner Meinung nach interessantesten Konzepte
von Clojure bieten.
Was beispielsweise nicht erwähnt wurde, ist die lock“-lose parallele Program”
mierung, die Clojure anbietet. In Java muss der Programmierer sich bei nebenläufigen Programmen mit Locks und Sychronisationsmechanismen beschäftigen, da es direkte Referenzen auf veränderbare Objekte gibt. In Clojure ist das
anders gelöst: Hier gibt es nur indirekte Referenzen auf unveränderliche persistente Datenstrukturen. Es sind keine Locks nötig, da in Clojure die Semantik
für Nebenläufigkeit bereits automatisch im Referenzmechanismus integriert ist.
Auch wurde auf eine genauere Durchleuchtung des Makro-Systems von Clojure
verzichtet, da der Schwerpunkt dieser Arbeit auf den funktionalen Aspekten von
Clojure liegt.
Quellen
[1] Clojure-home (Stand Mai 2009)
http://clojure.org/
[2] Object computing, inc. clojure - functional programming for the jvm (Stand
Mai 2009)
http://jnb.ociweb.com/jnb/jnbMar2009.html
[3] Clojure tutorial for the non-lisp programmer - Moxley Stratton (Stand Mai
2009)
http://www.moxleystratton.com/article/clojure/
for-non-lisp-programmers
14
[4] Clojure programming - wikibooks, collection of open-content textbooks
(Stand Mai 2009)
http://en.wikibooks.org/wiki/Clojure_Programming
[5] Youtube - clojure tutorial (Stand Mai 2009)
http://www.youtube.com/view_play_list?p=AC43CFB134E85266&search_
query=clojure
15