Arbeitsgruppe Programmiersprachen und Übersetzerkonstruktion Institut für Informatik Christian-Albrechts-Universität zu Kiel Clojure Dominik Köster 22.02.2013 Betreuer: Fabian Reck Inhaltsverzeichnis 1 Einleitung 1 2 Einordnung 2.1 Geschichte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Anwendungsgebiet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Paradigma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 2 3 3 Sprache 3.1 Read-Eval-Print-Loop (REPL): . . . 3.2 Sprachelemente . . . . . . . . . . . . 3.2.1 Datentypen . . . . . . . . . . 3.2.2 Datenstrukturen . . . . . . . 3.2.3 Allgemeine Struktur . . . . . 3.2.4 Sequences . . . . . . . . . . . 3.2.5 Kontrollstrukturen . . . . . . 3.2.6 Makros . . . . . . . . . . . . 3.2.7 Funktionen höherer Ordnung 3.2.8 Closures . . . . . . . . . . . . 3.2.9 Laziness . . . . . . . . . . . . 3.2.10 Namespaces . . . . . . . . . . 4 Clojures spezielle Eigenschaften 4.1 Nebenläufigkeit . . . . . . . 4.2 Clojure und Java . . . . . . 4.2.1 Java in Clojure . . . 4.2.2 Clojure in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 5 6 7 7 8 9 10 10 11 11 . . . . 12 12 17 17 18 5 Technische Unterstüzung 19 6 Fazit 20 ii 1 Einleitung Im Laufe der letzten Jahre ist eine Veränderung auf dem Prozessormarkt zu erkennen gewesen. Die Leistung früherer CPUs war von der Höhe der Taktrate abhängig. Diese sind aber im Laufe der Jahre an ihre physikalischen Grenzen gestoßen, da bei den hohen Frequenzen zu viel Abwärme produziert wurde. Heutige Prozessoren unterscheiden sich hauptsächlich in der Anzahl ihrer Kerne. Aber nicht jedes Programm kann diese Eigenschaft nutzen. Um davon zu profitieren, ist es von Nöten Programme zu schreiben, welche einzelne Threads auf die Kerne verteilt. Die Entwicklung solcher MultithreadingProgramme bezeichnet man als nebenläufige Programmierung. Aufgrund der Prozessorveränderungen hat diese in den letzten Jahren immer mehr an Bedeutung gewonnen. Die Aufteilung von Programmen bringt aber auch eine gewisse Komplexität mit sich, da man das Nutzen gemeinsamer Ressourcen zwischen den Threads abstimmen muss. Hierfür wurden bereits gewisse Techniken entwickelt, um dies zu ermöglichen. Die wohl bekanntesten sind Semaphoren. Und während alt eingesessene Programmierer versuchen ihre durch diese verursachten Deadlocks aus ihrem Code zu entfernen, versucht sich die noch junge Programmiersprache Clojure als ein Werkzeug, um die Nebenläufigkeit zu vereinfachen. 1 2 Einordnung 2.1 Geschichte Clojure ist eine noch sehr junge Sprache und kann deshalb noch auf keine lange Geschichte zurückgreifen. Entwickelt wurde sie im Jahre 2008 von Rich Hickey. Dieser wollte eine Programmiersprache entwickeln, welche speziell für die nebenläufige Programmierung gedacht ist. Die meisten älteren Sprachen, haben erst im im Laufe der Zeit Erweiterungen dieser Art erfahren. Hierbei bediente er sich bei zwei schon etablierten Sprachen. Zum einen Lisp, welche durch ihre einfache Syntax und den geringen Sprachkern einen leichten Interpreterbau garantiert. Zum anderen Java, welche schon ein großes Paket an Bibliotheken und mit der Java Virtual Machine (JVM) eine Plattformunabhängigkeit zur Verfügung stellt. Lisp wurde 1958 entwickelt und ist eine funktionale Sprache. Der bekannteste Vertreter der Lisp-Familie ist das 1984 entwickelte Common-Lisp. Viele Anwender behaupten, dass die gesamte Sprachfamilie eine Modernisierung benötigt, besonders im Multithreading, einem der wichtigsten Aspekte in Clojure. Java ist 1995 erschienen und ist eine objektorientierte Sprache. Sie zeichnet sich durch die JVM und einer stetig wachsenden Bibliothekensammlung aus, welche sich Clojure zu Nutze macht. Java hat sich in den letzten Jahren zu einer der beliebtesten Sprachen entwickeln können und ist besonders bei der jüngeren Programmierergeneration beliebt. Für Hickey war das Schaffen eines Lisps mit den Vorteilen Javas der treibende Grund für die Entwicklung Clojures. Man muss an dieser Stelle aber klar sagen, dass Clojure trotz Allem, was es sich von Java angeeignet hat, zu den Lisp-Dialekten gehört. Dies wird man an späteren Codebeispielen klar erkennen. Im Moment befindet sich die Sprache in der Version 1.4. 2.2 Anwendungsgebiet Aufgrund des jungen Alters ist Clojure auf dem kommerziellen Sektor noch nicht in Erscheinung getreten. Zwar hat sich schon eine gewisse Fangemeinde gebildet, doch beschränkt diese sich eher auf private oder auf Clojure spezifische Projekte, wie zum Beispiel Leiningen[1], welches das Arbeiten mit Clojure-Projekten vereinfachen soll. 2 2 Einordnung 2.3 Paradigma Clojure gehört zu den nicht rein funktionalen Sprachen. Fachleute sind sich nicht immer einig, welche Kriterien ausschlaggebend dafür sind, ob eine Sprache rein funktional ist oder nicht. Einige Merkmale rein funktionaler Sprachen sind: 1. Funktionen werden als Datentypen wie Zahlen oder Strings angesehen. 2. Seiteneffekte sollten nicht auftreten. 3. Rekursive Lösungen sind idiomatische Lösungen. 4. Ein statisches, starkes Typsystem. 5. Die Auswertung von Ausdrücken findet erst statt, wenn das Ergebnis benötigt wird. Clojure erfüllt nicht alle dieser Eigenschaften und ist somit keine rein funktionale Sprache, wie zum Beispiel Haskell. Clojure lässt Nebeneffekte zu und das Typsystem ist dynamisch. Das bedeutet, dass eine Typprüfung erst zur Laufzeit vorgenommen wird. Da Clojure auf Java-Klassen zugreifen kann, besteht in Clojure die Möglichkeit, Klassen und Objekte zu erzeugen. Da diese aber für die normale Programmierung mit Clojure nicht verwendet werden, gehört die Sprache nicht zu den objektorientierten Programmiersprachen. 3 3 Sprache 3.1 Read-Eval-Print-Loop (REPL): Wie die meisten Lispdialekte hat Clojure eine interaktive Programmierumgebung. Der Benutzer gibt Ausdrücke ein, welche vom Reader eingelesen und in eine für den Evaluator verstehbare Form übersetzt werden. Dieser wertet die Eingaben dann aus und übergibt sie dem Printer, welcher sie dann an den Benutzer zurückgibt. Daraufhin wartet das Programm auf eine neue Eingabe. Somit kann man schon während der Programmierung stetig Rücksprache mit dem Programm halten. Man kann die REPL auch verwenden um temporären Code zu implementieren, welcher während der aktuellen Sitzung gültig ist. Bei der Entwicklung größerer Programme wird jedoch empfohlen eine IDE statt der REPL zu benutzen. Durch die Verbindung mit Java stehen hierbei Add-Ons für bekannte IDEs wie Eclipse oder NetBeans zur Verfügung. Abbildung 3.1: Ein Ausschnitt einer REPL 4 3 Sprache 3.2 Sprachelemente 3.2.1 Datentypen Booleans: Booleans sind entweder true oder false. Zahlen: Zahlen werden in Clojure von der Klasse java.lang.Number abgeleitet. Somit existieren Integer, Long und BigInteger bei den Ganzzahlen, Float, Double und BigDecimal bei den Gleitkommazahlen. Wenn bei einem Ausdruck eine Gleitkommazahl auftritt, dann wird das Ergebnis automatisch zu einer Gleitkommazahl umgewandelt. Eine Besonderheit Clojures gegenüber Java ist der Zahlentyp Ratio. Die Darstellung eines solchen erfolgt mit einem Schrägstrich. Bei einer Ganzzahloperation, welche keine Ganzzahl zum Ergebnis hat, wird ein Bruch als Ergebnis geliefert und keine Gleitkommazahl. Dies hat den Vorteil, dass bei Rundungen nichts verloren geht. Characters: Bei den Characters hat sich Clojure bei der gleichnamigen Java-Klasse bedient. Sie werden durch einen vorangestellten Backslash gekennzeichnet, z.B.: \a. Es gibt auch speziell benannte Characters wie z.B. \newline. Strings: Auch hier handelt es sich wieder um eine übernommene Java-Klasse. Wie aus den meisten Sprachen bekannt, werden Strings durch Anführungszeichen erzeugt, z.B.: “Ich bin ein String”. Nil: Nil bedeutet ”kein Wert“ und kann mit null in Java gleichgesetzt werden. Symbole: Symbole sind Namen für Werte. Bei der Auswertung liefert das Symbol den Wert zurück, welcher an sie gebunden wurde. Keywords: Im Gegensatz zu Symbolen liefern Keywords bei ihrer Evaluierung sich selbst zurück und keinen an sie gebundenen Wert. Daher finden sie häufig Verwendung als Schlüssel in den weiter unten vorgestellten Maps. Funktionen: Wie in den meisten funktionalen Programmiersprachen werden auch in Clojure Funktionen wie ganz normale Datentypen gehandelt. So kann eine Funktion einer anderen als Parameter übergeben werden.(Siehe Unterabschnitt: Funktionen höherer Ordnung) 5 3 Sprache Typ Boolean Number Character String Nil Keyword Function Symbol Beispiel true, false 8, 2.1 \a “Moin” nil :tag (defn plus [a b] (+ a b)) einsymbol Tabelle 1: Übersicht über die Datentypen 3.2.2 Datenstrukturen Datenstrukturen ermöglichen das Arbeiten mit Daten. In Clojure stehen hierfür Listen,Vektoren, Maps und Sets zur Verfügung. Sie werden alle von java.util.Collection abgeleitet. Listen: In Clojure existieren einfach verkettete Listen, welche ,wie bei allen Lisp-Dialekten, durch runde Klammern im Code erzeugt werden. Sie werden immer evaluiert, indem das erste Element als Funktion und die weiteren Elemente als Argumente angesehen werden. Sollte die Liste als Literal angesehen werden, also nur als Liste von Elementen ohne anfängliche Funktion, muss die Liste mit Quotes vor der Evalierung geschützt werden. Vektoren: Vektoren garantieren einen nahezu konstanten Zugriff auf ihre Daten. Hierfür wird jedem Wert eine Zahl, beginnend bei 0, zugeordnet, mit welchem man auf ein Datum zugreifen kann. Sie werden durch eckige Klammern erzeugt. Maps: Maps werden für das Speichern assoziativer Schlüssel-Wert-Paare in Clojure genutzt. Es gibt zwei Arten. Zum einen die “Hash-Maps”, bei denen die Reihenfolge der Elemente nicht festgelegt ist, und die “Sorted-Maps”, bei denen die Elemente nach dem Schlüssel sortiert sind. Die Hash-Maps haben eine schnellere Zugriffszeit als die sortierten. Erzeugt werden sie durch geschweifte Klammern (Hash-Maps) oder den Funktionen hash-map und sorted-map. Mit der Funktion get kann man den Wert zu einem zugehörigen Schlüssel ermitteln. Sets: Sets sind eine Sammlung von Werten ohne Duplikate. Wie bei den Maps stehen wieder zwei Sorten von Sets zur Verfügung. Erzeugt werden sie mit den Funktionen hash-set und sorted-set. Hash-sets können auch durch #{} erzeugt werden. Bei der Erzeugung als Literal oder mit der hash-set-Funktion wird bei doppelten Einträgen eine IllegalArgumentException geworfen. Wenn jedoch ein Set aus einer anderen Datenstruktur erzeugt wird, werden Duplikate entfernt. 6 3 Sprache Abbildung 3.2: Definition der Datenstrukturen Die wichtigste Eigenschaft von Clojures Datenstrukturen sind deren Unveränderlichkeit und Persistenz. Somit können einmal erzeugte Strukturen nicht mehr verändert werden. Fügt man einer Liste ein neues Element hinzu, dann wird eine neue Liste erzeugt. Das neue Element enthält dann eine Referenz auf die alte Liste. Wenn alte Versionen der Datenstruktur nicht mehr benötigt werden, werden sie von der Garbage-Collection der JVM beseitigt. 3.2.3 Allgemeine Struktur Clojure-Code selbst besteht aus Listen, welche wiederum Listen und andere Datenstrukturen enthalten können. Wenn eine Funktion also evaluiert wird, wird das erste Argument als Funktion und alle weiteren als Parameter angesehen. Somit ergibt sich eine Präfixnotation. Wenn wir also das Beispiel des einfachen Hello-World-Programms aus Abbildung 3.3 betrachten, sehen wir eine Liste, welche als erstes Element die Funktion println und als zweites das Argument der Funktion, einen String, enthält. Im zweiten Beispiel wird eine einfache Funktion square definiert. Der Ausdruck def weist dem ersten Argument das zweite Argument zu. Dieses ist wiederum eine Liste, in der eine Funktion einen Vektor mit Parameter erhält. Das letzte Argument einer Funktion ist immer der Rückgabewert. In diesem Fall also das Ergebnis der Multiplikation. 3.2.4 Sequences Sequences sind eine Abstraktion der Zugriffsmechanismen von Listen, Vektoren, Maps, Sets, Strings und Bäumen. Des Weiteren können alle Collections aus Java damit an- 7 3 Sprache Abbildung 3.3: Einfache Beispiele gesprochen werden. Somit haben alle dieselben vier Funktionen für den Zugriff auf die Sequence. Mit first wird das erste Element ausgegeben, mit rest und next die restliche Sequenz. Hierbei unterscheidet sich das Verhalten am Ende der Sequence. Während rest nil zurückliefert und sich somit für logische Überprüfungen eignet, liefert next eine leere Liste zurück. Mit cons kann ein Element an die erste Stelle der Sequence angefügt werden. Abbildung 3.4: Sequence-Funktionen auf einer Liste 3.2.5 Kontrollstrukturen Kontrollstrukturen dienen dazu bestimmte Ausdrücke nur unter bestimmten Bedingungen auszuführen. Die bekannteste und einfachste Form ist die If-Anweisung, welche in Clojure folgende Struktur hat: (if (Bedingung) <Ausdruck 0> <Ausdruck 1>). Sollte die Bedingung true ergeben, wird <Ausdruck 0> ausgeführt, sonst <Ausdruck 1>. Abbildung 3.5 demonstriert ein einfaches Beispiel bei dem die größere von zwei Zahlen ausgegeben wird. 8 3 Sprache Abbildung 3.5: Einfache If-Anweisung Eine weitere häufig genutzte Kontrollstruktur ist cond. Dieses nimmt eine oder mehrere <Test> <Ausdruck> Paare und evaluiert einen Test nach dem Anderen. Wenn ein Test true ergibt, wird der dazugehörige Ausdruck ausgewertet und der Vorgang abgebrochen. In Abbildung 3.6 wird auf diese Weise einer Temperatur eine Beschreibung zugeordnet. Abbildung 3.6: Einfache cond-Anweisung Um in Clojure Schleifen zu implementieren, existieren die Ausdrücke loop[bindings*] <Ausdruck>*),welcher Symbolen einen Wert zuweist, und recur(<Ausdruck>*). recur springt an eine durch loop definierte Stelle (siehe Abbildung 3.7 Zeile 14) und verändert die Variablenbindungen. Dieser Operator darf nur an einer Rückgabestelle einer Funktion oder eines loop-Ausdruckes stehen. In Clojure können Rekursionen auch durch den Aufruf der Funktion innerhalb dieser erzeugt werden. Bei dieser Variante wird der Stack der JVM genutzt und ist somit beschränkt. Bei recur hingegen ist die Rekursionstiefe unbeschränkt. Abbildung 3.7: Beispiel für loop und recur 3.2.6 Makros Makros ermöglichen Programmierern eine Sprache um eigene Konstrukte zu erweitern. Sie erlauben es, Code zu schreiben, welcher wiederum Code erzeugt, bevor der Compiler 9 3 Sprache seine Arbeit aufnimmt. Makros werden oft dort verwendet, wo Aufgaben häufig wiederholt werden. In Sprachen ohne Makros wären Veränderungen am Sprachkern nötig. Der Operator defn ist ein Beispiel für ein solches Makro. In diesem Fall wurden die Operatoren def und fn miteinander kombiniert. Makros lassen sich, ähnlich wie Funktionen, mit defmacro implementieren. In Abbildung 3.8 sieht man die Implementierung von defn, welches einen Namen, Parameter und eine Funktionsdefinition erhält. Mit dem ListOperator und dem Quote wird eine Liste erzeugt, welche den späteren Code darstellt. Abbildung 3.8: Implementierung von defn 3.2.7 Funktionen höherer Ordnung Wie schon erwähnt, sind Funktionen in Clojure auch nur gewöhnliche Datentypen und werden somit wie Strings oder Zahlen gehandelt. Somit können Funktionen anderen Funktionen als Parameter übergeben werden. Funktionen, welche andere als Parameter entgegennehmen oder als Ergebnis liefern nennt man Funktionen höherer Ordnung. Ein allseits bekanntes Beispiel für eine solche Funktion ist map. Diese bekommt als ersten Parameter eine Funktion und als zweiten eine Liste von Elementen. Die übergebene Funktion wird auf jedes Element der Liste angewendet. Als Ergebnis erhält man die Liste der Ergebnisse. Im unteren Beispiel verwenden wir die schon implementierte SquareFunktion mit map und erhalten die erwartete Liste der Quadratzahlen von 3 bis 9. Abbildung 3.9: Verwendung von map 3.2.8 Closures Unter einem Closure oder einem Funktionsabschluss versteht man eine Funktion, welche sich den Variablenkontext der Erzeugerfunktion bewahrt, obwohl diese außerhalb des lexikalischen Geltungsbereiches liegen. Closures sind in Lisp so wichtig, dass Hickey Clojure nach ihnen benannt hat. Im folgenden Beispiel wird eine Funktion definiert, welche einen konstanten Wert auf einen anderen Wert addiert. Hierfür liefert die Funktion eine Funktion zurück, welche den zu addierenden Wert erhält. 10 3 Sprache In der REPL wird der Rückgabewert an eine Variable gebunden, welche daraufhin wie eine Funktion genutzt werden kann. Die Rückgabefunktion enthält hierbei immer noch den Wert von const, obwohl der Geltungsbereich von addierer schon verlassen wurde. 3.2.9 Laziness Clojures Sequences haben die Besonderheit, dass deren Elemente unter bestimmten Umständen erst bei Zugriff realisiert werden. Diese als “Laziness” bekannte Eigenschaft wirkt sich positiv auf die Performance aus. Bei der Fehlersuche macht sich Laziness aber eher negativ bemerkbar, da die Fehler auf den Zeitpunkt der Berechnung beruhen und somit die Suche nach diesen anders angegangen werden muss. Solche Lazy-Sequences werden von manchen Funktion automatisch erstellt, sobald sie eine Sequence zurückliefern. Ansonsten können sie mit dem Makro lazy-seq erzeugt werden. Funktionen welche Laziness unterstützen, sollten frei von Seiteneffekten sein. Im unteren Beispiel wird eine Kombination der Funktionen take und range ausgeführt. Mit (range y) wird eine Liste mit Zahlen von 0 bis y erzeugt und mit (take x) werden die ersten x Elemente einer Sequence ausgegeben. Wenn man nur range mit einer solch großen Zahl in die REPL eingibt, ist diese etwas länger beschäftigt, bei der unteren Eingabe erscheint die Lösung sofort, da nur die benötigten Daten berechnet wurden. Aus Performancegründen geschieht dies aber nicht einzeln sondern in kleinen Blöcken. Abbildung 3.10: Zusammenspiel von take und range 3.2.10 Namespaces Namespaces dienen der Strukturierung von Variablen und Funktionen. Um Namenskollisionen vorzubeugen, ist jede Definition in einem Namensraum. Symbole welche nicht in ihrem eigenen Namensraum genutzt werden, müssen mit der Angabe ihres eigenen Namensraumes vor ihrem Namen benutzt werden. 11 4 Clojures spezielle Eigenschaften 4.1 Nebenläufigkeit Unter Nebenläufigkeit versteht man das parallel laufen mehrerer Threads, welche dabei um gemeinsame Ressourcen konkurrieren. Die bekanntesten Lösungen für solche Probleme sind Locking-Mechanismen wie Semaphoren. Hickey empfand solche aber als zu kompliziert und wollte lieber die Besonderheiten von Clojures Datenstruktur nutzen. Statt die Ressourcen zu blockieren, damit jeder Thread nacheinander auf sie zugreifen kann, soll jeder eine eigene Kopie erhalten auf der er erstmal arbeiten kann. Erst wenn die Veränderung den Hauptthread betrifft sollte synchronisiert werden. Hickey entwickelte ein sehr anschauliches Beispiel um Clojures Herangehensweise bei der Nebenläufigkeit zu demonstrieren, einen Geh-Wettbewerb. Die einzige wichtige Regel beim Sport Gehen ist, immer einen Fuß auf dem Boden zu haben. Sollten beide in der Luft sein, so ist dies Laufen und wird als Foul gewertet. Somit ergibt sich folgender Algorithmus: 1. Betrachte den ersten Fuß. 2. Ist dieser auf dem Boden, ist alles in Ordnung. 3. Ist dieser in der Luft, betrachte den zweiten Fuß. 4. Ist dieser auch in der Luft, liegt ein Foul vor. Die Schiedsrichter müssen stetig überprüfen ob die Regeln eingehalten werden. Das Problem hierbei ist, dass während unserer Messungen das Rennen weitergeht. So können wir bei der Messung des zweiten Fußes nicht nicht davon ausgehen, dass der erste Fuß noch in der Luft ist. Dies ist aber bei den heutigen Programmen der Fall, sie lassen die Zeit einfach still stehen solange sie nicht selbst voranschreiten. Hier ist eine Art Snapshot des Momentes erwünscht, anhand dessen wir in aller Ruhe die Situation auswerten können. Mit Locking-Mechanismen könnte man so einen Wettbewerb also nicht realitätsgetreu nachbilden, da der Geher ja nicht während des Rennens innehalten kann, damit die Regeln überprüft werden können. Clojure bietet Werkzeuge um genau diese Veränderung von Identitäten im Laufe der Zeit zu handhaben. In diesem Abschnitt gehe ich darauf ein wie Clojure dies ermöglicht. Software Transactional Memory (STM) Clojure bedient sich bei konkurrierendem Zugriff bei den, aus den Datenbanken bekannten, Transaktionen. Deren wichtigste Eigenschaft ist, dass sie entweder ganz oder gar nicht stattfinden. Bei Datenbanken weisen sie vier Eigenschaften auf: 12 4 Clojures spezielle Eigenschaften A Sie sind atomar. Das heißt, dass unabhängig davon wie viele Daten verändert werden, von außen betrachtet, alles in einem Schritt abläuft. C Sie sind konsistent. Alle Daten, welche an einer Transaktion beteiligt sind, sind sowohl am Anfang als auch am Ende dieser konsistent. I Sie sind isoliert. Änderungen von Daten innerhalb einer Transaktion sind erst nach erfolgreicher Beendigung dieser von außen sichtbar. D Sie sind dauerhaft. Das bedeutet, dass Daten nach Beendigung der Transaktion auf einem Medium gesichert und so vor Software- und Hardwarefehlern geschützt werden. Von diesen Eigenschaften kann man die ersten drei zur Manipulation flüchtigen Speichers übertragen. Dies geschieht durch die im nächsten Unterabschnitt erläuterten Referenztypen. A, C und I werden durch die im nächsten Abschnitt vorgestellten Referenztypen erreicht. Das STM-Verfahren basiert auf einer Transaktionsmaschine, welche gleichzeitig laufenden Transaktionen jeweils einen eigenen Zugriff auf die zu verändernden Daten gewährt. Sobald eine Transaktion fertig ist, entscheidet die Transaktionsmaschine was zu tun ist. Die Änderungen der ersten Transaktion werden für die anderen Programmteile manifestiert (Commit). Die anderen gleichzeitig laufenden Transaktionen müssen in der Regel ihre Arbeit nochmal beginnen. Dies erfordert Funktionen ohne Nebeneffekte. Die konkrete Implementierung eines solchen Transaktionssystems kann mit Locking-Mechanismen programmiert werden, dies ist aber dann nur das Problem des System-Entwicklers und nicht der Anwender. STM hat zwei wichtige Vorteile: 1. Es ist ein optimistischer Ansatz: Lesende Zugriffe werden nicht blockiert und alle schreibenden Transaktionen können bis zum Commit ihrer Arbeit nachgehen. 2. Das System ist einfacher zu handhaben, als z.B. Locking-Mechanismen. Man muss nur darauf achten, dass die Funktionen keine Nebeneffekte haben. Der entscheidende Nachteil an STM sind die höheren Anforderungen an Speicher und CPU, da jede Transaktion eine komplette Kopie der zu verändernden Daten haben muss und die zusätzlichen Wiederholungen der Transaktionen mehr Rechenleistung verbrauchen. Bei n gleichzeitig laufenden Transaktionen kann es somit zu n-fachen Bedarf an Speicher und Rechenleistung kommen. Referenztypen Wie im vorherigen Kapitel beschrieben, sind Clojures Datenstrukturen unveränderlich. Für diese existieren aber spezielle Referenztypen, welche Daten veränderlich machen. Hierbei werden die Daten selbst nicht geändert, sondern die Referenz von einem Wert auf einen anderen umgehängt. Aus Java importierte Objekte sind hierfür aber nicht geeignet. Es gibt vier verschiedene Referenztypen: Vars, Atoms, Refs und Agents. 13 4 Clojures spezielle Eigenschaften Var: Eine Var kann an einen beliebigen Wert, wie ein Datum oder eine Datenstruktur, gebunden werden. Der Unterschied zu solchen besteht darin, dass der Wert geändert werden kann. Jeder Thread erbt diese Assoziation, kann sie aber lokal überschreiben. Abbildung 4.1: Funktionen auf Vars Atom: Atome koordinieren den Zugriff mehrerer Threads auf eine Ressource, wobei eine Änderung der Referenz atomar ist. Der Zugriff auf den Wert erfolgt über das Dereferenzieren des Atoms. Abbildung 4.2: Funktionen auf Atoms Ref: Refs verhalten sich großteils wie Atome. Mithilfe der STM können mehrere Werte manipuliert werden. Jede Funktion, die eine Ref ändert, muss mit einem dosyncBlock umschlossen werden. Die Funktionen in diesem Block werden in der Reihenfolge ihrer textuellen Reihenfolge ausgeführt. Sollte ein anderer Prozess die Ref geändert haben, muss der ganze Transaktionsblock wiederholt und gemachte Änderungen verworfen werden. 14 4 Clojures spezielle Eigenschaften Abbildung 4.3: Funktionen auf Refs Im folgenden Beispiel wird der Geldtransfer zwischen Bankkonten anhand einer Ref dargestellt. Zu Beginn erzeugen wir Threads, welche aus Java übernommen werden, und Konten, diese sind Maps mit Kontostand und Namen. Daraufhin werden die Konten Refs zugewiesen. Als nächstes werden die Funktionen zum Überweisen, Abheben und Gutschreiben implementiert. Die Funktion assoc dient der Veränderung von Maps. In der Transfer-Funktion werden die beiden Konten im dosync-Block verändert. Beim Start des Programmes wird init-accounts aufgerufen und mit transferThread können Transaktionen zwischen den Konten vorgenommen werden. 15 4 Clojures spezielle Eigenschaften Agent: Agents bieten die Möglichkeit unkoordinierte, asynchrone Änderungen an Werten vorzunehmen. Verschiedene Threads senden ihre Informationen an einen separaten Thread, welcher diese verarbeitet. Der aufrufende Thread läuft hierbei weiter und wartet nicht auf die Auswertung. Umgesetzt wird dieser Vorgang mit der Funktion send, welche eine Funktion mit Argumenten nimmt und so den Agent verändert. Sollte der Hauptthread irgendwann die gesamten Daten aus dem Agent benötigen, kann man mit dem Befehl await darauf warten, dass alle Threads ihre Änderungen vorgenommen haben und dann fortfahren. Agents werden meistens genutzt, wenn die Zeit der Berechnung keine Rolle spielt. Abbildung 4.4: Funktionen auf Agents Im folgenden Beispiel wird die Vorgehensweise mit Agents verdeutlicht. Von einem Thread wird zehn mal die Länge eines Vektors von Personen überprüft, welcher durch einen Agent repräsentiert wird. Währenddessen erzeugt ein weiterer Thread diesen Vektor und schickt ihn an den Agent. Dies geschieht indem er mit doseq über die Liste von Personen iteriert und in actualPerson schreibt. Mit conj wird das Element dem Agent zugefügt. Aufgrund des asynchronen Verhaltens des Agents kann es zu verschiedenen Ausgaben kommen, wobei aber nach den zehn Durchgängen eigentlich immer die Anzahl vier beträgt. Auf Seiteneffekte sollte man eigentlich normalerweise verzichten, sind aber aufgrund des simplen Beispiels nicht hinderlich. 16 4 Clojures spezielle Eigenschaften Typ Var Atom Ref Agent Kontext lokal übergreifend übergreifend übergreifend Koordination eine Identität eine Identität mehrere Identitäten eine Identität Ausführung synchron synchron synchron asynchron Tabelle 2: Übersicht über die Referenztypen 4.2 Clojure und Java Clojure bietet die Möglichkeit, mit der Programmiersprache Java zu interagieren. Clojure ist in Java implementiert und wird somit wie eine Bibliothek betrachtet. Deshalb kann man sich Java in Clojure zu Nutze machen und Clojure in Java. 4.2.1 Java in Clojure In Abbildung 4.5 wird eine an die Funktion übergebene Zahl mit einer Zufallszahl multipliziert. Zu Beginn wird dafür die Klasse java.util.Random geladen. Daraufhin wird ein neues Objekt von Random instanziiert. Dies geschieht immer durch den Aufruf von new oder mit einem Punkt am Ende des Klassennamens. Dem Random-Objekt wird mit nextInt eine Zahl zwischen 0 und 20 zugeordnet. Der Punkt vor rand gibt an, dass wir mit dem Objekt arbeiten. 17 4 Clojures spezielle Eigenschaften Abbildung 4.5: Import einer Java-Klasse in Clojure 4.2.2 Clojure in Java Neben der Verwendung von Java in Clojure, kann auch Clojure-Code zu Klassen kompiliert werden, welche von anderen abgeleitet sind oder Interfaces implementieren. Natürlich können die auch für sich alleine stehen. Im oberen Teil der Abbildung 4.6 wird der Text, welchen die main-Funktion erzeugt, aus einem Java-Programm heraus ausgegeben. Zu Beginn wird der Namespace dieser Datei angegeben, unter welchem wir die Funktion dann später in Java importieren können. Der Ausdruck :gen-class sorgt dafür, dass beim späteren kompilieren der Namespace zu einer Klasse wird. Durch den Befehl compile wird die Kompilation ausgeführt. Das Programm muss dann noch in eine *.jarDatei exportiert und in das Javaprojekt importiert werden. Mit Leiningen und Eclipse ist ein leichter Umgang gewährleistet. Das Beispiel wurde auch mit Eclipse erzeugt. Die Anwendung der Funktion in Java sieht man im unteren Teil der Abbildung. Abbildung 4.6: Funktion in Clojure 18 5 Technische Unterstüzung Trotz des jungen Alters gibt es für Clojure schon eine Vielzahl von frei zugänglichen Entwicklungsumgebungen. Neben Erweiterungen für bekannte Entwicklungsumgebungen wie Eclipse[2] (CounterClockwise[3]) oder NetBeans[4] (Enclojure[5]) gibt es auch eine Hand voll REPLs. Mit den Erweiterungen der IDEs lässt sich rein theoretisch die Interaktion mit Java stark vereinfachen. Da z.B. Eclipse häufig ins Geschehen eingreift, wie z.B Autovervollständigung von Klammern am Ende, ist es manchmal einfach angenehmer an einer REPL mit wenig Eigenschaften zu arbeiten, wo der Programmierer noch selbst die Kontrolle über alles hat. An frei zugänglichen REPLs mangelt es auch nicht, aber die meisten Clojure-Entwickler greifen zu Leiningen, einem Build-Management-Tool, welches ähnlich wie Maven arbeitet. Es kümmert sich um Abhängigkeiten und vereinfacht das Erstellen und Testen von Projekten. Mit der JVM hat Clojure ein mächtiges Werkzeug geerbt. Zum Einen läuft Clojure unter der Verwendung der JVM auf jedem System, welches Java unterstützt. Zum Anderen übernimmt es wichtige Bestandteile Javas, wie Teile des Typsystems, die Garbage Collection, die Speicherverwaltung sowie die Threads. Neben der JVM gibt es aber auch Varianten von Clojure für andere Plattformen, es existieren Portierungen für die Common Language Runtime (CLR), Python, JavaScript und der ActionScript Virtual Machine. 19 6 Fazit Wenn man nichts allzu kompliziertes programmieren will, ist Clojure eine angenehme Sprache. Mit der Verwendung der REPL kann man stets und ohne Umwege ein Feedback des Programmes abfragen. Aber da Clojure viele Fähigkeiten hat, auf die ich in dieser Arbeit nicht eingegangen bin, kann diese Sprache auch sehr komplex werden. Ab diesem Zeitpunkt streiten sich auch die Gemüter über die Übersichtlichkeit dieser Sprache, da am Ende des Codes häufig eine Kette von Klammern steht. Manche finden diese Art der Ordnung übersichtlich, andere eher nicht. Ich finde es wie schon gesagt bei kleineren Programmen angenehm, wird aber schnell störend. Clojure ist noch eine junge Sprache und entwickelt sich mit einer hohen Geschwindigkeit weiter, deswegen kann man behaupten, dass noch nicht das ganze Potenzial dieser Sprache ausgeschöpft wurde. Mit seiner Art der Nebenläufigkeit und den Versuchen mit neuen Technologien wie STM hat sich Clojure zumindest schon mehr ins Rampenlicht gerückt als seine vielen Verwandten in der Familie der Lispdialekte. Trotz des geringen Alters hat sich schon eine gewisse Fangemeinde um Clojure gebildet, welche aber eher aus Hobby-Programmierern besteht. Ob Clojure sich in kommerziellen Projekten durchsetzen wird, ist jetzt noch nicht beantwortbar. Für mich bestehen da aber gewisse Zweifel, da es in der heutigen Zeit als Lisp schwer ist, sich zu etablieren. Fachleute auf dem Bereich der Lisp-Programmierung bleiben lieber bei etablierten Varianten wie Common-Lisp und Programmierer anderer Sprachen können sich selten mit Lisp anfreunden. Im Grunde wird Clojure somit wahrscheinlich eher eine Art interessantes Spielzeug bleiben, mit welchem man im Bereich der Nebenläufigkeit attraktive Untersuchungen durchführen kann. Vielleicht wird diese Sprache auch irgendwann in der Lehre verwendet, wie ihr Cousin Scheme. Ich vermute aber, dass Clojure uns auf irgendeine Art und Weise lange Zeit erhalten bleibt. 20 Literaturverzeichnis [1] Leiningen. Website. Online unter http://leiningen.org/; Stand: 05.02.2013. [2] Eclipse. Website. Online unter http://www.eclipse.org/; Stand: 05.02.2013. [3] Counterclockwise. Website. http://code.google.com/p/counterclockwise/l; Stand: 26.01.2013. [4] NetBeans. Website. Online unter http://netbeans.org/; Stand: 05.02.2013. [5] Enclojure. 02.02.2013. Website. Online unter http://enclojure.wikispaces.com/; Stand: [6] S. Kamphausen; T. O. Kaiser. Clojure: Grundlagen, Concurrent Programming, Java. dpunkt.verlag, 2010. [7] Clojure.org. Website. http://clojure.org/; Stand: 09.10.2012. [8] Clojure Dokumentation. Website. http://clojuredocs.org/; Stand: 18.11.2012. [9] B. Neppert. Clojure unter der lupe. Website. http://itrepublik.de/jaxenter/artikel/Clojure-unter-der-Lupe-4072.html; Stand: 20.01.2013. [10] S. Tilkov. Clojure: Ein pragmatisches Lisp für die JVM. Website. http://itrepublik.de/jaxenter/artikel/Clojure-unter-der-Lupe-4072.html; Stand: 28.01.2013. [11] Clojure. Website. 20.01.2013. Online unter http://en.wikipedia.org/wiki/Clojurel; Stand: 21