Clojure - AG Programmiersprachen und Übersetzerkonstruktion

Werbung
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
Herunterladen