Modernes Clojure. Einsatz und Bewertung moderner Sprachfeatures in Clojure


Masterarbeit, 2013

98 Seiten, Note: 1,0


Leseprobe


Inhaltsverzeichnis

1 Einleitung
1.1 Motivation
1.2 Zielstellung

2 Grundlagen der Funktionalen Programmierung
2.1 Eigenschaften Funktionaler Programmierung
2.1.1 Werte und veränderliche Objekte
2.1.2 First-Class, High-Order und Reine Funktionen
2.1.3 Collections und Datentypen
2.2 Programmierparadigmen
2.2.1 Imperative und Prozedurale Programmierung1
2.2.2 Objektorientierte Programmierung
2.2.3 Deklarative Programmierung
2.2.4 Weitere Programmierparadigmen
2.3 Funktionale Idiome
2.3.1 Lambda und Anonyme Funktionen
2.3.2 Closures
2.3.3 Currying
2.3.4 Memoization
2.4 Zusammenfassung

3 Clojure-spezifische Sprachmerkmale
3.1 Makros
3.1.1 Makros in anderen Sprachen
3.1.2 Makros in Clojure
3.1.3 Quote und Syntax-Quote
3.1.4 Hygiene
3.2 Datentypen und Protokolle
3.2.1 Definition von Protokollen
3.2.2 Erweitern von Datentypen
3.2.3 Benutzerdefinierte Typen
3.2.3.1 defrecord
3.2.3.2 deftype
3.2.3.3 Inline Implementierung von Protokollen
3.3 Multimethoden
3.3.1 Mutlimethoden Type-Dispatch
3.3.2 Hierarchien
3.4 Ausnahmebehandlung
3.5 Clean Code und Entwurfsmuster
3.5.1 Dependency Injection
3.5.2 Strategy Pattern
3.6 Zusammenfassung

4 Sprachfeatures zur nebenläufigen Programmierung
4.1 Grundlagen paralleler Programmierung
4.1.1 Multitasking und Multithreading
4.1.2 Schwierigkeiten der parallelen Programmierung
4.1.3 Klassische Lösung der Threading-Probleme
4.2 Zustand und Identität
4.3 Referenztypen
4.3.1 Referenztyp var
4.3.2 Referenztyp atom
4.3.3 Referenztyp ref und Software Transactional Memory
4.3.4 Referenztyp agent
4.4 Change Tracking und Validierung von Referenztypen
4.5 Futures
4.6 Zusammenfassung

5 Erweiterter Einsatz von Clojure
5.1 Monaden
5.1.1 Definition einer Monade
5.1.2 Gesetze der monadischen Operationen
5.1.3 Verwendung und Einsatz
5.1.4 Weitere Monadentypen
5.1.4.1 Zustands-Monade
5.1.4.2 Maybe-Monade
5.2 core.logic
5.2.1 Einführung in die Logische Programmierung
5.2.2 Grundlagen von core.logic
5.2.2.1 Inferenzumgebung run
5.2.2.2 Relationen vs. Funktionen
5.2.2.3 Fakten und Regeln
5.2.2.4 Logische Variablen
5.2.2.5 conde und Pattern Matching
5.2.3 Anwendung
5.2.3.1 Anwendungsfälle
5.2.3.2 Praktisches Beispiel
5.2.3.3 Logisches Puzzle
5.3 Zusammenfassung

6 Alternative Funktionale Sprachen
6.1 Vorstellung alternativer Sprachen
6.1.1 Objective Caml
6.1.1.1 Das Typsystem
6.1.1.2 Funktionen
6.1.1.3 Pattern Matching
6.1.1.4 Rekursion
6.1.1.5 Weiteres
6.1.2 F#
6.1.2.1 F# vs. OCaml
6.1.2.2 Weiteres
6.1.3 Erlang
6.1.3.1 Multithreading-Support
6.1.3.2 Das Typsystem
6.1.3.3 Pattern Matching
6.1.3.4 Funktionen
6.1.3.5 Weiteres
6.2 Clojure im Vergleich mit Haskell
6.2.1 Funktionsumfang
6.2.2 Sprachfeatures
6.2.3 Performance
6.3 Zusammenfassung

7 Schlussfolgerung und Ausblick

Literatur- und Quellenverzeichnis

Abkürzungsverzeichnis

illustration not visible in this excerpt

Abbildungsverzeichnis

2.1 Überführung einer Form in einen Abstract Syntax Tree

2.2 Visualisierung von Structural Sharing

4.1 Indirekte Referenzierung

5.1 Ausschnitt der Menüstruktur von Clojure.org

5.2 HTML-gerenderte Breadcrumb-Navigation

6.1 Benchmark Clojure vs. Haskell (entnommen [Fulgham 2013b])

Listings

2.1 Quadratberechnung (C#)

2.2 Quadratberechnung (Clojure)

2.3 S-Expression und Form

2.4 S-Expression und Form

2.5 Quadrieren und Verdoppeln aller Elemente einer Sequenz .

2.6 Kennzeichnung einer Seiteneffekt-Funktion in Clojure

2.7 Structural Sharing bei Listen

2.8 Beispiele der Java-Interoperabilität

2.9 Anonyme Funktion vs. Funktionsliteral . .

2.10 Closures in Clojure

2.11 Closures in C#

2.12 Currying in Clojure

2.13 Memoization in Clojure

3.1 Dynamische Codegenerierung in JavaScript

3.2 Dynamische Codegenerierung in C# #1

3.3 Dynamische Codegenerierung in C# #2

3.4 Dynamische Codegenerierung in Clojure

3.5 Makroexpandierung

3.6 Makroexpandierung

3.7 Quotieren von Listen

3.8 Syntax-Quotieren und De-Quotieren

3.9 Syntax-Splicing

3.10 Unhygienisches Makro #1

3.11 Unhygienisches Makro #2

3.12 Hygienisches Makro #1

3.13 Hygienisches Makro #2

3.14 Definition eines Protokolls

3.15 Erweitern eines Protokolls

3.16 Verwenden des erweiterten Typs

3.17 Abbilden von Hierarchien mittels extend . .

3.18 Definieren von eigenen Typen

3.19 Type-Hinting

3.20 Reader-Unterstützung für Records

3.21 Zugriff auf die Felder eines deftype Typs . .

3.22 Ein definierter Typ mit veränderlichem Feld

3.23 Inline Implementierung des AddressProtocol

3.24 Definieren von Multimethoden

3.25 Serialisierung zu XML durch Multimethoden

3.26 Verwenden der Serialisierungsmethoden

3.27 Definieren von Hierarchien

3.28 Re-Definition der Serialisierungsmethoden

3.29 Ausnahmebehandlung am Beispiel Division durch 0

3.30 Abfangen von Ausnahmen

3.31 Auslösen einer Ausnahme

3.32 Dependency Injection

3.33 Strategy Pattern

4.1 Identität und Zustand in C#

4.2 Identität und Zustand in Clojure

4.3 Dereferenzierung von Referenztypen

4.4 Auszeichnung eines var mit dem Metadatenattribut :private

4.5 Funktion zum Ermitteln der Volljährigkeit

4.6 Custom-Funktion zum Ermitteln der Volljährigkeit (fehlerhafte Version)

4.7 Custom-Funktion zum Ermitteln der Volljährigkeit mit Hilfe einer dynamischen Variable (korrekte Version)

4.8 Beispielverwendung des Referenztyps atom

4.9 Beispielverwendung des Referenztyps ref

4.10 Beispielverwendung des Referenztyps agent

4.11 Beobachten von Änderungen mit Watches

4.12 Validieren von Änderungen mit Validatoren

4.13 Berechnung in andere Threads auslagern

4.14 Blockierende Dereferenzierung eines Future

5.1 Die Einheitsfunktion der Sequenzmonade

5.2 Monadische Funktionen der Sequenzmonade

5.3 Falsche Funktionskomposition

5.4 Monadische Bindungsfunktion

5.5 Monadische Funktionskomposition

5.6 Der Monadeneinschluß #1

5.7 Der Monadeneinschluß #2

5.8 Die Einheitsfunktion ist eine Identitätsfunktion

5.9 Die Einheitsfunktion ist eine monadische Funktion

5.10 m-bind ist eine links- und rechts-assoziative Operation.

5.11 Die Einheitsfunktion der Zustandsmonade

5.12 Vier monadische Funktionen der Zustandsmonade

5.13 Funktionskomposition der Zustandsmonade

5.14 Die Einheitsfunktion der Maybe-Monade

5.15 Die Bindungsfunktion der Maybe-Monade

5.16 Die monadischen Funktionen der Maybe-Monade

5.17 Probleme der Funktionskomposition ohne Maybe-Monade.

5.18 Funktionskomposition mit Maybe-Monade

5.19 Testen der Funktionskomposition der Maybe-Monade

5.20 Triviales core.logic Programm

5.21 Unifikation

5.22 Relation vs. Funktion

5.23 Stammbaum-Relationen

5.24 Stammbaum-Fakten

5.25 Stammbaum-Regeln

5.26 Frische logische Variablen

5.27 Logische Variablen als Platzhalter

5.28 lvar zur Definition von Platzhaltern

5.29 conde vs. matche

5.30 matche - Wildcards

5.31 matche - implizite Variablen

5.32 matche - Listendestruktion

5.33 Definition neuer Goals mit defne

5.34 Darstellung der Menüstruktur von Clojure.org als Vektor

5.35 menu-logic Namensraum

5.36 parento Relation

5.37 breadcrumb-patho Relation

5.38 breadcrumb-patho Relation im Pattern Matching Format

5.39 ext-parento Relation

5.40 ext-breadcrumb-patho Relation

5.41 breadcrumb Funktion

5.42 breadcrumb-html Funktion

5.43 Definition des Namensraums zebra-logic .

5.44 Aufbau des Ergebnisvektors

5.45 Relation ist-rechts-vono

5.46 Relation wohnt-nebeno

5.47 Abbilden aller Regeln im Goal zebrao

5.48 Ergebnisermittlung des Zebrarätsels

6.1 Definition von Variablen

6.2 Ausgaben in OCaml

6.3 Tupel

6.4 Record

6.5 Listen

6.6 Arrays

6.7 Definieren von Funktionen

6.8 Pythagoras Funktion

6.9 Partielle Funktionsanwendung

6.10 Generische Typparameter

6.11 if vs. Pattern Matching

6.12 Pattern Matching

6.13 Rekursive Funktion

6.14 Endrekursive Funktion

6.15 Ein weniger striktes Typsystem in F#

6.16 Veränderliche Variablen in F#

6.17 .NET Interopabilität

6.18 F# List Comprehension

6.19 F# Pipeline Operator

6.20 F# Lazy Sequenzen

6.21 Einfache Datentypen in Erlang

6.22 Listen und Tupel in Erlang

6.23 Pattern Matching in Erlang

6.24 Funktionen und Module in Erlang .

6.25 Additionsfunktion in Haskell

6.26 Additionsfunktion in Clojure

6.27 Generische Additionsfunktion in Haskell

6.28 Bedarfsauswertung in Haskell

6.29 Strikte Auswertung in Clojure

6.30 Typfehler in Haskell

6.31 Typfehler in Clojure

6.32 Modul Data.Map in Haskell

6.33 Aliasnamen über type in Haskell

6.34 Neue Typen über data in Haskell

6.35 Rekursive Typen über data in Haskell

6.36 Ranges in Haskell

6.37 List Comprehension in Haskell

6.38 List Comprehension in Clojure

6.39 Pattern Matching und Guards in Haskell

6.40 Guards in Haskell

6.41 Case Expressions in Haskell

6.42 Pattern Matching in Clojure #1

6.43 Pattern Matching in Clojure #2

6.44 Multimethoden-Einsatz in Clojure

6.45 Ein Personen-Typ in Haskell

6.46 Ein Personen-Typ in Clojure

6.47 Komponenten der Systembibliothek in Haskell

6.48 Benutzerdefinierte Ausgabe in Haskell

6.49 Benutzerdefinierte Ausgabe in Clojure

Kapitel 1 Einleitung

1.1 Motivation

Das Paradigma der Funktionalen Programmierung sowie die zugehörigen Sprachen erlebten in den letzten Jahren ein Revival. Viele Neuentwicklungen basieren auf dem Lisp-Dialekt und laufen auf modernen Laufzeitumgebungen wie der .NET Common Language Runtime (CLR) oder der Java Virtual Machine (JVM). Algorithmen lassen sich mit Hilfe von Funktionalen Sprachen viel kürzer und einfacher implementieren als mit ihren imperativen Gegenstücken, was sich positiv auf die Wartbarkeit der Programme auswirkt. Weiterhin verlangt das funktionale Paradigma den Verzicht auf Neben- bzw. Seiteneffekte sowie die Änderungen von Daten. Dies führt zu Vorteilen beim Einsatz dieser Sprachen im Bereich der parallelen Programmierung: Daten, welche sich nicht ändern können, bereiten auch beim Zugriff durch mehrere Threads keine Probleme. Somit lassen sich komplexe Synchronisierungsverfahren, die oft zu schwer auffindbaren Fehlern führen, weitestgehend vermeiden. Heutige Prozessoren erhöhen ihre Rechengeschwindigkeit nicht mehr durch Anheben der Taktfrequenz, sondern durch Erhöhung der CPU-Kerne. Durch die Merkmale von funktionalen Sprachen lassen sich asynchrone und nebenläufige Vorgänge leichter auf die Prozessorkerne des Computers verteilen.

1.2 Zielstellung

Diese Masterarbeit wird sich mit dem jungen und modernen Lisp-Dialekt Clojure und seinen Sprachfeatures beschäftigen. Clojure läuft innerhalb der JVM und kann somit nahtlos in objek- torientierte Java-Programme integriert werden, um beispielsweise nebenläufige Algorithmen um- zusetzen. Daneben existieren alternativ auch eine CLR- sowie JavaScript-Implementierung. Im Rahmen dieser Arbeit wird neben den Concurrency-Features auch die Technologie Software Tran- sactional Memory (STM) vorgestellt, mit dessen Hilfe sich die ACID-Eigenschaften auf den zu manipulierenden transienten Speicher abbilden lassen. Eine Transaktion bündelt Befehle, die auf gemeinsame Ressourcen zugreifen. Falls zwei Transaktionen auf die gleiche Ressource zugreifen möchten, wird eine der beiden Transaktionen abgebrochen. Die Sprache bietet hierfür die benö- tigten Konstrukte. Ein wichtiges Pattern innerhalb der Objektorientierten Programmierung ist die Trennung der Programmierschnittstelle von der Implementierung, realisiert durch Sprachelemente wie Interfaces und Klassen sowie Vererbung und Polymorphie. Clojure kann als funktionale Spra- che diesen traditionellen Lösungsansatz nicht verwenden und führt Alternativen ein. Die Sprach- elemente Protocols, Multimethods und Datatypes werden hierzu näher beschrieben.

Neben den Clojure-spezifischen Sprachfeatures werden weitere grundlegende Prinzipien und Ent- wurfsmuster der Funktionalen Programmierung wie z.B. Macros, Lambdas, Currying, Memoizati- on, Closures und Monaden vorgestellt. Um Clojure von anderen funktionalen Sprachen abgrenzen zu können, werden abschließend Alternativen vorgestellt und ein Vergleich bezüglich Funktionsumfang und Performance mit Haskell durchgeführt.

Kapitel 2 Grundlagen der Funktionalen Programmierung

Der funktionale Programmierstil etabliert sich derzeit auch außerhalb akademischer Institutionen und findet nun auch bei der Entwicklung von Unternehmenssoftware Anwendung. Dies liegt pri- mär an der besseren Lesbar- sowie Wartbarkeit von funktionalen Algorithmen. Zur Verdeutlichung wird die Berechnung der Quadrate einer Liste in Listing 2.1 klassisch imperativ und in Listing 2.2 funktional umgesetzt.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.1: Quadratberechnung (C#)

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.2: Quadratberechnung (Clojure)

Der funktionale Ansatz ist deutlich kürzer und einfacher lesbar. Weiterhin fällt auf, dass in Lis- ting 2.1 die Listenelemente mit ihrem quadrierten Resultat überschrieben werden. In Listing 2.2 wird im Gegensatz dazu eine neue Liste erzeugt. Methoden funktionaler Sprachen modifizieren meist keine übergebenen Parameter und Argumente - die Datentypen sind in der Regel unverän- derlich. Daten, die sich nicht ändern können, bereiten auch keine Probleme beim geteilten Zugriff aus mehreren Threads heraus. Daraus lässt sich der zweite große Vorteil des funktionalen Paradig- mas ableiten: die Vereinfachung von paralleler und asynchroner Programmierung. Auf komplexe Synchronisationsverfahren wie Semaphore und Monitore kann weitestgehend verzichtet werden. In diesem Kapitel werden die weiteren Eigenschaften von funktionalen Sprachen sowie die dar- aus resultieren Vorteile erläutert. Diese Eigenschaften gelten für alle Sprachen der funktionalen Gattung. Die Beispiele werden jedoch in Clojure verfasst.

2.1 Eigenschaften Funktionaler Programmierung

Clojure ist ein moderner LISP-Dialekt, welcher auf der JVM aufsetzt. Clojure Quellcode ist sehr einfach zu lesen und zu interpretieren, da er vollständig aus Ausdrücken besteht, welche zu einem Wert evaluiert werden. Anweisungen ohne Ausdruckssemantik existieren nicht, auch die Kontroll- strukturen zur Programmsteuerung sind als Expressions umgesetzt. Die wichtigste Datenstruktur in funktionalen Programmen stellt die Liste dar. Aufrufe von Funktionen können nur in dieser Listensemantik denotiert werden. In der Regel enthalten Listen Symbole, welche zu Werten im aktuellen Scope evaluiert werden. Man bezeichnet solche Listen in Clojure daher als symbolic expression (s-expression), um die Signifikanz von Symbolen hervorzuheben. Kann eine s-expression zu einem gültigen Wert evaluiert werden spricht man von einer Form.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.3: S-Expression und Form

Listing 2.3 zeigt die Definitionen einiger Listen. Liste 1 ist eine gültige s-expression, jedoch keine Form, da der Wert 1 an der Aufrufposition (erste Position) nicht zu einer Funktion evaluiert wer- den kann. Liste 2 hingegen stellt eine gültige Form dar. Symbole in der Aufrufposition einer Liste werden zu der Funktion oder dem Makro im aktuellen Scope evaluiert. Alternativ stellt Clojure mit den Special Forms eine Möglichkeit zur Verfügung, auf vordefinierten Code zuzugreifen. Die Liste 3 zeigt die Verwendung der Special Form def zum Definieren von Variablen im aktuellen Namespace. Alle Kontrollstrukturen in Clojure sind als Special Forms implementiert. Eine wei- tere essentielle Eigenschaft von LISP-basierten Dialekten ist die Homoikonizität. Dies bedeutet, dass die Befehle die ein Programm bilden gleichzeitig Datenstrukturen derselben Sprache sind. So sind Listen in Clojure nicht nur Datenstrukturen, sondern werden ebenfalls genutzt um Pro- gramme zu bilden. Jede Programmiersprache muss zur Ausführung von Quellcode diesen in eine geeignete Datenstruktur zur Evaluierung überführen, den Abstract Syntax Tree (AST). Clojure re- präsentiert den AST direkt durch seine Listen-Befehlsstruktur. Abbildung 2.1 zeigt den trivialen Zusammenhang zwischen einer einfachen Form und dessen Repräsentation als AST.

Abbildung in dieser Leseprobe nicht enthalten

Abbildung 2.1: Überführung einer Form in einen Abstract Syntax Tree

Zur Auswertung eines Ausdrucks ist es nicht notwendig, Kenntnisse über Operatorenpräzedenzen zu besitzen. Operatoren sind als Funktionen implementiert und stehen immer in der Aufrufposition einer Liste. Infix oder Postfix Notationen werden vermieden, wodurch sich die Evaluierung einer Expression stark vereinfacht. Wenn die Form in Abbildung 2.1 in der Infix Notation abgebildet wird, ist es erforderlich die Standardpräzedenz der mathematischen Operatoren zu überschreiben.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.4: S-Expression und Form

Wie in Listing 2.4 ersichtlich muss mittels Klammer-Operator der Vorrang der Ausführung des Additionsausdrucks erzwungen werden, um das korrekte Ergebnis zu erhalten. Die triviale Evaluierungssemantik macht Clojure - sowie alle weiteren LISP-basierten Dialekte - zu einer sehr ausdrucksstarken und prägnanten Sprache.

Die weiteren Vorteile von funktionalen Sprachen ergeben sich aus den folgenden Eigenschaften:

- Unveränderliche Datenstrukturen werden bevorzugt eingesetzt.
- Funktionen sind First-Class Sprachelemente.
- Funktionen sind in der Regel rein, d.h. sie produzieren keine Seiteneffekte.
- Die Bereitstellung von optimalen Datentypen und Collections zur Unterstützung der Funktionalen Programmierung.

2.1.1 Werte und veränderliche Objekte

Das primäre Ziel funktionaler Sprachen ist die Minimierung von unerwartetem Programmverhal- ten. Dies wird durch die Reduzierung von Seiteneffekten, welche den Zustand einer Anwendung beeinflussen, erreicht. Imperative Sprachen verwenden bevorzugt veränderliche (mutable) Varia- blen, welche in der Regel als Objekte bezeichnet werden. Wird bei dem Aufruf einer Funktion oder Objektmethode ein veränderliches Objekt als Argument übergeben, kann ohne Kenntnis der Interna der Prozedur nicht sichergestellt werden, dass das entsprechende Objekt nicht modifiziert wird. Funktionale Sprachen verwenden hingegen bevorzugt unveränderliche (immutable) Varia- blen, welche zur Abgrenzung zu Objekten Werte genannt werden. Somit werden unerwünschte Zustandsänderungen innerhalb von Prozeduren vermieden. Durch die Verwendung von Werten ergeben sich weitere Vorteile:

- Unveränderliche Variablen können zuverlässig als Keys in Maps (Dictionaries) verwendet werden, da sich die Vergleichssemantik nicht ändert.
- Da die Werte keine Änderung erfahren, können diese auch gecached werden.
- (Globale) Werte können optimal in multithreaded Umgebungen eingesetzt werden, ohne sich Gedanken über Sperrungen (Locks) machen zu müssen.

2.1.2 First-Class, High-Order und Reine Funktionen

Die Funktion stellt in funktionalen Sprachen das wichtigste Konstrukt dar. Im Gegensatz zu anderen Sprachparadigmen ist die Funktion anderen Datentypen gleichgestellt und kann somit in Variablen gespeichert und als Ein- sowie Rückgabeparameter verwendet werden. Diese Eigenschaft wird als First-Class Sprachelement bezeichnet. Selbst andere moderne Hochsprachen wie Java1 sind derzeit nicht in der Lage Funktionen in Variablen zu speichern. In Clojure wird die Special Form fn genutzt um eine Funktion zu erzeugen, sowie defn um eine erzeugte Funktion einer Variablen im aktuellen Namespace zuzuordnen.

Funktionen, welche Funktionen produzieren oder konsumieren werden als High-Order Functi- on (HOF) bzw. höherranginge Funktionen bezeichnet. Die wichtigsten vordefinierten HOF’s in Clojure sind map, reduce2 3, apply, partial und comp. Mit Hilfe einer übergebenen Funk- tion bildet map eine Eingangssequenz auf eine Ausgangssequenz ab. reduce reduziert eine Ein- gangssequenz auf einen skalaren Wert. Die drei letzteren Funktionen bilden die Grundlage der Funktionskomposition, mit deren Hilfe sich bestehende Logik leicht zu einer neuen Komponente aggregieren lässt. Mit apply kann eine beliebige Funktion mit einer Sequenz von Eingangsparametern aufgerufen werden. partial bietet die Möglichkeit eine bestehende Funktion mit Werten vorzubelegen und als neue Funktion zur Verfügung zu stellen. comp ist ein nützliches Konstrukt zur Verkettung von Funktionen.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.5: Quadrieren und Verdoppeln aller Elemente einer Sequenz

Listing 2.5 zeigt die Verwendung von comp zur Verkettung von 2 Funktionen. Funktion 1 (letztes Argument) quadriert hierbei alle Elemente einer übergebenen Sequenz. Die neu berechnete Sequenz wird an Funktion 2 (erstes Argument) weitergereicht, welche jedes Element der Collection verdoppelt. Beide Funktionen greifen hierbei auf die Funktionalität von partial zurück, um map jeweils mit einer gewünschten Mapping-Funktion vorzubelegen. Die neu komponierte Funktion kann über den Namen square-and-double aufgerufen werden.

Ein weiteres funktionales Sprachmerkmal stellen die reinen und idempotenten Funktionen dar. Funktionen, welche abhängig von externen Zuständen sind, werden als Seiteneffekt-Funktionen bezeichnet. Da das Ergebnis eines Aufrufs abhängig von weiteren (meist unbekannten) Parametern ist, ist der Rückgabewert nicht deterministisch bestimmbar. Das prominenteste Beispiel hierfür stellt ein Zufallsgenerator dar, welcher bei jedem Aufruf ein anderes Ergebnis liefert. Wenn eine Funktion demgegenüber deterministische Ergebnisse abbildet, also keine Abhängigkeiten zu externen Zuständen aufweist, spricht man von idempotenten Funktionen. Ist die Funktion weiterhin frei von Seiteneffekten, wie Logging in eine Datei oder Schreibzugriffe auf eine Datenbank, wird sie zusätzlich als rein oder pur bezeichnet. Alle mathematischen Operationen sind reine Funktionen, deren Verwendung eine Reihe von Vorteilen aufweist:

- Da reine Funktionen keine Abhängigkeiten haben und ihre Ergebnisse deterministisch liefern, können sie ohne Komplikationen in jedem Kontext aufgerufen werden.
- Deterministische Funktionen lassen sich leichter testen, da hierbei auch der Erwartungswert deterministisch ist. Mocking-Frameworks zum Abbilden von Abhängigkeiten werden nicht gebraucht.
- Reine Funktionen sind leicht cache- und parallelisierbar, da die Funktionsaufrufe referenziell transparent sind. Dies bedeutet, dass der Aufruf jederzeit mit seinem Ergebnis ersetzt werden kann. Das Caching von Funktionen wird Memoization (siehe 2.3.4) genannt.

Clojure empfiehlt alle mit Seiteneffekten behafteten, nicht reinen Funktionen mit einem Ausrufezeichen im Funktionsnamen zu kennzeichnen. Zum Beispiel:

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.6: Kennzeichnung einer Seiteneffekt-Funktion in Clojure

2.1.3 Collections und Datentypen

Neben den skalaren und primitiven Datentypen wie Strings, Booleans und Numbers verfügt Clojure über eine Reihe von Datenstrukturen zur Abbildung von Sammlungen. Alle Datentypen in Clojure haben die folgenden Eigenschaften gemein:

- Sie sind unveränderlich und können daher nicht modifiziert werden.
- Die Persistenz (Dauerhaftigkeit) der Typen wird durch die gemeinsame Verwendung von bestehenden Strukturen erreicht (Structural Sharing).
- Die Clojure-Funktionen arbeiten mit abstrakten Typen (Abstractions), anstatt mit den spezialisierten Implementierungen.

Alle Datentypen in Clojure sind initial unveränderlich. Die Funktionen, welche auf den persis- tenten Datenstrukturen operieren, erzeugen neue Strukturen ohne die Bestehenden zu verändern. Dabei werden keine Daten kopiert, sondern die Elemente der Strukturen gemeinsam wiederver- wendet. In Listing 2.7 wird eine Liste mit den Elementen 1, 2 und 3 definiert. Die Operation conj erzeugt auf Basis der Liste a eine neue Liste b, welcher das Element 0 vorangestellt wird.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.7: Structural Sharing bei Listen

Die Elemente der Liste sind einfach verkettet, sodass jeder Knoten eine Referenz auf seinen Nachfolger hält. Die Variable a referenziert hierbei nur den Kopf der Liste, das Element 1. Beim Erzeugen der Liste b mit Hilfe von conj wird ein neues skalares Element 0 erstellt, welches die Variable b als Kopfelement referenziert. Dem Knoten 0 wird das Element 1 als direkter Nachfolger angegeben. Die Liste b verwendet somit die Elemente der Liste a wieder. Dieser Prozess des Structural Sharing wird in Abbildung 2.2 visualisiert. Viele Operationen verrichten auf Basis dieses Persistenzverhaltens ihre Arbeit in konstanter Zeit.

Abbildung in dieser Leseprobe nicht enthalten

Abbildung 2.2: Visualisierung von Structural Sharing

Die Unveränderlichkeit von Werten bedingt eine ständige Allokierung von zusätzlichem Speicher bei jeder Aktualisierung. So erzeugt conj jeweils eine Referenz auf die neu generierte Liste. Bei einer Massenaktualisierung würde sich das Persistenzverhalten unperformant auf die Anwendung auswirken und eventuell zu OutOfMemory Exceptions führen. Clojure bietet zur Optimierung sol- cher Szenarien alternativ transiente Datenstrukturen an. Transiente Typen können direkt manipu- liert werden und reduzieren somit die Notwendigkeit von kummulativen Objektallokationen sowie zusätzlichen Garbage Collection Zyklen. Sie können mit Hilfe der Funktion transient aus per- sistenten Typen erzeugt und mit persistent! wieder umgewandelt werden. Zur Aktualisierung von transienten Typen sind in Clojure eigene Funktionen vorhanden, wie beispielsweise conj! oder assoc!. Um die Vorteile von unveränderlichen Werten nicht zu verlieren, sollten transiente Typen nur innerhalb eines eingeschränkten Scopes zu Optimierungsszwecken verwendet werden. Alle öffentlichen Funktionen sollten der Semantik persistente Collection in, persistente Collection out entsprechen.

In der objektorientierten Programmierung wird der Zustand von Objekten über deren Methoden verändert. Die Memberfunktionen operieren daher immer auf dem Typ ihrer Klasse und sind somit auf diesen beschränkt. Durch Anwendung von Vererbung und Polymorphismus kann die Wieder- verwendbarkeit von Methoden durch die Typen der Vererbungslinie erhöht werden, bleibt jedoch auf diese Klassenhierarchie beschränkt. Java Maps und Java Listen stellen beispielsweise ihre eigenen isolierten Methoden zur Verfügung. Die Clojure-Kernfunktionen hingegen arbeiten mit Datenstrukturabstraktionen und sind daher universell einsetzbar. So liefert die Funktion count für alle Collection-Typen ein Ergebnis. Es wird empfohlen bei der Implementierung von eigenen Funktionen auf die universellen Kernfunktionen von Clojure zurückzugreifen, um die größtmögli- che Wiederverwendbarkeit des eigenen Quellcodes zu gewährleisten. In Clojure existieren sieben verschiedene Datenstrukturabstraktionen:

- Collection. Alle Datenstrukturimplementierungen partizipieren in dieser allgemeinen Abstraktion. Die wichtigsten Operationen, welche die Collection-Abstraktion verwenden, sind conj (hinzufügen eines Items zu einer Collection), count (ermitteln der Anzahl der Items einer Collection) und = (vergleichen der Wertgleichheit von Collections).
- Sequence. Sequenzen sind sequentielle Ansichten von beliebigen Datenquellen oder das Re- sultat einer sukzessiven Berechnung von Werten. Die Operation seq erzeugt eine Sequenz auf Basis der übergebenen Collection in Listenform. lazy-seq kann genutzt werden um eine Lazy Sequence zu erstellen. Die Elemente werden hierbei erst beim Zugriff auf den jeweiligen Index berechnet, was als Realisierung bezeichnet wird. Im Gegensatz zu den aus Java bekannten Iteratoren werden die Elemente einer Sequenz nach ihrer Realisierung im Speicher gehalten und bei allen weiteren Abfragen nicht erneut berechnet. Zum Konsu- mieren von Sequenzen stehen u.a. die Funktionen first (liefert das erste Element einer Sequenz) und rest (liefert die Restsequenz) zur Verfügung. Viele Kernfunktionen, welche eine Sequenz erwarten, generieren diese implizit auf Basis der übergebenen Collection.
- Associative. Die assoziative Abstraktion wird von Datenstrukturen verwendet, welche mit Key und Values arbeiten. Dies gilt für Maps, Sets und Vektoren, wobei bei Sets der Wert und bei Vektoren der Index den Key repräsentiert. Die wichtigsten Operationen für assoziative Abstraktionen sind assoc (fügt der Collection einen Key-Value hinzu), dissoc (entfernt einen Key-Value) und get (liefert den Value für einen bestehenden Key).
- Indexed. Die indizierte Abstraktion wird von einer einzigen Funktion genutzt: nth. Diese Funktion liefert das n-te Element einer numerisch-indizierten Collection zurück. Die Operation kann mit Vektoren, Listen, Sequenzen, Strings und allen Index-orientierten JavaCollections (z.B. Java Arrays/Listen) genutzt werden. Als Alternative zu nth kann get der assoziativen Abstraktion verwendet werden. get ist allgemeiner einsetzbar, da es mit assoziativen und indizierten Datenstrukturen umgehen kann.
- Stack. Stapel-orientierte Datenstrukturen unterstützen die sogenannte Last-in First-out (LIFO) Semantik. Ein Stapel kann durch die Operationen conj (legt einen Wert auf dem Stack ab), pop (liefert den Stack ohne den obersten Wert) und peek (ruft den letzten Wert vom Stack ab) modifiziert werden. Listen und Vektoren partizipieren in der Stack-Abstraktion und können mit diesen Operationen verwendet werden.
- Set. Die Set-Abstraktion wird zur Abbildung von Mengen verwendet. Die Datenstrukturimplementierung Set partizipiert in der gleichnamigen Abstraktion. Mit Hilfe von conj können der Menge Elemente hinzugefügt werden, disj entfernt das spezifizierte Element von der Collection. Der Namensraum clojure.set enthält eine Vielzahl an Funktionen zur Manipulation von Mengen (u.a. Schnittmenge, Vereinigung).
- Sorted. Datenstrukturen, welche der sortierten Abstraktion angehören, bieten die Möglich- keit ihre Elemente mit Hilfe eines Prädikats zu sortieren. Nur für Maps und Sets existie- ren sortierte Äquivalente. Mit sorted-map bzw. sorted-set können die sortierten Strukturen unter Angabe eines Prädikats erstellt werden. Mit den Operationen rseq und rsubseq können die Elemente in umgekehrter Reihenfolge als Sequenz abgefragt wer- den.

Clojure bietet 4 konkrete Datenstrukturimplementierungen, welche in den jeweiligen genannten Abstraktionen partizipieren. Dies sind

- Listen. Stellt die einfachste Collection-Implementierung in Clojure dar und wird primär zur Kennzeichnung von Funktionsaufrufen genutzt. Sie werden über das Reader-Literal () erstellt
- Vektoren. Sind sequentielle Datenstrukturen und bieten wahlfreien Zugriff auf ihre Elemente Die Erstellung erfolgt über das Reader-Literal [].
- Sets. Werden zur Darstellung von Mengen verwendet. Sie existieren in einer sortierten sowie unsortierten Variante. Das Reader-Makro zur Erstellung lautet #{}.
- Maps. Finden Verwendung bei der Abbildung von Key-Value basierten Dictionaries und liegen ebenfalls in einer sortierten und unsortierten Variante vor. Die Erstellung erfolgt über das Reader-Literal {}.

2.2 Programmierparadigmen

Ein Programmierparadigma bezeichnet einen Programmierstil, das heißt die Erstellung von Quellcode nach vorgegebenen Regeln. Neben dem Funktionalen Programmierstil (deklarative Programmierung) existieren noch eine Reihe weiterer Paradigmen.

2.2.1 Imperative und Prozedurale Programmierung

Der imperative oder prozedurale Programmierstil bezeichnet ein Verfahren, bei dem Befehle sequentiell verarbeitet werden. Die verwendeten Anweisungen besitzen im Gegensatz zu funktionalen Sprachen nicht zwingend Ausdruckssemantik. Zur Steuerung der Befehlsausführung werden von imperativen Sprachen Kontrollstrukturen zur Verfügung gestellt. Für die Umsetzung von Iterationen werden Schleifen eingesetzt. Die funktionalen Sprachen bieten hierfür die Technik der Rekursion an. Der imperative Programmierstil ist das am längsten bekannte und zugleich einfachste Paradigma zum Entwurf von Computerprogrammen.

2.2.2 Objektorientierte Programmierung

Der objektorientierte Programmierstil baut auf das imperative Paradigma auf und erweitert es um eine striktere Datenkapselung. Hierbei meint Datenkapselung das Verbergen von internen Details einer Datenstruktur und die Definition von Schnittstellen zum Zugriff auf die Daten. Diese Kapse- lung wurde in rein imperativen Sprachen rudimentär durch die Zusammenfassung von Prozeduren in einem Modul erreicht. Bei der objektorientierten Programmierung können logisch zusammen- gehörige Prozeduren und die Daten auf denen diese Operieren in einer Klasse zusammengefasst werden. Die Objekte, welche auf Basis dieser Klasse erstellt werden, stellen ihre Daten über öf- fentliche Funktionsmember4 der Welt zur Verfügung. Die weiteren essentiellen Konzepte sind:

- Abstraktion. Es wird mit Objekten gearbeitet, ohne Kenntnisse über die Implementierungs- details besitzen zu müssen. Dies wird durch die Nutzung von Interfaces (Schnittstellen) erreicht.
- Klassen und Vererbung. Objekte werden durch Klassen beschrieben. Die Attribute werden durch die Datenmember, das Verhalten durch die Methoden einer Klasse festgelegt. Durch das Prinzip der Vererbung kann bestehender Quellcode wiederverwendet werden.
- Polymorphismus. Bezeichnet die Möglichkeit von Objekten auf eine Nachricht (also einen Methodenaufruf) unterschiedlich zu reagieren.
- Persistenz. Objekte verfallen nicht nach der Abarbeitung einer Methode, sondern erst wenn keine Referenzen mehr auf das Objekt existieren.5

Funktionale Programmiersprachen unterstützen die objektorientierten Konzepte meist nur rudi- mentär, da sie andere Idiome verfolgen. Clojure hat einige Konzepte der Objektorientierten Pro- grammierung in die Funktionale übernommen. So verfügt die Sprache über eine ausgezeichnete Interoperabilität mit Java und ist in der Lage Klassen der Java Runtime Environment (JRE) zu instanziieren (1) oder statische Klassenmethoden aufzurufen (2) wie in Listing 2.8 abgebildet.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.8: Beispiele der Java-Interoperabilität

Die Clojure Kernfunktionen arbeiten mit Abstraktionen (wie Collections) anstatt mit den Daten- implementierungen (wie Listen oder Maps). Somit ist sichergestellt, dass die Kernprozeduren mit einer großen Vielzahl an Datentypen genutzt werden können. Die Funktionen conj und count können beispielsweise mit allen Datentypen arbeiten, welche in der Collection-Abstraktion par- tizipieren. Da alle vordefinierten Collection-Datentypen an der Collection-Abstraktion teilhaben (partizipieren), können diese Kernfunktionen sehr flexibel eingesetzt werden. Bei der Erweite- rung des Sprachumfangs durch eigene Funktionen sollte sich der Programmierer auf die Clojure- Kernfunktionen stützen, um die größtmögliche Flexibilität seines Quellcodes zu erreichen. Bei der Erweiterung des Sprachumfangs durch eigene Typen, stellt Clojure Konstrukte wie Protocols und Multimethods zur Verfügung, um die Programmierung auf Abstraktionen zu ermöglichen und die Wiederverwendung von Quellcode zu erhöhen.

2.2.3 Deklarative Programmierung

In der deklarativen Programmierung wird mit einer formalen Sprache das Problem beschrieben, welches (was) gelöst werden soll. Die Lösung wird automatisch vom System ermittelt. Im Gegensatz dazu wird bei allen anderen Paradigmen beschrieben, wie ein bestimmtes Problem gelöst werden soll. Folgende Gruppen gehören zu den deklarativen Sprachen:

- Funktionale Sprachen. (Clojure, LISP, Haskell, etc.)
- Logische Sprachen. Ein Programm besteht hierbei nicht aus einer Reihe von Anweisungen, sondern aus einer Menge von Fakten und Regeln. Dem regelbasierten System können Fragen gestellt werden. (Prolog)
- Funktional-Logische Sprachen. Mit der Bibliothek Core.Logic kann Clojure zu einer relationalen Sprache wie Prolog erweitert werden.6
- Build-Management-Werkzeuge. Befehle zum Erzeugen von Programmcode werden in Abhängigkeit von verschiedenen Bedingungen ausgeführt. (make, Ant)
- Transformationssprachen. Mit Sprachen wie Extensible Stylesheet Language Transformation (XSLT) können aus Quelldokumenten (z.B. XML) Zieldokumente (z.B. HTML) generiert werden. Für diese Abbildung sind Transformationsvorschriften notwendig, welche durch formale Sprachen wie XSLT definiert werden.
- Abfragesprachen. Formale Sprachen zum Einfügen, Bearbeiten und Löschen7 von Daten. (SQL, XPath)

Die deklarativen Sprachen werden gern Akademiker-Sprachen genannt, etablieren sich jedoch im Moment auch in der Wirtschaft.

2.2.4 Weitere Programmierparadigmen

Der Vollständigkeit halber seien noch die exotischeren Paradigmen genannt.

- Generische Programmierung. Funktionen und Prozeduren werden sehr allgemeingültig ent- worfen um die Wiederverwendung zu erhöhen. Die Funktionen können somit von einem breiten Spektrum an Datentypen genutzt werden. C++, Java und C# unterstützen dieses Pa- radigma durch den Einsatz von Templates und Generics. Clojure verfolgt den Ansatz, dass Datentyp-Implementierungen in Abstraktionen partizipieren, um Funktionen allgemeingül- tig nutzen zu können.
- Datenstromorientierte Programmierung. Der Datenstrom wird von einer Quelle erzeugt, durch Knoten transformiert und schließlich durch eine Senke konsumiert. In der Funktionalen Programmierung werden die Quellen und Senken als Nomaden und die Knoten als Funktionen implementiert.
- Generative Programmierung. Der Programmcode wird automatisiert durch einen Programmgenerator erzeugt. So kann beispielsweise ein Unified Modeling Language (UML)-Klassendiagramm zu Quellcode, eine XML-Datei über XSL Formatting Objects (XSL-FO) zu PDF oder eine Active Server Pages (ASP) WebForms Seite zu HTML transformiert werden.
- Programmierung mit Komponenten und Agenten. Das Programmierparadigma wird häufig im Bereich der Künstlichen Intelligenz angewendet. Eine formale Sprache beschreibt die mentalen Zustände eines Systems (dem Agenten), die durch Annahmen und Verpflichtungen bestimmt werden.
- Aspektorientierte Programmierung (AOP). Neben den funktionalen Anforderungen an ein Programm, welche sich leicht durch eigenständige Komponenten abbilden lassen, existieren noch die sogenannten verwobenen Anforderungen. Diese werden auch querschnittliche Belange genannt (Cross-Cutting Concerns), da Aspekte wie beispielsweise das Logging quer im Quellcode verteilt sind. AOP ist ein Werkzeug, um diesen Code zu zentralisieren. Vom Programmierer werden zusätzlich Regeln angegeben, um den zentralisierten Code automatisch an den richtigen Stellen einzuweben. In Clojure ist es mit vars und First-Class Functions relativ einfach möglich eine AOP Bibliothek zu entwerfen.

2.3 Funktionale Idiome

2.3.1 Lambda und Anonyme Funktionen

Es ist typisch für die Funktionale Programmierung, neben den benannten Funktionen auch un- benannte Funktionen zu verwenden. Benannte Funktionen erhalten bei ihrer Deklaration einen eindeutigen Namen zur Identifikation. Demgegenüber werden unbenannte Funktionen über ei- ne Referenz und nicht den Namen angesprochen. Funktionen ohne Namen werden als anonyme oder Lambda-Funktion, in Anlehnung an das Lambda-Kalkül, bezeichnet. Werden Funktionen nur an einer bestimmten Stelle im Quellcode gebraucht, empfiehlt sich die Verwendung von an- onymen Methoden. Der Namensraum wird somit nicht „verunreinigt“ und auch die Intellisense- Unterstützung einer Integrated Development Environment (IDE) bietet diese temporäre Funktion nicht zur Auswahl an. In Clojure existieren 2 Möglichkeiten eine unbenannte Funktion zu erzeu- gen.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.9: Anonyme Funktion vs. Funktionsliteral

In Listing 2.9 wird zwei mal die map Funktion bemüht, um jedes Element des Eingangsvektor mit Hilfe einer unbenannten Funktion zu quadrieren. Beim ersten map-Aufruf (1) wird durch Ver- wendung der fn (Special) Form eine Referenz auf eine anonyme Funktion erzeugt, welche direkt an die map Funktion übergeben und genutzt wird. Beim zweiten Aufruf (2) wird zum Erzeugen der unbenannten Funktion ein Funktionsliteral angegeben. Funktionsliterale beginnen mit einem Hash (#) vor dem Listenaufruf. Der Zugriff auf die übergebenen Argumente erfolgt mit einem Prozentsymbol (%). % bzw. %1 ermöglichen hierbei den Zugriff auf den ersten Parameter. Beim Zugriff auf die weiteren Argumente der Funktion muss die dem Prozentzeichen folgende Zahl entsprechend erhöht werden.

2.3.2 Closures

Da Funktionen First-Class Values sind, sind sie in der Lage, andere Funktionen zu konsumieren und zu produzieren. Wenn eine Funktion eine andere Funktion produziert, so wird diese innerhalb der aufgerufenen Funktion definiert. Verweist der Code der inneren Funktion auf Werte die nicht als Parameter übergeben wurden, sondern im lokalen Geltungsbereich der umgebenden Funktion existieren, so werden diese Werte mit in die innere Funktion übernommen. Somit werden beim Verlassen der umgebenden Funktion die übernommenen lokal gebundenen Werte nicht zerstört, sodass beim Aufruf der inneren Funktion der Zugriff auf die Variablen gewährleistet ist. Inne- re Funktionen, welche Variablen des äußeren Geltungsbereichs einschließen, werden als Closure bezeichnet. Listing 2.10 demonstriert die Verwendung einer Closure in Clojure.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.10: Closures in Clojure

Die Funktion make-greeting-fn liefert dem Aufrufer eine anonyme Funktion zurück, welche ge- nutzt werden kann um eine Person zu begrüßen. Die übergebene Grußformel greeting-prefix wird von der inneren Funktion genutzt, wodurch die lokal gebundene Variable in die Closure ein- geschlossen wird. Auch nach dem Verlassen der Funktion make-greeting-fn existiert die Varia- ble greeting-prefix weiterhin im Kontext der anonymen Funktion. Die zurückgelieferte anonyme Funktion wird innerhalb der Funktion greeting-hello mit dem übergebenen Namen aufgerufen. Daraufhin wird der Begrüßungstext erzeugt, wobei nur der Name als Parameter übergeben wird. Die Grußformel greeting-prefix existiert im Kontext der Closure als übernommene Variable.

Closures sind ein wichtiges Konzept, um notwendige Daten nicht zwingend als Parameter über- geben zu müssen. Alle wichtigen objektorientierten Hochsprachen wie Java, C# und JavaScript haben Closures mit in ihren Sprachumfang aufgenommen. Listing 2.11 zeigt obiges Beispiel in C#.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.11: Closures in C#

2.3.3 Currying

In der Funktionalen Programmierung erhalten Funktionen in der Regel nur einen Parameter, um allgemein verwendbar zu sein. So erwartet beispielsweise die Funktion map eine Funktion, wel- che sequentiell mit stets einem Argument aufgerufen wird, dem aktuellen Element der Liste. Um diese Anforderung in eigenen Funktionen umsetzen zu können, bieten funktionale Sprachen 2 Möglichkeiten.

- Die Argumente einer Funktion werden zu einem Collection-Parameter aggregiert.
- Anwendung von Currying.

Beim Currying wird eine Funktion, welche mehrere Argumente erwartet, in eine Funktion mit nur einem einzigen Argument konvertiert. Dabei liefert der Aufruf einer Curry-Funktion eine Funktion zurück, welche den nächsten Parameter erwartet. Clojure bietet die Möglichkeit mit der partiellen Funktionsanwendung über partial Curry-Funktionen zu implementieren.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.12: Currying in Clojure

In Listing 2.12 wird eine Funktion add-4-numbers definiert, welche ihre 4 übergebenen Argu- mente addiert und die Summe zurückliefert. Alternativ zum normalen Aufruf (1) zeigt das Listing das Currying der Funktion durch partielle Funktionsanwendung (2). Jeder Aufruf nimmt ein Ar- gument entgegen und liefert eine Funktion zurück, welche um einen Parameter zum vorherigen Funktionsaufruf reduziert wurde. Die letzte partielle Funktionsanwendung liefert eine Funktion mit einem verbleibenden Parameter zurück, welche aufgerufen wird und die Summe als skalaren Wert zurückliefert. Die Umsetzung von Currying in statisch typisierte Sprachen wie C# und Ja- va ist nur sehr schwer möglich, da der Rückgabewert und die Parameteranzahl zur Compilierzeit feststehen müssen. Es ist also nicht ohne weiteres möglich, eine Funktion anstatt eines skalaren Wertes zurückzuliefern.8

2.3.4 Memoization

Wie in 2.1.2 beschrieben, haben reine Funktionen den Vorteil durch ihre referenzielle Transparenz cachebar zu sein. Dabei wird die Funktion nur beim ersten Mal ausgeführt und der zurückgegebene Wert gespeichert. Bei allen weiteren Aufrufen wird der gespeicherte Wert geliefert (sofern die Parameter übereinstimmen) und die Funktion nicht erneut berechnet. Diese Technik wird Memoization genannt und wird genutzt, um Algorithmen zu beschleunigen.

Abbildung in dieser Leseprobe nicht enthalten

Listing 2.13: Memoization in Clojure

In Listing 2.13 wird mit der Funktion time-consuming-operation eine zeitintensive Operation simuliert. Mit Hilfe von memoize wird eine memoisierte Version der ursprünglichen Funktion erzeugt. Anhand der Aufrufzeiten ist ersichtlich, dass der zweite Aufruf den vorher ermittelten Wert aus dem Cache verwendet, anstatt die Funktion erneut aufzurufen.

2.4 Zusammenfassung

Clojure unterstützt alle wichtigen Basiskonzepte der Funktionalen Programmierung wie HOF’s, Lambda-Funktionen, Closures, Currying und Memoization. Darüber hinaus verfügt Clojure über Features, welche durch die objektorientierte Programmierung inspiriert wurden. Diese werden in den folgenden Kapiteln beschrieben.

Kapitel 3 Clojure-spezifische Sprachmerkmale

Clojure bietet einige Sprachmerkmale, welche es von anderen funktionalen Sprachen abhebt. Dazu gehören Makros, Datentypen und Protokolle, sowie Multimethoden. Auch die Ausnahmebehandlung und die Umsetzung verschiedener Entwurfsmuster wird beschrieben.

3.1 Makros

Clojure besitzt nur wenige eingebaute Operatoren, welche als Special Forms bezeichnet werden. Die gesamte weitere Funktionalität wird durch Funktionen und Makros gebildet. Mit Hilfe von Makros ist der Entwickler in der Lage den Clojure Compiler zu kontrollieren und somit neue Operationen zu implementieren, welche nicht im Sprachumfang enthalten sind. Makros generieren Quellcode, welcher anschließend vom Clojure Compiler evaluiert und ausgeführt wird.

3.1.1 Makros in anderen Sprachen

Die dynamische Generierung sowie Evaluierung von Programmcode ist ebenfalls in anderen Programmiersprachen verfügbar. Beispielsweise bietet JavaScript die Möglichkeit mit dem Befehl eval Anweisungen und Ausdrücke auszuführen.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.1: Dynamische Codegenerierung in JavaScript

Die Nachteile der Zeichenketten-orientierten Evaluierung sind offensichtlich:

- Die Zeichenkette wird beim Parsen des JavaScript Codes nicht evaluiert. Syntaktische Fehler werden somit erst bei der Ausführung des eval Befehls sichtbar. Auf etwaige IntellisenseUnterstützung einer IDE muss ebenfalls verzichtet werden.
- Eine Zeichenkette ist strukturlos, was die Manipulation des Codes wesentlich erschwert und auf die Möglichkeiten Regulärer Ausdruck und String-Verkettung beschränkt.
- eval ist anfällig für Code Injection. Der Angreifer versucht dabei, die zu evaluierende Zeichenkette durch schädlichen Fremdcode zu ersetzen.

Auch statisch typisierte Sprachen wie C# bieten die Möglichkeit zur Laufzeit Quellcode zu untersuchen, zu manipulieren und auszuführen.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.2: Dynamische Codegenerierung in C# #1

Die Funktion in Listing 3.2 überprüft, ob der übergebene Parameter kleiner als 5 ist und liefert einen boolschen Wert als Ergebnis zurück. Um die Funktion dynamisch zu generieren, müssen Expressions erzeugt und miteinander verknüpft werden (Listing 3.31 ).

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.3: Dynamische Codegenerierung in C# #2

Die Verwendung von Objekten (Expressions) zur Generierung von Ausdrücken hat den Vorteil, dass syntaktische Fehler schon beim Übersetzen der Anwendung durch den Compiler bemängelt werden. Jedoch sind auch hier wesentliche Nachteile erkennbar.

- C# Expression Trees sind auf Ausdrücke beschränkt. Ein Statement-Body (mehrere Anweisungen) kann nicht über einen Expression-Tree abgebildet werden.
- Durch die statische Typisierung müssen alle Expressions und Function-Delegates (Variable zum Speichern einer Funktion) einen Typ erhalten, was die Lesbarkeit des Quellcodes deutlich erschwert. Selbst die Abbildung der trivialen Funktion von Listing 3.2 ist mit einem Vielfachen an Lines of code (LOC) verbunden und kompliziert zu erfassen.
- Die Infix-Notation der Operatoren (num < 5 statt (< num 5)) führt zu komplexen Ausdrucksbäumen, welche schwierig zu Parsen und zu erstellen sind.

3.1.2 Makros in Clojure

Wie in Kapitel 2.1 beschrieben, handelt es sich bei Clojure um eine homoikonische Sprache. Ein Clojure-Programm besteht aus einer Ansammlung von Listen, welche Aufrufe von Funktionen oder Special Forms darstellen. Da die Liste ebenfalls die wichtigste Datenstruktur in Clojure ist, spricht man von einer Code-as-Data Sprache. Die Manipulation von Ausdrücken, also das dyna- mische Generieren von neuem Code, lässt sich somit über die einfachen Listenoperationen reali- sieren. Um die Anforderung aus Listing 3.2 durch ein Clojure Makro umzusetzen, ist der folgende Code ausreichend.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.4: Dynamische Codegenerierung in Clojure

Im Gegensatz zu einem Funktionsaufruf werden bei einem Makro die übergebenen Argumente nicht evaluiert. Das Makro num-less-than-five aus Listing 3.4 erhält somit num als Sym- bol und nicht den Wert, den es benennt. Als Rückgabewert muss ein Makro ausführbaren Code generieren (also Listen), welcher anschließend durch den Clojure Compiler evaluiert wird. Die Symbole, welche nicht innerhalb des Makro evaluiert werden sollen, müssen gequotet werden (’). Diese Symbole werden somit erst durch den Clojure Compiler evaluiert, wenn das Makro expan- diert wurde und das Ergebnis vorliegt. Mit Hilfe von macroexpand bzw. macroexpand-1 ist dies möglich. Ein expandiertes Makro kann weitere Makros enthalten, welche von Clojure rekur- siv expandiert werden, bis der erzeugte Code keine weiteren Makros mehr enthält. Bei Verwen- dung von macroexpand-1 erfolgt die Expandierung nur einmal, bei macroexpand hingegen rekursiv. Beide dienen dem Debugging und werden nicht in Produktivumgebungen verwendet.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.5: Makroexpandierung

Die Anforderung des Makros num-less-than-five lässt sich deutlich einfacher durch eine Funktion umsetzen. Mit Hilfe von Makros lassen sich Konstrukte realisieren, welche nicht durch Funktionen abgebildet werden können. Das folgende Makro lässt sich nicht durch eine Funktion umsetzen, wie das folgende Beispiel zeigt.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.6: Makroexpandierung

Das Makro reverse-it aus Listing 3.62 durchläuft alle Elemente der übergebenen Form, invertiert alle Symbole und liefert die neu generierte Liste zurück, welche anschließend vom Clojure Compiler evaluiert und ausgeführt wird. Eine Funktion könnte die Anforderung nicht erfüllen. Makros sollten dann verwendet werden, wenn eigene Sprachkonstrukte benötigt werden. Anderenfalls sollten Funktionen bevorzugt werden.

3.1.3 Quote und Syntax-Quote

Bei der Bearbeitung und Generierung von Codekonstrukten innerhalb von Makros steht die Erstellung von Listen im Vordergrund, da diese die Befehle repräsentieren. Dabei werden einige Elemente im Makro-Body evaluiert, andere erst nach Expandierung durch den Clojure Compiler. Um die Evaluierung von Symbolen im Makro zu unterdrücken, wird die quote Form verwendet. Im Regelfall wird das kürzere Reader-Literal ’ verwendet.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.7: Quotieren von Listen

Es besteht die Möglichkeit mittels der Form list eine neue Liste zu erstellen und die Elemente, welche nicht evaluiert werden sollen, zu quotieren. Durch die Vielzahl an Hochkommata wird jedoch die Lesbarkeit des Codes erschwert. Um die Anzahl zu verringern, kann alternativ auch die gesamte Liste quotiert werden, was zur Quotierung aller Listenelemente führt. Dies ist jedoch nicht immer gewünscht, da eventuell vereinzelte Elemente dennoch innerhalb des Makros evaluiert werden sollen. Clojure bietet die Möglichkeit, Symbole auch durch Anwendung des Syntax-Quote zu quotieren (´). Einzelne Elemente innerhalb von Syntax-quotierten Listen können durch das Voranstellen von ˜ wieder de-quotiert werden, was zur Evaluierung dieser Symbole führt.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.8: Syntax-Quotieren und De-Quotieren

Wie in Listing 3.8 erkennbar, führt die Syntax-Quotierung einer Liste zur Darstellung aller Elemente durch ihren voll-qualifizierten Namen. Als weitere Komfort-Funktion können innerhalb von Syntax-quotierten Listen die Elemente anderer Listen eingefügt werden. Dies wird als SyntaxSplicing bezeichnet und durch das Reader-Literal ˜@ gekennzeichnet.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.9: Syntax-Splicing

Die Syntax-Quotierung sowie das -Splicing sind nicht auf die Verwendung innerhalb von Makros beschränkt, bilden dort jedoch den Hauptanwendungsfall.

3.1.4 Hygiene

Eine häufige Fehlerquelle beim Einsatz von Makros ist die Kollision von Symbolnamen innerhalb von Makros mit denen des umgebenden Codes. Makros, welche gezielt versuchen diese Probleme zu umgehen, werden als hygienische Makros bezeichnet.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.10: Unhygienisches Makro #1

Das unhygienische Makro in Listing 3.10 generiert eine let Form, welche alle Befehle des über- gebenen Bodies ausführt und anschließend den Status ready auf der Standardausgabe ausgibt. Der Status wird (exemplarisch) innerhalb der let Form an die lokale Variable x gebunden. Da die Syntax-Quotierung das Symbol x zu einem voll-qualifizierten Symbol im aktuellen Namensraum erweitern würde (user/x), wäre die let Bindung nicht möglich und würde einen Fehler verursa- chen. Ein let Binding darf nur unqualifizierte Namen enthalten, da es sich um die Abbildung eines lokalen Scopes handelt. Der Ausdruck nem voll-qualifizierenden Namen.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.11: Unhygienisches Makro #2

Das lokale let Binding in Listing 3.11 wird aufgrund der Namenskollision innerhalb des Makros verdeckt. Fehler dieser Art sind schwer auffindbar, da keine Fehlermeldung erscheint, sondern lediglich das Verhalten geändert wird. Da dem Benutzer in der Regel der Makrocode nicht vorliegt, muss der Entwickler des Makros dafür sorgen, Namenskollisionen zu verhindern. Clojure stellt mit gensym einen Befehl zur Verfügung, welcher bei jedem Aufruf ein Symbol mit einem einzigartigen Namen erzeugt und somit Namenskollisionen vermeidet.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.12: Hygienisches Makro #1

Innerhalb des Makro wird über ein let Binding das neue Symbol an die Variable x gebunden. Der Code, welcher vom Makro zurückgeliefert wird, verwendet diese Variable, welche vorher zum ein- zigartigen Symbolnamen evaluiert wird. Da das expandierte Makro somit nicht mehr den Namen x für das lokale Binding benutzt, wird die Namenskollision umgangen. Als Komfort-Funktion besteht die Möglichkeit, die innerhalb von Syntax-quotierten Forms verwendeten Symbole auto- matisch zu gensym-Symbolen zu expandieren. Dazu müssen die Symbole das Suffix # erhalten. Diese Symbole werden als auto-gensym Symbole bezeichnet. Listing 3.13 zeigt die deutlich kür- zere Version des Codes aus Listing 3.12 unter Verwendung von auto-gensym.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.13: Hygienisches Makro #2

3.2 Datentypen und Protokolle

Protokolle sind Clojure Konstrukte zur Lösung des sogenannten dynamischen Expression-Problems. In [Emerick et al. 2012a] wird das Problem beschrieben. Das Ziel ist es neue Datentypen zu erstel- len, welche existierende Schnittstellen implementieren sowie neue Schnittstellen auf bestehende Datentypen anzuwenden, ohne eine Recompilierung erforderlich zu machen. Die Implementie- rung von bestehenden Interfaces in neuen Typen ist prinzipiell mit allen gängigen objektorien- tierten Programmiersprachen möglich. Das Erweitern von vorhandenen Typen um die Methoden von neuen Interfaces ist jedoch in nur wenigen Sprachen möglich. Objective-C unterstützt das Konzept durch Kategorien, C# ab Version 3.5 durch Erweiterungsmethoden und Clojure durch die sogenannten Protokolle.

3.2.1 Definition von Protokollen

Protokolle sind für Clojure, was Interfaces für Java oder C# repräsentieren: Container für Metho- densignaturen, welche eine Schnittstelle kennzeichnen. Alle Funktionen eines Protokolls müssen mindestens ein Argument besitzen, welches als privilegierter Parameter bezeichnet wird. Der Pa- rameter kann mit dem üblichen this bzw. self Parameter von objektorientierten Sprachen verglichen werden und bietet Zugriff auf die Eigenschaften des Objekts. Ein Protokoll wird mit Hilfe der Form defprotocol definiert.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.14: Definition eines Protokolls

In Listing 3.14 wird ein Protokoll definiert, welches die Signatur einer Funktion zur Berechnung des Betrags eines dreidimensionalen Vektors enthält. Laut Clojure-Konvention ist es üblich, Protokollnamen in der Camel-Case-Schreibweise zu formulieren. Dies wird empfohlen, da der Clojure Compiler die Protokolle zu nativen Java-Interfaces übersetzt. Um Protokolle verwenden zu können, müssen diese auf die entsprechenden Typen erweitert werden. Dabei muss nicht zwingend das gesamte Protokoll implementiert werden. Clojure verfolgt den Clean-Code Ansatz You Ain’t Gonna Need It (YAGNI). Es bezeichnet ein Prinzip des Extreme Programming (XP), welches besagt, dass in einem Programm erst dann Funktionalität implementiert werden sollte, wenn klar ist, dass diese Funktionalität tatsächlich gebraucht wird.

3.2.2 Erweitern von Datentypen

Um ein Protokoll für einen bestimmten Typen zu implementieren bietet Clojure die folgenden Möglichkeiten:

- Inline-Implementierung
- extend
- extend-type
- extend-protocol

Bei der Inline-Implementierung wird ein Protokoll bei der Definition eines neuen Typs mit deftype bzw. defrecord ausprogrammiert. Dies kann mit der konventionellen Erstellung von neuen ob- jektorientierten Klassen, welche Schnittstellen implementieren, verglichen werden (siehe Kapitel

3.2.3).

Die beiden extend-* Befehle erweitern bestehende Typen um die angegebenen Protokolle. extend-protocol erweitert dabei mehrere Typen um ein Protokoll, extend-type erweitert einen Typ um mehrere Protokolle. Um Vektoren um das Protokoll aus Listing 3.14 zu erweitern, bietet sich extend-protocol an.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.15: Erweitern eines Protokolls

Um die Funktion zu Quadratwurzelberechnung verfügbar zu machen, wird die externe Bibliothek clojure.contrib.math eingebunden.3 In Clojure lassen sich die mittels defrecord und deftype erstellten Typen, Java-Klassen sowie Interfaces um Protokolle erweitern. Um die Clojure-Vektoren zu erweitern wird das Interface clojure.lang.IPersistentVector angegeben, welches alle ClojureVektoren implementiert. Die in Kapitel 2.1.3 beschriebene Datenstruktur-Abstraktion basiert vollständig auf der Umsetzung durch Java-Interfaces und Protokolle.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.16: Verwenden des erweiterten Typs

Beim Aufruf der Operation vector-length wird überprüft, ob für den Typen des ersten Parameters das entsprechende Protokoll implementiert wurde. Wenn dies der Fall ist, wird die Funktion mit dem Objekt als privilegierten Parameter aufgerufen. Dieses Vorgehen wird als Single Type Dispatch bezeichnet, da auf Basis eines einzelnen Parameters die aufzurufende Funktion ermittelt wird.4 Da Clojure-Listen die Schnittstelle clojure.lang.IPersistentVector nicht implementieren, schlägt der Single Type Dispatch mit einer IllegalArgumentException fehl.

Das Konzept der Clojure-Protokolle kennt keine Formen von Vererbungshierarchien. Dies ist Ab- sicht, da Vererbung im Bereich der objektorientierten Programmierung zu vielen Problemen ge- führt hat. Die Probleme waren so tiefgreifend, dass die Gang of Four (GOF)5 empfohlen hat Kom- position der klassischen Vererbung vorzuziehen.6 In Clojure kann über die Funktion extend ein Typ beliebig erweitert werden. Die Protokollimplementierung wird dabei in Form einer Implemen- tation Map angegeben, wobei die jeweiligen Funktionen nach Belieben erstellt werden können.

Abbildung in dieser Leseprobe nicht enthalten

Listing 3.17: Abbilden von Hierarchien mittels extend

In Listing 3.17 wird das Protokoll VectorCalcualtions mit Hilfe von extend ebenfalls für Listen implementiert. Die Implementation Map enthält im Slot vector-length die Umsetzung der entsprechenden Protokollfunktion. Dabei wird auf die existierende (Basis-)Implementierung des Protokolls für Vektoren zurückgegriffen. Das Symbol VectorCalcualtions wird zu einer Protokoll Map evaluiert, welche eine Vielzahl an Informationen über das Protokoll enthält. Dies wird als Protocol Introspection bezeichnet. Über das Keyword :impls erhält man Zugriff auf al- le Typen, welche das Protokoll erweitern. Die Implementierung der Funktion vector-length des Typs clojure.lang.IPersistentVector wird über ein Let-Binding in der Variablen base-fn gespeichert. Beim Aufruf von vector-length mit einer Liste, wird die Liste in einen Vek- tor konvertiert und anschließend der Funktion base-fn übergeben. Die bestehende Protokoll- Implementierung wird wiederverwendet.

3.2.3 Benutzerdefinierte Typen

Clojure bietet die Möglichkeit mit Hilfe von defrecord und deftype eigene Typen zu definie- ren. Die erstellten Typen werden von der Laufzeitumgebung zu nativen Java-Klassen kompiliert.

[...]


1 Dieser Nachteil soll in naher Zukunft mit dem Projekt Lambda beseitigt werden.

2 NoSQL-Datenbanken wie Hadoop nutzen ebenfalls die Algorithmik von Map/Reduce zur parallelen Berechnung von großen Datenmengen.

3 Auch andere Objektorientierte Sprachen haben die Semantik von map/reduce mittlerweile adaptiert, z.B. C# LINQ

4 Funktionsmember werden in der objektorientierten Programmierung oft als Methoden bezeichnet

5 Dies gilt nur für Sprachen mit Garbage Collector. Bei nativen Sprachen wie C++ muss der Programmierer die Objekte manuell zerstören, um Memory Leaks zu vermeiden.

6 Das Projekt ist unter Github zu finden: https://github.com/clojure/core.logic

7 Man spricht in diesem Kontext von CRUD-Operationen (Create, Read, Update, Delete )

8 Es besteht die Möglichkeit Currying über die Definition von Hilfsmethoden abzubilden. Unter http://mikehadlow.blogspot.de/2008/03/currying-in-c-with-oliver-sturm.html ist ein Umsetzung des Currying in C# zu finden.

1 Das Beispiel wurde der Seite http://msdn.microsoft.com/en-us/library/bb397951.aspx entnommen.

2 Das Beispiel wurde [Emerick et al. 2012e] entnommen.

3 Die Bibliothek ist unter http://richhickey.github.io/clojure-contrib/index.html verfügbar.

4 Werden mehrere Indikatoren zum Ermitteln einer Funktion benötigt, kennt Clojure das Konzept der Multimethoden.

5 Eine berühmte Autorengruppe von Entwurfsmustern und Best Practises.

6 Vgl. http://www.clean-code-developer.de/Favour-Composition-over-Inheritance-FCoI.ashx

Ende der Leseprobe aus 98 Seiten

Details

Titel
Modernes Clojure. Einsatz und Bewertung moderner Sprachfeatures in Clojure
Hochschule
Beuth Hochschule für Technik Berlin
Note
1,0
Autor
Jahr
2013
Seiten
98
Katalognummer
V381390
ISBN (eBook)
9783668578692
Dateigröße
864 KB
Sprache
Deutsch
Schlagworte
Clojure, Concurrency, Core.Logic, Software Transactional Memory
Arbeit zitieren
Lutz Leonhardt (Autor:in), 2013, Modernes Clojure. Einsatz und Bewertung moderner Sprachfeatures in Clojure, München, GRIN Verlag, https://www.grin.com/document/381390

Kommentare

  • Noch keine Kommentare.
Blick ins Buch
Titel: Modernes Clojure. Einsatz und Bewertung moderner Sprachfeatures in Clojure



Ihre Arbeit hochladen

Ihre Hausarbeit / Abschlussarbeit:

- Publikation als eBook und Buch
- Hohes Honorar auf die Verkäufe
- Für Sie komplett kostenlos – mit ISBN
- Es dauert nur 5 Minuten
- Jede Arbeit findet Leser

Kostenlos Autor werden