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