FAKULTÄT FÜR INFORMATIK DER TECHNISCHEN UNIVERSITÄT MÜNCHEN Höllische Programmiersprachen Masterseminar im Wintersemester 2014/15 Mutable vs. Immutable Datentypen, Pure vs. Impure Bearbeiter: Eva Heckmeier Betreuer: Stefan Schulze Frielinghaus 20. Januar 2015 Zusammenfassung Die Konzepte von “Immutable” Datentypen und “Pure Functions” ermöglichen fehlerfreies paralleles Ausführen von Programmteilen. Der Einsatz von geeigneten Maßnahmen ermöglicht diese Parallelität auch bei veränderbaren Datentypen. Eine solche Maßnahme ist das Modell des Software Transactional Memory. Parallelität gewinnt aufgrund der derzeitigen Entwicklung immer mehr an Bedeutung. Inhaltsverzeichnis 1 Einleitung 1 2 Betrachtete Grundkonzepte 2.1 Mutable vs. Immutable . . . . 2.1.1 Bedeutung . . . . . . . 2.1.2 Vor- und Nachteile . . 2.2 Pure vs. Impure . . . . . . . . 2.2.1 Bedeutung . . . . . . . 2.2.2 Vor- und Nachteile . . 2.3 Zusammenhang der Konzepte 1 1 1 1 2 2 2 3 3 . . . . . . . . . . . . . . . . . . . . . Einsatz der betrachteten Grundkonzepte in Java und Clojure 3.1 Grundlegende Verwendung . . . . . 3.2 Implementierungsunterschiede . . . 3.2.1 Programmbeispiel Java . . . 3.2.2 Programmbeispiel Clojure . 3.3 Zusammenspiel von Java und Clojure . . . . . . . . . . . . . . . . . . . 3 3 4 4 4 5 4 Software Transactional Memory 4.1 Ursprung und Einsatzziel . . . . . . 4.2 Funktionsweise . . . . . . . . . . . . 4.3 Vor- und Nachteile . . . . . . . . . . 6 6 7 7 5 Fazit und Ausblick 8 1 Einleitung Mehrkernprozessoren sind in heutigen Systemen nicht mehr wegzudenken. In der Vergangenheit wurde in erster Linie durch immer höhere Taktfrequenzen höhere Rechenleistung erzielt. Im Moment ist der Trend nach immer mehr CPU-Kernen vorherrschend, um den immer höheren Anforderungen an Rechenleistung gerecht zu werden. Heutige Systeme sollen viele Aufgaben gleichzeitig behandeln, wobei diese jedoch häufig auf gleiche Ressourcen zugreifen müssen. Dadurch ist es nicht mehr selbstverständlich, dass diese Aufgaben auf den unterschiedlichen Kernen wirklich parallel ohne Fehler ablaufen können. Im Folgenden werden die Grundkonzepte von “Immutable Objects” sowie “Pure Functions” als Lösungsansätze für diese Problematik betrachtet. Zunächst werden die Begriffe geklärt und die Vor- und Nachteile sowie der Zusammenhang der beiden Grundkonzepte aufgezeigt. Im Anschluss wird am Beispiel von Java und Clojure der Einsatz der beiden Konzepte von der grundlegenden Verwendung bis hin zur konkreten Implementierung veranschaulicht. Daraufhin wird noch auf den Software Transactional Memory als Möglichkeit eingegangen, um mit veränderlichen Daten umzugehen. Abschließend folgt ein kurzes Fazit, inwieweit diese Grundkonzepte als sinnvolles Werkzeug eingesetzt werden können. 2 2.1 2.1.1 Betrachtete Grundkonzepte Mutable vs. Immutable Bedeutung Zunächst wird das Grundkonzept der Unveränderlichkeit betrachtet. “Immutable Objects” beziehungsweise unveränderbare Objekte sind solche, die nachdem sie erzeugt wurden, nicht mehr verändert werden können. Das heißt, dass immer wenn ein anderer Wert, also eine Änderung gewünscht ist, ein neues Objekt erzeugt werden muss. Falls also zum Beispiel eine unveränderbare Liste existiert, muss, sobald ein neues Objekt dieser hinzugefügt werden soll, eine neue Liste erstellt werden. Bei “Mutable Objects” beziehungsweise veränderbaren Objekten ist dies nicht nötig, da Änderungen an den bestehenden Objekten möglich sind.[6] 2.1.2 Vor- und Nachteile Zunächst kann die eben bereits erwähnte Tatsache, dass bei Änderungen ein Objekt komplett neu erzeugt werden muss, als Nachteil angesehen werden. Für dieses Problem gilt es geeignete Maßnahmen zu ergreifen, um beispielsweise nicht bei jedem Hinzufügen eines Listenelementes eine unter Umständen sehr große Liste immer wieder neu erzeugen zu müssen und damit unnötig Speicherplatz zu verbrauchen und die Performance negativ zu beeinflussen. Um akzeptable Performance zu erreichen, gibt es geeignete Konzepte, wie zum Beispiel “Shared 1 Structures”. Bei diesem Konzept werden Datenteile, an denen keine Änderungen durchgeführt werden, nicht mit kopiert. Realisiert wird dies beispielsweise mittels Bäumen und entsprechenden Teilbäumen. Der angesprochene Nachteil erweist sich zugleich jedoch auch als Vorteil insofern, als dass dadurch persistente Datenstrukturen, im Sinne von Datenstrukturen, von denen nach einer Manipulation immer noch die vorherige Version verfügbar ist, effizient zu implementieren und organisieren sind. Um ungewollte Manipulation zu vermeiden und dadurch Sicherheit zu gewährleisten, ist dies ein guter Ansatzpunkt. Diese persistenten Datenstrukturen sind außerdem die Grundlage für den bereits erwähnten Software Transactional Memory, mit welchem fehlerfreie Parallelität auch bei nötiger Datenveränderung möglich ist. Dies weist zu einem weiteren Vorteil von unveränderbaren Objekten nämlich, dass diese threadsicher sind, das heißt, dass Parallelität sehr einfach ohne Synchronisationsschwierigkeiten möglich ist. Dies basiert darauf, dass der klassische Fehler nicht vorkommen kann, dass zwei Akteure Änderungen an einem Konto vornehmen wollen und beide gleichzeitig den Ursprungskontostand lesen, auf diesem basierend ihre Änderungen vornehmen und eine Änderung verloren geht, da eben keine Änderungen möglich sind.[4] Weitere Vorteile sind zum einen, dass unveränderbare Objekte ohne Probleme zu cachen sind und dass sie einfacher zu konstruieren, benutzen und vor allem zu testen sind, da sie sich immer in ihrem konsistenten Zustand befinden.[5] 2.2 2.2.1 Pure vs. Impure Bedeutung “Pure Functions” beziehungsweise reine Funktionen zeichnen sich durch zwei Eigenschaften aus. Zum einen verursachen sie keine Seiteneffekte, wie Ausgaben oder Veränderungen an “Mutable Objects”, die bei “Impure Functions” beziehungsweise unreinen Funktionen durchaus erlaubt sind.[2] Zum anderen folgen sie dem Prinzip der referentiellen Transparenz, was bedeutet, dass eine reine Funktion nur von ihren Eingabeargumenten abhängt und dadurch bei gleicher Eingabe immer zum gleichen Ergebnis führt. Bei unreinen Funktionen ist es jedoch möglich, dass das Ergebnis beispielsweise von internen Zuständen oder Benutzereingaben, also von der “Außenwelt” abhängen kann und dadurch die Funktion nicht bei gleicher Funktionsparameter Eingabe das gleiche Ergebnis liefert.[1] 2.2.2 Vor- und Nachteile “Pure Functions” weisen viele Vorteile auf. So sind zum Beispiel Fehler ausgeschlossen, die auf unerwünschten Seiteneffekten basieren. Oft sind Seiteneffekte jedoch sogar erwünscht, wodurch unreine Funktionen nötig werden. Dies gilt beispielsweise wenn Ein- oder Ausgaben erwünscht sind oder das Speichern von Daten nötig ist. Ein kompletter Ausschluss ist deshalb oft nicht möglich. Reine Funktionen jedoch sind aufgrund der strikten Vermeidung von Seiteneffekten 2 parallelisierbar. Ein weiterer Vorteil besteht darin, dass reine Funktionen leichter zu analysieren und zu testen sind. Dies gründet darauf, dass sicher ist, dass bei bestimmten Eingaben immer die jeweilig gleichen Ausgaben folgen, sich die Funktion also deterministisch verhält. Dies hat außerdem zur Folge, dass keine Änderungen an Datenbeständen betrachtet werden müssen, sondern rein die Ausgabe der Funktion richtig zur jeweiligen Eingabe passen muss. Durch den strikten Determinismus ist es zudem möglich, die Ergebnisse der Funktionen zu cachen, damit beispielsweise aufwendige Rechnungen mit den gleichen Eingabewerten jeweils nur einmal durchgeführt werden müssen.[3] 2.3 Zusammenhang der Konzepte Es wurde bereits deutlich, dass sich die Vorteile der beiden betrachteten Grundkonzepte stark überschneiden, jedoch einmal bezogen auf Objekte und einmal bezogen auf Funktionen. Wenn beide Konzepte strikt eingesetzt werden, eignet sich dies beispielsweise sehr gut für die Berechnung von komplexen rechenaufwändigen mathematischen Aufgaben, unter anderem aufgrund der möglichen Parallelität und Erleichterung des Testens. Für viele Anwendungen ist dies jedoch nicht genug, dort sind beispielsweise Seiteneffekte gewünscht. Es gibt Ansätze die Vorteile der Konzepte durch ihren Einsatz an geeigneten Stellen zu nutzen. Bei verschiedenen Programmiersprachen geschieht dies in unterschiedlichem Maße. Exemplarisch werden nun Java und Clojure betrachtet.[3] 3 3.1 Einsatz der betrachteten Grundkonzepte in Java und Clojure Grundlegende Verwendung Bei der Programmiersprache Java handelt es sich um eine objektorientierte Programmiersprache. Clojure dagegen ist eine funktionale Programmiersprache, die in erster Linie zur vereinfachten Parallelisierung von Programmteilen und deren gemeinsamer Nutzung von Ressourcen entwickelt wurde. Bei Clojure handelt es sich jedoch nicht um eine rein funktionale Programmiersprache, das heißt, sie lässt auch “Impure Functions” zu. Konkret erlauben sowohl Clojure als auch Java Funktionen mit Seiteneffekten. Jedoch sind bei Clojure, im Gegensatz zu Java, Funktionen mit Nebeneffekten die Ausnahme. Ohne Seiteneffekte wären jedoch weder Eingaben noch Ausgaben möglich. Bei der Betrachtung des zweiten behandelten Grundkonzepts, der Unveränderlichkeit, weisen die beiden Programmiersprachen einen noch größeren Unterschied auf. Bei Java gibt es sowohl “Immutable Objects” als auch “Mutable Objects”, wobei veränderliche Objekte gängig sind. Clojures Datenstrukturen hingegen sind alle unveränderbar mit Ausnahme von Referenztypen und aus Java importierten Objekten. Manipulationen abzubilden ist deshalb aufwendiger. Darauf ist noch genauer im Abschnitt Software Transactional Memory einzugehen. Zunächst wird nun die unterschiedliche Implementierung beispielhaft aufgezeigt.[4] 3 3.2 3.2.1 Implementierungsunterschiede Programmbeispiel Java In Java gibt es zum einen “Pure Functions”, wie zum Beispiel die folgende Funktion der Klasse Math zum Ermitteln des Maximums zweier Integer Zahlen. i n t maximum = Math . max( i n t x , i n t y ) Und zum anderen gibt es die dominierenden “Impure Functions”, wie zum Beispiel folgende Funktion der Klasse ArrayList zum Hinzufügen eines neuen Elements zu einer Liste, wobei eine Änderung an einer bestehenden Liste durchgeführt wird. Somit ist dies gleichzeitig ein Beispiel für “Mutable Objects” in Java. Wie auch in diesem Beispiel würden sich zwar veränderbare Objekte leicht in unveränderbare Objekte umwandeln lassen, indem jedes Mal ein verändertes neues Objekt zurückgegeben werden würde. Jedoch müssten dann geeignete Maßnahmen gegen Performance Einbußen getroffen werden, vor allem bei Beispielen wie Listen, die äußerst groß werden können. A r r a y L i s t l i s t 1 = new A r r a y L i s t ( ) ; l i s t 1 . add ( " ( 1 2 3 ) " ) ; System . out . p r i n t l n ( l i s t 1 . t o S t r i n g ( ) ) ; Die resultierende Ausgabe ist (1 2 3) Zur Betrachtung eines “Immutable Object” wird die “Immutable Class” String, die wohl bekannteste unveränderliche Klasse in Java, verwendet. Bei dem folgenden Beispiel wird die Methode “public String substring (int beginIndex)” auf einem String “string1” ausgeführt, welche einen neuen String zurückgibt, der als Wert einen Teil des “string1” hat und zwar alle Buchstaben ab der in “beginIndex” angegebenen Stelle. Der neue String wird in “string2” gespeichert und “string1” bleibt unverändert. S t r i n g s t r i n g 1 = new S t r i n g ( " Guten Morgen ! " ) ; String string2 = string1 . substring (6); System . out . p r i n t l n ( s t r i n g 1 ) ; System . out . p r i n t l n ( s t r i n g 2 ) ; Die resultierende Ausgabe ist somit Guten Morgen ! Morgen ! Die Verwendung der Konzepte in Clojure wird im folgenden Abschnitt behandelt.[6] 3.2.2 Programmbeispiel Clojure In Clojure sind die meisten Funktionen “pure” und die meisten Datenstrukturen “immutable”, dazu wird folgendes einfaches Beispiel betrachtet: 4 user> ( def l i s t 1 ’(2 3)) #’ u s e r / l s t 1 user> ( conj l i s t 1 1) (1 2 3) user> l i s t 1 (2 3) Hierbei wird zunächst eine Liste erstellt mit den Elementen “2” und “3”. Anschließend wird die Funktion “conj” aufgerufen, welche als erstes Argument eine Liste bekommt und als zweites Argument ein neues Listenelement, welches vor die angegebene Liste gehängt wird, in diesem Fall die “1”. Als Ergebnis wird die Liste “(1 2 3)” geliefert, wobei es sich um eine neue Liste handelt. Dies lässt sich auf die Unveränderlichkeit von Datenstrukturen in Clojure zurückzuführen. Damit gibt der Aufruf der Liste immer noch den zu Beginn initialisierten Wert zurück. Auch das Grundkonzept der “Pure Functions” ist erfüllt, jedoch kann dies durch Hinzunahme der folgende Programmzeile leicht geändert werden. user> ( p r i n t l n l i s t 1 ) Nun wurde eine Ausgabe hinzugefügt, dadurch handelt es sich nicht mehr um eine “Pure Function”, weil ein Seiteneffekt vorliegt. Clojure implementiert vier sogenannte Referenztypen, nämlich “Var”, “Atom”, “Ref” und “Agent”, wobei es sich um veränderliche Typen handelt. “Vars” sind zwar veränderlich, gelten jedoch jeweils nur innerhalb eines Threads, wodurch konkurrierende Zugriffe ausgeschlossen sind. Deshalb ergeben sich keine Probleme. “Vars” werden nicht explizit dereferenziert im Gegensatz zu den anderen 3 Referenztypen. “Atoms” können einen beliebigen einzelnen Wert aufnehmen und koordinieren diesen thread-übergreifend, wobei Änderungen in anderen Threads sichtbar sind. “Agents” koordinieren ebenfalls einen Wert, jedoch asynchron, wodurch die Fehlerbehandlung aufwändiger ist. “Refs” ermöglichen die konsistente Manipulation mehrerer Werte, ermöglicht durch den bereits mehrfach erwähnten Software Transactional Memory. Die folgende Tabelle zeigt eine zusammenfassende Gegenüberstellung der Referenztypen mit ihren jeweiligen Eigenschaften.[4] 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 1: Referenztypen Clojure [4] 3.3 Zusammenspiel von Java und Clojure In Java operieren häufig sogenannte Methoden, anstatt reiner Funktionen, auf zu ändernden Zustandsvariablen von Objekten. Dabei ergeben zwei Aufrufe nicht 5 immer das gleiche Ergebnis. Dies bedeutet, dass diese nicht nur von den eingegebenen Funktionsparametern abhängen. Der Ansatz von Clojure, von unveränderlichen Werten auszugehen und Ausnahmen bei parallelem Zugriff geeignet zu synchronisieren, erlaubt dagegen fehlerfreie parallele Programmausführung und erleichtert formale Verifikation und Analyse. Die Unveränderlichkeit der Werte hat jedoch auch einen Nachteil, nämlich dass der resultierende Kopieraufwand zwar durch geeignete Methoden verringert werden kann aber trotzdem nicht vernachlässigbar klein bleiben muss. Somit haben beide Ansätze, sowohl der objektorientierte Ansatz der Zustandsübergänge, wie auch der Ansatz der funktionalen Programmierung mit unveränderlichen Werten, ihre Berechtigung. Sie müssen beide zur Problemstellung passend eingesetzt werden. Mit Clojure und Java ist das sehr gut möglich, da es möglich ist beide Vorteile zu verbinden, indem sowohl Clojure Code in Java Programme integriert werden kann, als auch Java Bibliotheken in Clojure Programmen genutzt werden können.[3] 4 4.1 Software Transactional Memory Ursprung und Einsatzziel Grundsätzlich stellt das Ändern von veränderbaren Datenstrukturen kein Problem dar. Erst wenn mehrere Threads auf die gleichen Daten zugreifen, während sich diese in einem inkonsistenten Zustand befinden, können Fehler entstehen. Um das Problem der konkurrierenden Zugriffe auf gemeinsame Ressourcen zu lösen, gibt es neben dem klassischen Locking weitere Modelle. Hier wird im folgenden genauer auf das Modell des Software Transactional Memory (kurz STM) eingegangen. Gängig ist das transaktionale Modell für Datenbanken. Dabei werden Datenzugriffe und Änderung mittels Transaktionen verwaltet, welche sich durch ihre grundlegende Eigenschaft auszeichnen, dass sie ganz oder gar nicht durchgeführt werden. Transaktionen im Datenbankbereich folgen den folgenden vier Eigenschaften: A - “atomic” (sie sind atomar, das heißt, alle Änderungen finden zusammen in einen Schritt statt oder gar nicht) C - “consistent” (sie sind konsistent und die Daten, auf welche zugegriffen wird, sind am Anfang und Ende der Transaktion in einem konsistenten Zustand) I - “isolated” (sie sind isoliert, das heißt, Änderungen der Transaktion sind erst nach deren Abschluss sichtbar) D - “durable” (sie sind dauerhaft, das heißt, nach dem Abschluss einer Transaktion sind die Änderungen sicher auf einem Medium gespeichert). Die letzte Eigenschaft entfällt bei dem STM, doch die restlichen Eigenschaften können auf den STM, auf welchen im Folgenden noch näher eingegangen wird, übertragen werden.[4] 6 4.2 Funktionsweise Das Modell des STM erlaubt zunächst allen für die Parallelisierung relevanten Transaktionen gleichzeitig jeweils einen eigenen Zugriff auf die benötigten Daten. Alle Transaktionen dürfen solange sämtliche Lese- und Schreibzugriffe ausführen, bis eine Transaktion signalisiert, dass sie nun committen möchte. Dann werden ihre Daten-Manipulationen auch für anderen Programmteile manifestiert und sie kann beendet werden. Es kann von parallelen Transaktionen immer nur eine den abschließenden Commit durchführen und zwar jene, die zuerst ihren Commit durchführen möchte. Die übrigen Transaktionen müssen üblicherweise erneut gestartet werden. Damit dies möglich ist, müssen alle Funktionen, die in Transaktionen durchgeführt werden, zurücksetzbar sein. Im Normalfall bedeutet dies, dass sie keine Seiteneffekte haben dürfen. Dies erfüllen die betrachteten “Pure Functions”. In Clojure gibt es eine sehr einfache Syntax für Transaktionen. Exemplarisch betrachten wir Transaktionen in denen “Refs” geändert werden. Die Syntax für solche Transaktionen besteht nur aus der Funktion “dosync”, wobei der Inhalt des Rumpfes in einer Transaktion ausgeführt wird. Der Programmierer muss sich nicht weiter um die Synchronisation kümmern. [4] Abbildung 1 zeigt einen möglichen Verlauf von zwei Transaktionen. Die Transaktion 1 wird dabei zunächst umsonst ausgeführt, da die Transaktion 2 als erste committen möchte und deshalb die Funktion 1 erneut starten muss. Erst bei dem abschließenden Commit werden die Änderungen an den “Refs” durchgeführt, die für alle Programmteile sichtbar sind.[3] Abbildung 1: Veränderungen von Refs in Clojure mittels STM [3] 4.3 Vor- und Nachteile Ein großer Vorteil des STM-Modells ist, dass es sowohl einfach zu verstehen als auch für den Programmierer einfach zu handhaben ist. Dadurch, dass die konkrete Implementierung beispielsweise mittels Locking für den Programmie7 rer verborgen ist, muss sich dieser mit der Synchronisationsproblematik nicht weiter auseinandersetzen. Der Programmierer muss sich nur an die Vorgabe halten, dass Funktionen, die in Transaktionen genutzt werden, keine Seiteneffekte zur Folge haben, dies erfüllem die behandelten “Pure Functions”. Ein weiterer Vorteil ist, dass ein höherer Grad an Parallelisierung dadurch möglich ist, dass der STM im Vergleich zum klassischen Locking ein optimistischerer Ansatz ist. Dies ist dadurch begründet, dass das Transaktionssystem bis zur Commit-Phase keine Lese- oder Schreibzugriffe blockiert, solange das System keine möglichen Konflikte erkennt. Ein Nachteil ist dagegen, dass der Ablauf von Transaktionen vom Laufzeitverhalten abhängig ist und dadurch möglicherweise keine eindeutige Vorhersagbarkeit gegeben ist. Zudem besteht das Problem, dass durch STM erheblich höhere Anforderungen an Speicher und CPU herrschen. Es ist möglich, dass jede Transaktion eine eigene komplette Kopie der zu verändernden Daten benötigt, was bei n Transaktionen bis zu n-fachem Speicher- und Rechenleistungsbedarf führen kann. Hochgradig parallele und komplexe Anwendungen werden dadurch unmöglich.[4] 5 Fazit und Ausblick Die derzeitig Entwicklung zeigt, dass an Software immer höhere Anforderungen gestellt werden. Die Entwicklung der Hardware zu immer mehr Kernen ist nutzlos, wenn diese nicht genutzt werden können. Dazu ist auch eine geeignete Software nötig, welche die zur Verfügung gestellten Ressourcen richtig nutzen kann. Die betrachteten Grundkonzepte stellen eine gute Grundlage für die Schwierigkeiten dar, die sich aus der Nutzung von Parallelisierungsmöglichkeiten ergeben. Wo Parallelisierungen möglich sind, sollten diese genutzt werden. Dies ist mit dem entstehenden Mehrbedarf an Speicherkapazität abzuwägen, der zwar durch geeignete Organisationsstrukturen verringert wird, aber durch das Konzept der unveränderbaren Datenstrukturen zwangsweise entsteht. Vor allem die Verknüpfung von Java und Clojure, die es möglich macht, beide Programmiersprachen zu einer Programmeinheit zusammenzuführen, ist eine hervorragende Möglichkeit den bestmöglichen Nutzen aus der objektorientierten Programmiersprache Java und der funktionalen Programmiersprache Clojure zu ziehen. Dadurch besteht die Chance, je nach Aufgabe die Programmiersprache zu wechseln und die jeweiligen Vorteile zu nutzen beziehungsweise die jeweilige Nachteile zu umgehen. Die Modelle zum Umgang mit veränderlichen Daten, wie der hier betrachtete Software Transactional Memory, sind weiterhin Gegenstand aktueller Forschung. Für hochgradig parallele und komplexe Anwendungen gibt es teilweise noch immer nur unzureichende Lösungen. Die Notwendigkeit, Parallelität zu ermöglichen, ist allen Programmiersprachen gegeben. Die Entwicklung zeigt bereits, dass dies dem Programmierer durch geeignete Bibliotheken zunehmend erleichtert wird. Die weitere Entwicklung bleibt abzuwarten. 8 Literatur [1] David Sabel. Realisierung der Ein-/Ausgabe in einem Compiler fur Haskell bei Verwendung einer nichtdeterministischen Semantik, September 2003. [2] Dr. David Sabel. Einführung in die funktionale Programmierung. Technical report, Januar 2011. [3] Chas Emerick, Brian Carper, and Christophe Grand. Clojure Programming. O’Reilly Media, Inc., 2012. [4] Stefan Kamphausen and Tim Oliver Kaiser. Clojure - Grundlagen, Concurrent Programming, Java, volume 1. dpunkt.verlag, 2010. [5] Manfred Meyer. Java: Algorithmen und Datenstrukturen - Mit einer Einführung in die funktionale Programmiersprache Clojure. W3L-Verlag, 2012. [6] Walter Savitch and Kenrick Mock. ABSOLUTE JAVA, volume 4. Pearson Education, 2010. 9