FAKULTÄT FÜR INFORMATIK

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