30.03.2014 Aufrufe

Die fertige Bachelorarbeit steht hier zum Download. - WordPress.com

Die fertige Bachelorarbeit steht hier zum Download. - WordPress.com

Die fertige Bachelorarbeit steht hier zum Download. - WordPress.com

MEHR ANZEIGEN
WENIGER ANZEIGEN

Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.

YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.

Fachbereich Informatik<br />

Universität Hamburg<br />

Deklarativ, wenn möglich;<br />

imperativ, wenn nötig<br />

Deklarativer Programmierstil heute<br />

Karsten Pietrzyk<br />

Matrikelnummer: 6209622<br />

31.7.13<br />

<strong>Bachelorarbeit</strong><br />

Erstgutachter:<br />

Zweitgutachter:<br />

Prof. Dr.-Ing. Wolfgang Menzel<br />

Prof. Dr. rer. nat. Leonie Dreschler-Fischer


Inhaltsverzeichnis<br />

1 Einleitung 5<br />

1.1 <strong>Die</strong>se Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5<br />

1.2 Was ist deklarative Programmierung? . . . . . . . . . . . . . . . . 6<br />

1.3 Wozu deklarative Programmierung? . . . . . . . . . . . . . . . . . 7<br />

1.4 Imperative Programmierung . . . . . . . . . . . . . . . . . . . . . . 8<br />

1.5 Objektorientierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 8<br />

1.6 Aufbau dieser Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . 8<br />

2 Deklarativer Programmierstil um 1980 10<br />

2.1 Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />

2.1.1 Unifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . 13<br />

2.1.2 Einordnung von Prolog . . . . . . . . . . . . . . . . . . . . 13<br />

2.1.3 Prolog und funktionale Programmierung . . . . . . . . . . 15<br />

2.2 Miranda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15<br />

3 Deklarativer Stil um 2000 19<br />

3.1 Analyse von deklarativen Sprachelementen . . . . . . . . . . . . . 20<br />

3.2 Deklarative Elemente von F# . . . . . . . . . . . . . . . . . . . . . 21<br />

3.2.1 Definition und Verwendung von Typen . . . . . . . . . . . 21<br />

Funktionstyp . . . . . . . . . . . . . . . . . . . . . . . . . . 22<br />

Algebraischer Datentyp . . . . . . . . . . . . . . . . . . . . 23<br />

Datensatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24<br />

Klasse und Interface . . . . . . . . . . . . . . . . . . . . . . 25<br />

Struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25<br />

3.2.2 Pattern matching . . . . . . . . . . . . . . . . . . . . . . . . 26<br />

3.2.3 Active Patterns . . . . . . . . . . . . . . . . . . . . . . . . . 29<br />

Klassifizierer . . . . . . . . . . . . . . . . . . . . . . . . . . 30<br />

Validierer und Parameterised Active Pattern . . . . . . . . 31<br />

Konvertierter . . . . . . . . . . . . . . . . . . . . . . . . . . 31<br />

Parser und Partial Active Patterns . . . . . . . . . . . . . . 31<br />

2


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Übersicht der Anwendungsfälle . . . . . . . . . . . . . . . . 33<br />

3.2.4 Computation Expressions . . . . . . . . . . . . . . . . . . . 34<br />

3.3 Domänenspezifische Sprachen . . . . . . . . . . . . . . . . . . . . 38<br />

3.3.1 Was sind DSLs? . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />

3.3.2 Reguläre Ausdrücke . . . . . . . . . . . . . . . . . . . . . . 39<br />

3.4 Metaprogrammierung . . . . . . . . . . . . . . . . . . . . . . . . . 41<br />

3.4.1 Was ist Metaprogrammierung? . . . . . . . . . . . . . . . . 41<br />

3.4.2 Quotations . . . . . . . . . . . . . . . . . . . . . . . . . . . 42<br />

4 Imperativer Stil 44<br />

4.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44<br />

4.2 Imperative Konsistenzprüfungen . . . . . . . . . . . . . . . . . . . 46<br />

4.3 Imperativ-objektorientierte Typen . . . . . . . . . . . . . . . . . . 47<br />

4.3.1 Modellieren einer festen Anzahl an Varianten . . . . . . . . 49<br />

4.3.2 Bezug zu Typ-Definitionen in F# . . . . . . . . . . . . . . . 50<br />

4.3.3 Structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50<br />

4.4 Imperativ-objektorientierte Fallunterscheidungen . . . . . . . . . . 51<br />

4.4.1 Mittel der Fallunterscheidungen reiner imperativer Programmierung<br />

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52<br />

4.4.2 Mittel der imperativ-objektorientierten Programmierung . 52<br />

4.4.3 Wann ist welche Technik zu benutzen? . . . . . . . . . . . 53<br />

5 Evaluation der Fallunterscheidungstechniken 54<br />

5.1 Pattern matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55<br />

5.2 Subtyp-Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . 58<br />

5.3 Philip Wadlers expression problem . . . . . . . . . . . . . . . . . . . 59<br />

5.4 Imperative Ansätze . . . . . . . . . . . . . . . . . . . . . . . . . . . 59<br />

5.5 Das Besucher-Entwurfsmuster (Visitor pattern) . . . . . . . . . . . 60<br />

5.6 Abschließende Betrachtung . . . . . . . . . . . . . . . . . . . . . . 61<br />

6 Fazit 63<br />

6.1 Dargestellte Aspekte . . . . . . . . . . . . . . . . . . . . . . . . . . 63<br />

6.2 Ausgelassene Aspekte . . . . . . . . . . . . . . . . . . . . . . . . . 64<br />

6.3 Schlusswort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65<br />

7 Anhang 70<br />

7.1 Listing 1: Miss Grant’s Controller in F# . . . . . . . . . . . . . . . 70<br />

7.2 Listing 2: XML-Traversierung mit Active Patterns in F# . . . . . . 72<br />

7.3 Listing 3: Generische Variante von Counting Sort in F# . . . . . . 73<br />

7.4 Listing 4: Prolog-ähnliche Quotation in F# . . . . . . . . . . . . . 74<br />

7.5 Listing 5: Evaluator - algebraische Datentypen / Pattern matching 75<br />

7.6 Listing 6: Evaluator - datenorientiert mit Typtests . . . . . . . . . 79<br />

7.7 Listing 7: Evaluator - datenorientiert mit switch über Type code . 82<br />

7.8 Listing 8: Evaluator - verhaltensorientiert mit Polymorphie . . . . 85<br />

7.9 Listing 9: Evaluator - verhaltensorientiert mit Visitor pattern . . . 88<br />

7.10 Listing 10: Test der Evaluator-Implementierungen in C# . . . . . . 93<br />

3


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Zusammenfassung<br />

In dieser Arbeit geht es darum, den deklarativen Programmierstil zu untersuchen,<br />

weil er den Fokus darauf legt, dem Computer mitzuteilen, was gemacht werden<br />

soll, im Gegensatz <strong>zum</strong> imperativen Stil, bei dem es um das Wie geht.<br />

Gerade heute beinhalten Programmiersprachen deklarative Elemente, wodurch<br />

mit ihnen produktiver gearbeitet werden kann. Eine moderne, multiparadigmatische<br />

Programmiersprache mit vielen deklarativen Elementen ist F#. <strong>Die</strong> Verwendung<br />

deklarativer Programmiersprachenelemente führt zu elegantem Quellcode<br />

und das damit oft assoziierte zustandslose Programmieren bringt insbesondere<br />

Vorteile bei parallelen Berechnungen. Dennoch müssen nicht alle Probleme<br />

zwingend zustandslos gelöst werden. <strong>Die</strong> Alternative, imperativ und zustandsorientiert<br />

zu programmieren, hat vor allem bei der Implementierung effizienter<br />

Algorithmen ihre Anwendung.<br />

Ziel der Arbeit ist es, eine konkrete Idee von deklarativer Problemlösung zu vermitteln<br />

und die entgegengesetzten Programmierstile abzuwägen. Im Rahmen dieser<br />

Arbeit werden deklarative Programmiersprachenelemente von F# untersucht<br />

und ihren imperativen Pendants gegenübergestellt.<br />

4


1<br />

Einleitung<br />

1.1 <strong>Die</strong>se Arbeit<br />

<strong>Die</strong>se Arbeit stellt die deklarative Programmierung vor und vergleicht Konzepte<br />

aus diesem Programmierstil mit dem gegensätzlichen imperativen Paradigma.<br />

Letztendlich werden auch deklarative Programme auf einer niedrigen Maschinenebene<br />

umgesetzt, deshalb ist das Wissen um die Arbeitsweise deklarativer Sprachen<br />

eine Voraussetzung, um moderne Programmiersprachenfeatures zu bewerten<br />

und ihre Verwendung abzuwägen. <strong>Die</strong>se Arbeit ist aber keine Auflistung von<br />

beliebigen Programmiersprachenfeatures. Geeignete Modularisierung und hohes<br />

Abstraktionsniveau sind die zwei Kernanforderungen, die an eine Programmiersprache<br />

gestellt werden sollten, daher werden sie als Leitlinie benutzt.<br />

F# tritt in dieser Arbeit als Vertreter der modernen deklarativen Programmierung<br />

auf und orientiert sich an der ML-Familie (ML <strong>steht</strong> für meta language) und ist<br />

funktional geprägt. Zuerst gehe ich auf Anfänge deklarativer Sprachen ein, weil<br />

heutige Programmiersprachenkonzepte auf Ideen alter Programmiersprachen basieren.<br />

Dann werden deklarative Ansätze in F# untersucht, um eine konkrete Idee<br />

von deklarativer Programmierung zu geben.<br />

In der Einleitung werden zuerst verschiedene Erklärungen und Definitionen von<br />

„deklarativer Programmierung“ vor- und gegenübergestellt. Daraus resultiert eine<br />

eigene Definition, die in der Arbeit als Leitfaden verwendet wird. Mit einem<br />

kurzen Überblick über verwendete Paradigmen und der Zielsetzung dieser Arbeit<br />

schließt das Kapitel.<br />

5


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

1.2 Was ist deklarative Programmierung?<br />

Definition nach [InformatikLexikon]:<br />

„deklarativ: Erklärend, verkündend; Programmiersprachen der 4.<br />

Generation; in einer d. Programmiersprache erklärt der Progammierer,<br />

was er erhalten oder erreichen möchte, aber die Einzelschritte <strong>zum</strong> Ziel<br />

entscheidet der Compiler oder Interpreter; man erklärt das Was, nicht<br />

aber das Wie; Abfragesprachen sind im Allgemeinen d., weil wir nur<br />

eine Datenmenge beschreiben: im Beispiel SELECT name FROM mitglieder<br />

übernimmt das System die Öffnung der Datei und/oder die Steuerung<br />

des Cursors durch alle Datensätze hindurch sowie Optimierungen u. a.;<br />

funktionale Programmiersprachen sind d., weil es in ihnen keine<br />

Anweisungen, sondern nur Funktionen gibt, vergleiche Lambda-Kalkül;<br />

siehe als Gegensatz: deskriptiv“ [InformatikLexikon, S. 215]<br />

Nach [PractAdvantDecl]:<br />

„Informally, declarative programming involves stating what is to be<br />

<strong>com</strong>puted, but not necessarily how it is to be <strong>com</strong>puted. [...] The key<br />

idea of declarative programming is that a program is a theory (in some<br />

suitable logic) and that <strong>com</strong>putation is deduction from the theory. “<br />

[PractAdvantDecl, S. 1]<br />

Nach [DeklarativeProgrammierung]:<br />

„Gemeinsam ist allen deklarativen Sprachen, daß die Lösung eines<br />

Problems auf einer hohen Abstraktionsebene spezifiziert, und nicht auf<br />

einer niedrigen Maschinenebene ’ausprogrammiert’ wird. Offensichtlich<br />

sind gemäß dieser Unterscheidung die Grenzen fließend! Entscheidend<br />

ist <strong>hier</strong>bei, was wir als ’high-level’ vs. ’low-level’ bezeichnen. Gemeint<br />

ist in diesem Zusammenhang etwa nicht der Unterschied zwischen<br />

maschinennahen (Assembler-) und problemorientierten (’höheren’)<br />

Programmiersprachen. “ [DeklarativeProgrammierung, S. 7]<br />

Nach [MultiparadigmenProg]:<br />

„<strong>Die</strong> zustandsfreien Sprachen (auch oft deklarative Sprachen genannt)<br />

werden im Wesentlichen in funktionale, logische und constraint-basierte<br />

Programmiersprachen unterteilt. “ [MultiparadigmenProg, S. 8]<br />

Folgende Eigenschaften und Ideen lassen sich extra<strong>hier</strong>en:<br />

• Was statt Wie (der Computer entscheidet die konkrete Umsetzung)<br />

• deklarative Programme sind zustandsfrei / referentiell transparent<br />

• Problem wird auf einer hohen Abstraktionsebene spezifiziert<br />

• funktionale, logische und constraint-basierte Programmiersprachen sind deklarativ<br />

Insbesondere finde ich die Haltung des deklarativen Programmierers wichtig, d.h.<br />

Konzentration auf das Problem, nicht auf die Umsetzung auf Maschinenebene.<br />

Für diese Arbeit schlage ich deshalb eine eigene, kurze Formulierung vor, welche<br />

die Tätigkeit des Programmierers beschreibt, ohne mich dabei auf Programmier-<br />

6


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

sprachfamilien einzuschränken. So können auch domänenspezifische Sprachen<br />

und geeignete Konzepte aus imperativ-objektorientierten Programmiersprachen<br />

deklarativ genannt werden.<br />

„Deklarative Programmierung ist das Formulieren von Regeln und<br />

Zusammenhängen, die in ein ausführbares Programm überführt werden können.<br />

“<br />

<strong>Die</strong> Kernpunkte dieser Definition werden <strong>hier</strong> einmal aufgeschlüsselt:<br />

• Das Formulieren geschieht üblicherweise durch das Schreiben von Text oder<br />

Quelltext, aber auch grafische Symbolmanipulation ist denkbar.<br />

• Regeln beschreiben Bedingungen, um Fälle zu klassifizieren oder zu behandeln.<br />

• Zusammenhänge sind z.B. funktionale Zusammenhänge oder logische Relationen.<br />

• Ein ausführbares Programm ist entweder eine <strong>fertige</strong> Anwendung oder ein<br />

Programmbaustein, der in einer anderen Anwendung benutzt wird.<br />

• Überführt werden die Beschreibungen in ausführbare Programme z.B. durch<br />

Codegenerierung und Kompilierung oder Programmtransformation und -<br />

Interpretation.<br />

Obwohl Definitionen wie „a program is a theory (in some suitable logic) and [...]<br />

<strong>com</strong>putation is deduction from the theory“ [PractAdvantDecl] der deklarativen<br />

Idee recht nahe kommen, halte ich es dennoch für wichtig genug, zwei zentrale<br />

Elemente der Programmierung in den Vordergrund zu stellen: Regeln zur Unterscheidung<br />

und Zusammenhänge zur Berechnung. Insbesondere da gängige Programmierung<br />

bisher noch mit Quellcode und (noch) nicht mit Formeln aus der<br />

Mathematik funktioniert.<br />

1.3 Wozu deklarative Programmierung?<br />

„Bei deklarativer Programmierung konzentriert man sich auf die wesentliche<br />

Problemlösung und nicht auf Details der Umsetzung. “<br />

• Problemlösung ist der Zweck von Programmen.<br />

• Details der Umsetzung lenken von der Problemlösung ab und können komplex<br />

(z.B. Speicher<strong>hier</strong>archie), fehlerträchtig (z.B. Speicherfreigabe) oder<br />

lästig (z.B. Datenprüfung) sein. Bei der Arbeit mit solchen Details kann fehlende<br />

Programmierdisziplin oder Unwissen zu fatalen Fehlern führen. Unterstützt<br />

eine Programmiersprache nicht die nötige Modularisierung, sind<br />

einfache Aufgaben nur durch Schreiben von sogenanntem Boilerplate-Code<br />

(größere Quellcode-Ausschnitte, die geringfügig abgeändert an mehreren<br />

Stellen auftreten) zu lösen [RealWorldFP, vgl. S. 130].<br />

7


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

1.4 Imperative Programmierung<br />

Der Gegensatz von deklarativer Programmierung ist imperative Programmierung<br />

(auch deskriptive oder prozedurale Programmierung [InformatikLexikon]). <strong>Die</strong><br />

Kernidee imperativer Programmierung ist es, den Zustand während des Programmablaufs<br />

durch die Ausführung von Anweisungen zu verändern, sodass das Ziel<br />

erreicht wird. Durch die aus der prozeduralen Programmierung ermöglichten<br />

Prozeduren kann Quellcode organisiert werden, was Programmbibliotheken und<br />

Unterprogramme ermöglicht [MultiparadigmenProg, vgl. S. 11-12]. Imperative<br />

Programmierung ist wichtig, weil sie mit der Arbeitsweise des Computers übereinstimmt<br />

und gängige Algorithmen, die in Pseudocode notiert sind, sich auf imperative<br />

Programmierung berufen.<br />

1.5 Objektorientierung<br />

Ein besonders wichtiges Programmier-Paradigma der heutigen Zeit ist Objektorientierung<br />

(kurz OO). Der wichtigste Vertreter in der Anfangszeit dieses Paradigmas<br />

ist die Programmiersprache Smalltalk. <strong>Die</strong> zentrale Idee ist die Strukturierung<br />

von Programmen durch Klassen und Organisierung von Programmlogik durch<br />

Methoden. Viele heutige Frameworks basieren auf diesem Paradigma und es ist<br />

Teil des Lehrplans vieler Schulen und Universitäten. <strong>Die</strong> bekanntesten Vertreter<br />

des objektorientierten Paradigmas sind Java, C# und C ++ [InformatikLexikon,<br />

vgl. S. 701].<br />

Beim objektorientierten Entwurf wird oft UML (Unified Modeling Language) verwendet,<br />

eine grafische Beschreibungssprache zur Kommunikation von Zusammenhängen<br />

von Klassen und anderen Bausteinen. Es haben sich Entwurfsmuster<br />

aus zahlreichen objektorientierten Entwürfen herauskristallisiert, die in dem<br />

Buch [Entwurfsmuster] strukturiert dargestellt wurden. <strong>Die</strong>se Entwurfsmuster lösen<br />

häufig auftretende Probleme in objektorientierten Programmen, und gehören<br />

<strong>zum</strong> Handwerkszeug für OO-Programmierer. In anderen Programmierparadigmen<br />

können vergleichbare Probleme durch Konzepte des Paradigmas elegant gelöst<br />

werden. Eine Gegenüberstellung der objektorientierten Entwurfsmuster mit<br />

Konzepten des Funktionalen Paradigmas gibt [AnalyseEntwurfsmusterFP].<br />

1.6 Aufbau dieser Arbeit<br />

Als frühe Vertreter der deklarativen Programmierung werde ich Prolog und Miranda<br />

vorstellen. Es folgt sodann ein Katalog der deklarativen Programmierung<br />

mit F#. Im Kapitel Imperativer Stil (S. 44) werden einerseits einige imperativeobjektorientierte<br />

Konzepte beleuchtet, insbesondere alternative Techniken und<br />

Herangehensweisen im Vergleich <strong>zum</strong> deklarativen Stil. Andererseits werden Vorteile<br />

imperativer Programmierung dem deklarativen Ansatz gegenüber gestellt<br />

und Kombinationsmöglichkeiten vorgestellt. So können aus beiden Denkweisen<br />

8


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

einige Vorteile vereint werden. Ausführlicher werden Fallunterscheidungsmöglichkeiten<br />

aus beiden Paradigmen gegenübergestellt und verglichen. Dazu dient<br />

als Beispiel ein Code-Interpreter. Ein Fazit schließt die Arbeit.<br />

Zuvor habe eine Definition und Erklärung gegeben:<br />

„Deklarative Programmierung ist das Formulieren von Regeln und<br />

Zusammenhängen, die in ein ausführbares Programm überführt werden können.<br />

“<br />

„Bei deklarativer Programmierung konzentriert man sich auf die wesentliche<br />

Problemlösung und nicht auf Details der Umsetzung. “<br />

<strong>Die</strong> beiden oben genannten Sätze lassen sich auch zu einem zusammenfassen:<br />

„Deklarative Programmierung ist das Formulieren von Regeln und<br />

Zusammenhängen, die in ein ausführbares Programm überführt werden können,<br />

wobei man sich auf die wesentliche Problemlösung und nicht auf Details der<br />

Umsetzung konzentriert. “<br />

Zwei Techniken außerhalb von Programmiersprachenkonzepten möchte ich auch<br />

behandeln: DSLs und Metaprogrammierung. Zu deklarativen Sprachen zählt man<br />

auch domänenspezifische Sprachen (domain-specific languages, kurz DSLs), eingeschränkte<br />

fachspezifische Sprachen, die Vorzüge gegenüber generellen Programmiersprachen<br />

haben [DSLs, vgl. S. 33] und im gleichnamigen Abschnitt (S. 38)<br />

vorgestellt werden. Metaprogrammierung (S. 42) ist einerseits eine Techniken-<br />

Sammlung für DSLs und andererseits werden dadurch Programmtransformationen<br />

möglich, die den deklarativen Horizont erweitern. Beispielsweise können Programmteile<br />

in F# geschrieben werden, die in Code einer anderen Programmiersprache<br />

transformiert werden. <strong>Die</strong>s ist Thema des gleichnamigen Kapitels.<br />

9


2<br />

Deklarativer Programmierstil um 1980<br />

Der Begriff deklarative Programmierung wird ebenfalls als Sammelbegriff für funktionale,<br />

logikbasierte und constraintbasierte Programmierung verwendet [MultiparadigmenProg,<br />

vgl. S. 8]. Frühe Vertreter der deklarativen Programmierung<br />

sind LISP (1958), Miranda (1985) und Prolog (1972), sowie weniger bekannte<br />

Spezifikationssprachen, die jedoch im Allgemeinen nicht ausgeführt werden<br />

können und nur Beschränkungen formulieren [FPWithMiranda, vgl. S. 4]. Insbesondere<br />

ist der mit diesen genannten Sprachen verfolgte deklarative Ansatz ein<br />

völlig anderer als der Ansatz der imperativen Programmierung mit C-ähnlichen<br />

Sprachen.<br />

Der deklarative Ansatz soll mit meiner Definition abgeglichen und anhand von<br />

Prolog und Miranda als alte deklarative Sprachen illustriert werden.<br />

Eine wichtige Eigenschaft in diesem Zusammenhang ist die referenzielle Transparenz.<br />

Sie bedeutet, dass eine Funktion bei gleichen Argumenten immer denselben<br />

Wert produziert und der Wert nur von den Argumenten abhängig ist. <strong>Die</strong> Idee ist<br />

es, Gleiches mit Gleichem zu ersetzen; das heißt konkret, dass die Funktionsapplikation<br />

mit dem Ergebnis der Applikation ersetzt werden könnte. <strong>Die</strong>se Ersetzung<br />

nennt sich auch Auswertung oder Reduktion.<br />

In [FPWithMiranda, S. 2] wird der Ausdruck (z.B. der arithmetische Ausdruck<br />

2 * a + b * c) als Gemeinsamkeit aller deklarativen Sprachen genannt. <strong>Die</strong>ses<br />

Konstrukt taucht auch in der imperativen Programmierung oft auf. Es gibt aber<br />

Unterschiede - sogar innerhalb von deklarativen Sprachen:<br />

• Miranda und reine funktionale Programmiersprachen: keine Seiteneffekte<br />

und verzögerte Auswertung (laziness). Das Schreiben eines Ausdrucks führt<br />

dazu, dass gespeichert wird, wie das Ergebnis berechnet wird. <strong>Die</strong>s geschieht<br />

nur, wenn der Wert ausdrücklich für die weitere Berechnung benötigt wird.<br />

• Prolog: verzögerte Auswertung dank Strukturen. Das Schreiben eines Ausdrucks<br />

führt zur Konstruktion einer Struktur, d.h. eines Datenbehälters. Der<br />

Prolog-Ausdruck 1 + 2 * X ist lediglich eine Infix-Operator-Schreibweise der<br />

geschachtelten Struktur ’+’(1, ’*’(2, X)). Mithilfe des is-Prädikats kann<br />

10


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

der Wert berechnet werden. Arithmetik in Prolog wird dadurch erschwert<br />

und durch den relationalen Charakter müssen Rechenergebnisse an Parameter<br />

des Prädikats gebunden werden (anstatt wie in funktionalen Sprachen<br />

implizit einen Rückgabewert zu berechnen).<br />

• Funktionale Programmiersprachen ohne Laziness (z.B. F#): Arithmetik funktioniert<br />

intuitiv (wie in der Mathematik), evtl. muss aber auf Zahl-Typen<br />

geachtet werden (Ganz- und Gleitkommazahlen mit unterschiedlicher Genauigkeit)<br />

und Überläufe müssen behandelt werden.<br />

• Imperativ(-objektorientierte) Programmiersprachen (z.B. C#): Ausdrücke<br />

können nur an einigen Stellen stehen, etwa nach einem return, auf der rechten<br />

Seite einer Zuweisung oder in einer Argumentstelle eines Prozedur- bzw.<br />

Methodenaufrufs. Ansonsten verhalten sich Ausdrücke wie in funktionalen<br />

Programmiersprachen ohne Laziness.<br />

i, n > 0 zeigt sich<br />

der große Unterschied: Seq.reduce (*){1..n} als deklarativer F#-Ausdruck und in<br />

C# als imperative Berechnungsvorschrift:<br />

Auch wenn es Unterschiede bei Ausdrücken in deklarativen Sprachen gibt, ist es<br />

ihnen gemeinsam, dass die Ermittlung des Ergebnisses (Auswertungsstrategie) eines<br />

solchen Ausdrucks vom System übernommen wird. Es wird nur das Ergebnis<br />

spezifiziert, weniger der Weg, um es zu berechnen. Bei einfachen Ausdrücken,<br />

mag dies auf imperative Programmiersprachen ebenfalls zutreffen, aber bei aufwendigeren<br />

Ausdrücken wie der Fakultätsvorschrift n! = ∏ n<br />

i=1<br />

int prod = 1;<br />

for(int i = 1; i


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

2.1 Prolog<br />

Prolog ist eine deklarative Programmiersprache, die auf der Prädikatenlogik der<br />

ersten Stufe basiert und deren zentraler Mechanismus die Unifikation ist. Programmiert<br />

wird, indem in einer Wissensbasis (d.h. einer Datenbank) Prädikate<br />

mithilfe von Fakten und Regeln definiert werden, die in Abfragen (queries)<br />

benutzt werden können, um Informationen zu erhalten. Dazu wird vom System<br />

die Eingabe mit passenden Klauseln (Fakten oder Regeln) aus der Datenbank<br />

unifiziert und es werden Variablenbindungen ausgegeben. Der Unifikations-<br />

Mechanismus wird im Anschluss an die Beispiele erklärt.<br />

Prolog ist ein hervorragendes Beispiel für den deklarativen Stil, weil <strong>hier</strong> nur das<br />

Was definiert werden kann. Zuweisung und Sprünge sind nicht möglich. Es wird<br />

stattdessen mit Variablenbindung, Rekursion und Backtracking gearbeitet. Zudem<br />

ist Prolog eine mächtige, aber recht kompakte Sprache. Eine abschließende<br />

Bewertung folgt am Ende dieses Abschnitts.<br />

Ein zentraler Vorteil von Prologprogrammen ist die Richtungsunabhängigkeit von<br />

Prädikaten. Das klassische Beispiel ist die das Prädikat Append, die zwei Listen zu<br />

einer konkatenierten Liste transformieren kann. Gleichzeitig kann es in anderen<br />

Aufrufvarianten benutzt werden, z.B. zur Extraktion von Prä- und Suffixen und<br />

zur Generierung von Listen-Teilen.<br />

Eine Liste ist entweder die leere Liste (geschrieben als [], auch Nil genannt)<br />

oder ein Element gefolgt von einer Liste (angedeutet durch [_|_], auch Cons genannt).<br />

append([], X, X).<br />

append([Head|Tail], X, [Head|Result]) :- append(Tail, X, Result).<br />

/* Beispiele:<br />

append([1,2], [3,4], L). -> L = [1,2,3,4].<br />

append([1,2], Suf, [1,2,3,4]). -> Suf = [3,4].<br />

append(Pre, [3,4], [1,2,3,4]). -> Pre = [1,2].<br />

append(Pre, Suf, [1,2,3,4]). -><br />

Pre = [], Suf = [1,2,3,4];<br />

Pre = [1], Suf = [2,3,4];<br />

Pre = [1,2], Suf = [3,4];<br />

Pre = [1,2,3], Suf = [4];<br />

Pre = [1,2,3,4], Suf = [].<br />

*/<br />

Auffällig ist die Kürze des Append-Prädikats. Nur Zwei Zeilen sind nötig, um die<br />

Eigenschaften von Append zu formulieren:<br />

<strong>Die</strong> erste Zeile lässt sich so beschreiben: „<strong>Die</strong> Konkatenation der leeren Liste mit<br />

einer anderen Liste ist die andere Liste.“ <strong>Die</strong>se Zeile stellt den Rekursionsabbruch<br />

dar. <strong>Die</strong> zweite Zeile besagt: „Um eine nicht-leere Liste aus Head und Tail mit<br />

einer anderen Liste zu konkatenieren, wird der Head in die Ergebnisliste übernommen<br />

und der Rest des Ergebnisses wird durch die Konkatenation des Tails<br />

und der anderen Liste geliefert.“ Hier geschieht die Konsumption, die für das<br />

Terminieren der Rekursion wichtig ist, auf der ersten Argumentstelle.<br />

12


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Ein anderes wichtiges Prädikat, das mehrere Aufgaben erfüllt, die mit der Enthaltenseinbeziehung<br />

zusammenhängen, ist das Member-Prädikat:<br />

member(Head, [Head|_]).<br />

member(Element, [_|Tail]) :- member(Element, Tail).<br />

/* Beispiele:<br />

member(E, [1,2,3,4]). -> E = 1; E = 2; E = 3; E = 4.<br />

member(3, [1,2,3,4]). -> yes.<br />

member(5, [1,2,3,4]). -> no.<br />

member(5, L). -> L = [5|_]; L = [_,5|_]; L = [_,_,5|_]; ...<br />

*/<br />

Wieder lässt sich eine natürlichsprachliche Formulierung finden: „Der Kopf der<br />

Liste ist Element der Liste, ebenso wie Elemente des Restes der Liste.“<br />

Mit dem Prädikat kann man durch eine Liste iterieren, Werte auf Enthalten-sein<br />

prüfen und es lassen sich (unendlich viele) Listen generieren, die ein bestimmtes<br />

Element enthalten. <strong>Die</strong> letzte Ausgabe zeigt außerdem eine weitere Besonderheit<br />

von Prolog: die Verwendung von freien Variablen in Datenstrukturen. Eine weitere<br />

Eigenschaft von Prädikaten wird deutlich: <strong>Die</strong> Klauseln, aus denen das Prädikat<br />

be<strong>steht</strong>, müssen einander nicht ausschließen.<br />

2.1.1 Unifikation<br />

Der Unifikations-Mechanismus versucht, zwei Werte „gleich zu machen“, indem<br />

Variablenbindungen hergestellt werden. Sind zwei Terme nicht unifizierbar, ist<br />

das Ergebnis false (was <strong>zum</strong> Abbruch des Prädikats führt), andernfalls werden die<br />

Bindungen hergestellt und das Ergebnis ist true (was zur weiteren Ausführung des<br />

Prädikats führt).<br />

Der Mechanismus unterscheidet, welche zwei Werte miteinander unifiziert werden<br />

sollen. Es gibt in Prolog als Werte nur Atome (wie Symbole, Zahlen, Bool’sche<br />

Werte), Variablen und Strukturen. Da Unifikation symmetrisch ist, gibt es nur die<br />

folgenden Fälle (gebundene Variablen entsprechen ihrem gebundenen Wert, daher<br />

gibt es <strong>hier</strong> nur freie Variablen):<br />

FreieVariable = FreieVariable führt zur Koreferenz von zwei Variablen (wird im<br />

Folgenden eine der beiden gebunden, wird es die andere auch).<br />

FreieVariable = konstante führt zur Bindung der Variablen an die Konstante.<br />

FreieVariable = struktur(...) führt zur Bindung der Variablen an die Struktur.<br />

konstante = konstante unifiziert, weil die Konstanten gleich ist.<br />

struktur(...)= struktur(...) unifiziert, wenn die Stelligkeit der Strukturen übereinstimmt<br />

und alle Argumente rekursiv unifiziert werden können.<br />

2.1.2 Einordnung von Prolog<br />

Prolog wird oft für Wissensdatenbanken verwendet, denn Datenbankanfragen<br />

sind leicht zu schreiben, z.B. ist kunde(2, Name) die Abfrage des Namens des Kun-<br />

13


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

dens mit der Nummer 2. Voraussetzung ist das Wissen über die genaue Struktur<br />

der Datenbank; in dem Beispiel die Kenntnis, dass kunde eine zweistellige Relation<br />

ist, deren ersten Argumentstelle die Kundennummer und die zweite Stelle der<br />

Name ist. Eine Datenbank be<strong>steht</strong> aus Fakten:<br />

kunde(1, john).<br />

kunde(2, paul).<br />

kunde(3, george).<br />

kunde(4, ringo).<br />

<strong>Die</strong> Ausgabe für die Beispielabfrage kunde(2, Name) ist Name = paul.<br />

Der Bezug zur Definition „Formulieren von Regeln und Zusammenhängen“ ist<br />

durch folgende Eigenschaften hergestellt:<br />

• Formulieren ist das Schreiben von Prädikaten.<br />

• Regeln sollen Fälle unterscheiden: Das passiert in Prolog mithilfe von Prädikaten,<br />

die Fälle klassifizieren. <strong>Die</strong> Unterscheidungen nimmt der Prolog-<br />

Programmierer durch geeignete Klausel-Köpfe vor (im Append-Beispiel durch<br />

Unterscheidung nach [] oder [Head|Tail]). Zudem können Fälle im Klausel-<br />

Körper mithilfe von beliebigen Prädikaten genauer unterschieden werden.<br />

• Zusammenhänge führen <strong>zum</strong> Berechnungsergebnis. <strong>Die</strong>s ist mit funktionalen<br />

Zusammenhänge der Form X is Y + Z möglich, aber vor allem durch<br />

Verwendung der mithilfe von Prädikaten definierten Relationen.<br />

Mithilfe von Prolog kann das Problem auf hoher Abstraktionsebene beschrieben<br />

werden und das System übernimmt die Suche nach Lösungen. Prolog eignet sich<br />

zur Lösung von Logikrätseln, Beschreibung von Spielregeln, Datenbankabfragen,<br />

Listenverarbeitung, Suche in Hierarchien, Sprachverarbeitung und Anwendungen<br />

im Bereich der Künstlichen Intelligenz (KI). Eine weitere große Stärke ist<br />

Metaprogrammierung (Code-Generierung und -Auswertung) und die Definition<br />

eigener Operatoren, um eine eigene (domänen-spezifische) Sprache zu entwickeln.<br />

Berechnung von arithmetischen Ausdrücken wird durch die Vorgabe, Prädikate<br />

zu verwenden, erschwert. Das Schreiben von funktionalen Ausdrücken wie N - 1<br />

führt zur Erstellung der Struktur -(N, 1), welche mit dem is-Prädikat ausgerechnet<br />

werden kann. <strong>Die</strong> Quadrierungsfunktion x ↦→ x · x ist beispielsweise in Prolog<br />

als richtungsabhängiges Prädikat zu schreiben: square(N, NmalN):- NmalN is N *<br />

N.. Zustandsabhängige Programmierung ist durch spezielle Prädikate wie assert<br />

und retract (zur Veränderung der Wissensbasis) möglich.<br />

Außen vor gelassen wurde in dieser Betrachtung die Negation und der Cut, welche<br />

beide in realen Prolog-Anwendungen häufig verwendet werden.<br />

Prolog ist dynamisch typisiert, was insbesondere Listenverarbeitung leicht macht,<br />

wodurch sich aber einige Programmier-Fehler erst zur Laufzeit bemerkbar machen.<br />

14


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

2.1.3 Prolog und funktionale Programmierung<br />

<strong>Die</strong> Erben von Prolog sind unter anderem Curry und Mercury, zwei logikfunktionale<br />

Sprachen, die unter anderem schnellen C-Quelltext produzieren und dem<br />

Programmierer Features von Prolog und funktionalen Programmiersprachen wie<br />

Haskell (im Fall von Curry) bieten.<br />

Aktuell sind funktional-objektorientierte Sprachen wie Scala und F# auf dem<br />

Weg, populär zu werden. Das lässt sich durch aussagekräftigeren und kürzeren<br />

Quelltext, aber auch durch Vorteile bei paralleler Programmierung erklären.<br />

Es ist abzuwarten, ob sich logikfunktionale Sprachen durchsetzen werden.<br />

An der Universität in Kiel wird beispielsweise Curry gelehrt, wobei funktionale<br />

und Logik-Programmierung mittels einer Programmiersprache unterrichtet werden<br />

kann [Curry, S. 334].<br />

Das Konzept Pattern matching spielt in Miranda und anderen funktionalen Programmiersprachen<br />

eine wichtige Rolle. Pattern matching ist eine eingeschränkte<br />

Form der Unifikation, bei der nur die linke der beiden Seiten Variablen enthalten<br />

darf.<br />

2.2 Miranda<br />

In dem Buch Functional Programming with Miranda [FPWithMiranda] wird nicht<br />

nur die rein-funktionale Programmiersprache Miranda vorgestellt, es geht auch<br />

darum, verständlichen Quelltext zu schreiben, der an der Mathematik angelehnt<br />

ist. Dazu wird beispielsweise auf Funktionen und Rekursion zurückgegriffen.<br />

Programme, die in funktionalen Programmiersprachen geschrieben sind, bestehen<br />

aus Funktionen, die Eingabe-Werte auf Ausgabe-Werte abbilden. Das Attribut<br />

„rein-funktional“ fordert, dass alle Funktionen referenziell transparent (S. 10)<br />

sind.<br />

Gleichungen bestehen aus dem Musterabgleich (Pattern matching) auf der linken<br />

Seite und der Berechnungsvorschrift auf der rechten Seite.<br />

<strong>Die</strong>s stellt den Bezug zur Definition „Formulieren von Regeln und Zusammenhängen“<br />

her: Regeln werden durch Muster auf der linken Seite formuliert und die<br />

rechte Seite stellt den funktionalen Zusammenhang dar. Es können für eine Funktion<br />

mehrere Gleichungen geschrieben werden, um Fälle zu unterscheiden. Wie<br />

in anderen funktionalen Programmiersprachen werden anstelle von Schleifen-<br />

Konstrukten rekursive Funktionen verwendet.<br />

Referenzielle Transparenz verhindert Seiteneffekte 1 und führt zur Gleichbehandlung<br />

von Gleichheit und Identität von Werten. Außerdem bringt sie noch den<br />

Vorteil der Wiederverwendung von Datenstrukturen. Nur wenn auf Seiteneffekte<br />

verzichtet wird, ist verzögerte Auswertung (laziness) sinnvoll, welche wieder-<br />

1 Eine Manifestation von Seiteneffekten ist das Verändern des Zustandes eines Objektes, welches<br />

unter einem Alias, d.h. mehr als einem Namen [SICP, S. 295] zugreifbar ist.<br />

15


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

um die Arbeit mit unendlichen Listen ermöglicht. Gleichzeitig werden änderbare<br />

Arrays als wichtigste imperative Datenstruktur verhindert [FPWithMiranda, S.<br />

93] 2 . Eine gute Übersicht, wie mit dem Problem umgegangen wurde und welche<br />

Arten von „funktionalen Arrays“ entwickelt wurden, gibt [FuncArrays, S. S. 2-3].<br />

In neueren multiparadigmatischen Sprachen wie F# und Scala gibt es jedoch die<br />

Möglichkeit, auf imperative Arrays wie in C# oder Java zuzugreifen, weshalb<br />

nicht weiter auf funktionale Arrays eingegangen wird.<br />

Miranda verfügt als rein-funktionale Programmiersprache nicht über Zuweisungen.<br />

Stattdessen gibt es Variablenbindungen, die einen Bezeichner an einen Wert<br />

binden. Dank Laziness wird der Wert erst dann berechnet, wenn er wirklich zur<br />

Ermittlung eines Berechnungsergebnisses benötigt wird.<br />

Eine Besonderheit der Sprache Miranda ist, dass auf stukturierende Klammern wie<br />

etwa in if (Condition){ ThenBlock } else { ElseBlock } verzichtet wird. Stattdessen<br />

wird die sogenannte off-side rule verwendet, in der die Einrückungstiefe den<br />

Geltungsbereich von Variablen bestimmt, sodass gänzlich auf strukturierende<br />

Block-Klammern verzichtet werden kann. 3<br />

Eine zweite Besonderheit ist die Typinferenz, dank derer auf Typ-Annotation<br />

verzichtet werden kann, wodurch der Code von Typisierungs-Details befreit wird.<br />

Allein aus der Verwendung von Parametern kann deren Typ abgeleitet werden.<br />

Werden im Quellcode keine besonderen Operationen mit Werten durchgeführt,<br />

die auf den Typ schließen lassen, bleibt der Typ offen und funktioniert für alle<br />

konkreten Typen (generische Programmierung).<br />

<strong>Die</strong>se Eigenschaften führen zu schönen Programmen und Programmbestandteilen,<br />

wie etwa die bekannten Funktionen Map, Filter, Fold, die viele Iterationsaufgaben<br />

elegant lösen. Dazu werden Listen rekursiv verarbeitet, indem eine Funktion<br />

auf den Kopf und Rest der Liste hat zugreift und diese zu einem Ergebnis<br />

transformiert.<br />

||| Abbildung einer Liste mit Transformationsfunktion<br />

||| Beispiel: map (+1) [1,2,3,4] -> [2,3,4,5]<br />

map f [] = []<br />

map f (head:tail) = (f head) : (map f tail)<br />

||| Filterung einer Liste mit Prädikat<br />

||| Beispiel: filter (>1) [1,2,3,4] -> [2,3,4]<br />

filter pred [] = []<br />

filter pred (head:tail)<br />

= head : (filter pred tail), if pred(head)<br />

= filter pred tail, otherwise<br />

||| Zusammenfalten einer Liste mit Startwert und 2-stelliger Funktion<br />

||| Beispiel: fold 0 (+) [1,2,3,4] -> 10<br />

fold seed f [] = seed<br />

fold seed f (head:tail) = fold (f seed head) f tail<br />

2 Stattdessen werden oft verschiedene Arten von Bäumen verwendet.<br />

3 <strong>Die</strong> off-side rule wird ebenfalls in F# verwendet und bezieht sich dort unter anderem auf die<br />

Einrückungstiefe bei let-Bindungen. [FSharpSpec, vgl. S. 232ff.]<br />

16


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Abbildung 2.1: Vereinfachte Funktionsweise von Map, Filter und Fold<br />

[FPWithMiranda, nach S. 42ff.].<br />

<strong>Die</strong> Funktionsweise von Map, Filter und Fold lässt sich wie in Abbildung 2.2<br />

illustieren.<br />

Map, Filter und Fold sind sogenannte Funktionen höherer Ordnung, weil sie als Parameter<br />

Funktionen aufnehmen (<strong>hier</strong> f und pred). Pattern matching wird ähnlich<br />

wie in den Prolog-Prädikaten aus Abschnitt Prolog (S. 12) wieder benutzt, um<br />

den Rekursionsabbruch darzustellen.<br />

Mit partieller Anwendung von Funktionen (currying) lassen sich aus vorhandenen<br />

Funktionen neue Funktionen erstellen, die weniger Argumente benötigen. In dem<br />

Beispiel fold 0 (+)[1,2,3,4] wird deutlich, dass man die Summe einer Liste bilden<br />

möchte. Mathematiker kennen dafür die (n-stellige) Summe, die mit ∑ dargestellt<br />

wird. Programmierer sollten auch Zugriff auf dieses Konstrukt haben und könnten<br />

sich eine entsprechende Funktion definieren. Für das (n-stellige) Produkt gibt es<br />

∏ , welches sich auch definieren ließe. <strong>Die</strong> Integration von erweiterten Unicode-<br />

Zeichen in Programmiersprachen ist noch nicht gelungen, was an Schwierigkeiten<br />

bei der Eingabe mit der Tastatur und unflexiblen Parsern liegt. 4 Daher werden<br />

sprechende Namen statt mathematischer Symbole verwendet:<br />

sum = fold 0 (+)<br />

prod = fold 1 (*)<br />

||| n-stellige Summenfunktion<br />

||| n-stellige Produktfunktion<br />

Obwohl fold eine Funktion ist, die zur Berechnung drei Argumente benötigt, lassen<br />

sich auch weniger Argumente übergeben, sodass eine Funktion konstruiert<br />

wird, die nur noch auf das letzte Argument „wartet“, um das Ergebnis zu berechnen.<br />

Zwecks Dokumentation verzichtet man in Funktionsdefinitionen gelegentlich<br />

auf diese Eigenschaft und schreibt sum numbers = fold 0 (+)numbers.<br />

Abgesehen von Typen, die später genauer betrachtet werden, sind dies die Kernideen<br />

vieler funktionaler Programmiersprachen. Eine kurze Auflistung von Eigenschaften<br />

funktionaler Programmierung, mit Miranda als Beispiel für eine rein<br />

funktionale Programmiersprache, bietet [FPWithMiranda, S. 8-9]:<br />

statt.<br />

• Algorithmen können funktional mit Klarheit und Kürze formuliert werden.<br />

• Kürzere Programme sind leichter zu schreiben, zu testen, anzupassen und<br />

zu warten.<br />

• Funktionale Programmiersprachen sind mathematisch formalisierbar und<br />

lassen sich durch referenzielle Transparenz leichter parallelisieren.<br />

4 Eine Betrachtung des Themas Unicode in Programmiersprachen findet in [FunktProg, S. 4-5]<br />

17


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

• <strong>Die</strong> Nachteile gegenüber prozeduralen Programmen waren jedoch geringere<br />

Verfügbarkeit (der Laufzeitumgebung für Miranda beispielsweise), fehlende<br />

Interoperabilität zur Systemprogrammierung (Betriebssystem-, Datenbankbibliotheken)<br />

und geringere Performanz.<br />

<strong>Die</strong> eben genannten Nachteile wurden durch intensive Forschung reduziert und<br />

neue funktionale Programmiersprachen lösen diese Anforderungen zufriedenstellend.<br />

<strong>Die</strong> Integration mit anderen Programmiersprachen und Interoperabilität<br />

von Programmbibliotheken ist auch durch gemeinsame Frameworks und Laufzeitumgebungen<br />

gelungen.<br />

18


3<br />

Deklarativer Stil um 2000<br />

Im Zusammenhang mit paralleler Programmierung und Daten-basierten Programmen<br />

sind funktionale Programmiersprachen für heutige Programmierung wichtig<br />

geworden. Da Objektorientierung oft auf einem imperativen Kern aufbaut, ist das<br />

Paradigma für parallele Ausführung weniger geeignet, wohingegen reine funktionale<br />

Programmierung weiterhilft [RealWorldFP, S. 10-11]. Logikprogrammierung<br />

ist im Bereich der KI wichtig, für typische Anwendungs-Programmierung<br />

aber nicht ausgelegt (gemeint sind z.B. grafische Benutzerschnittstellen, Webentwicklung<br />

oder Computer-Spiele).<br />

Symbol- und Zahlenmanipulation ist einer der Kernbereiche der funktionalen Programmierung<br />

und die Integration mit objektorientierten popuären Frameworks<br />

wie .NET und dem Java-Framework erlaubt den Umstieg von objektorientierten<br />

Programmiersprachen zu funktionalen Sprachen.<br />

Es gibt viele funktionale Programmiersprachen, die ihre Vorteile und Eigenheiten<br />

haben, vor allem was Effizienz und Interoperabilität anbelangt. Eine neue<br />

bzw. neu entdeckte Entwicklung sind multiparadigmatische Programmiersprachen.<br />

Mit ihnen lassen sich Paradigmen wie Objektorientierung und funktionale<br />

Programmierung vereinen. Bekannte Vertreter sind Scala für die JVM als Programmiersprache,<br />

die gut mit Java-Bibliotheken zusammenarbeitet, und F# für<br />

das .NET-Framework. Beide sind statisch typisierte funktionale Programmiersprachen,<br />

verfolgen aber im Gegensatz zu Miranda strikte Auswertung und übernehmen<br />

das objektorientierte Typsystem aus dem zugrundeliegenden Framework<br />

(Java oder .NET).<br />

Scala ist als besseres Java entworfen worden und löst viele typische Probleme aus<br />

Java. <strong>Die</strong> vielen funktionalen Aspekte von Scala lassen sich bei der Programmierung<br />

für die Java-Plattform gewinnbringend einsetzen. Der Name „Scala“ <strong>steht</strong><br />

für scalable Language und bezieht sich auf die (im Vergleich zu Java) flexible Syntax<br />

und Erweiterbarkeit der Programmiersprache.<br />

F# wurde ursprünglich als OCaml-Variante für .NET entwickelt, bietet nun aber<br />

von ML unabhängige Konzepte, welche die Programmierung durch geeignete Modularisierung<br />

erleichtern. Für C#-Programmierer ist F# durch seine Kürze und<br />

19


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Aussagekraft eine gute Alternative. Dem Programmierer wird nicht nur oftmals<br />

das lästige Schreiben von Typannotationen abgenommen, die Programme sind<br />

durch ihren funktionalen Charakter meist kürzer, verständlicher und näher an der<br />

Intention des Autors als vergleichbare C#-Programme. Da F# zudem auch Werkzeugunterstützung<br />

bietet, ist es eine gute Wahl für .NET-Programmierer. An dieser<br />

Stelle soll keine Beschreibung der Werkzeugunterstützung stattfinden, es soll<br />

nur erwähnt sein, dass der Editor Autovervollständigung und Kontext-Hilfe besitzt<br />

sowie über einen interaktiven Prompt verfügt. Der interaktive Prompt nennt<br />

sich F# interactive und wird im Rahmen von Quellcode-Beispielen in dieser Arbeit<br />

benutzt. <strong>Die</strong> Notation ist wie folgt zu lesen:<br />

> Ausdruck ;;<br />

val it: Typ = Wert<br />

Hinter dem Zeichen > zur Eingabeaufforderung wird ein Ausdruck eingegeben;<br />

die Eingabe wird mit der Endesequenz ;; abgeschlossen. <strong>Die</strong> Ausgabe enthält<br />

den Typ und den Wert des ausgewerteten Ausdrucks.<br />

Modularisierung und Aussagekraft sind zwei treibende Faktoren bei der Verwendung<br />

einer Programmiersprache. F# bietet in dieser Hinsicht neue Techniken,<br />

die richtig verwendet zur deklarativen Programmierung führen. Deshalb wird im<br />

Folgenden F# als Beispiel für moderne deklarative Programmierung benutzt und<br />

analysiert. Dazu wird zunächst ein Muster vorgestellt, mit dem die wichtigen<br />

Konzepte besprochen und abgewägt werden können.<br />

3.1 Analyse von deklarativen Sprachelementen<br />

Programmiersprachen werben mit Features: In der einen Programmiersprachen<br />

wird eine typische Aufgabe mit weniger Schreibaufwand gelöst, als in einer anderen.<br />

Design von Programmiersprachen ist eine hohe Kunst, jedoch keine elitäre<br />

Angelegenheit mehr. Tools und Tutorials können dem Laien die Konstruktion<br />

einer Programmiersprachen erleichtern. Viele Programmiersprachen sind Verschmelzungen<br />

von anderen ähnlichen Programmiersprachen unter Hinzunahme<br />

von Eigenschaften und Konstrukten nach Geschmack des Konstrukteurs.<br />

Gerade deshalb ist ein roter Faden, der im Programmiersprache-Design zu finden<br />

sein muss, Pflicht. <strong>Die</strong> Orientierung an einem Paradigma wie dem funktionalen<br />

oder dem objektorientierten kann solch ein Faden sein; die Angst jedoch, einer<br />

anderen Sprache in etwas nachzustehen, verführt jedoch wieder zur Hinzunahme<br />

von beliebigen Features und <strong>zum</strong> Verlust dieses Fadens. 1<br />

Ich schlage eine Struktur vor, mit der Programmiersprachenfeatures im Allgemeinen<br />

und deklarative Sprachelemente im Speziellen analysiert werden können.<br />

In dieser Arbeit liegt das Ziel der Analyse darin, deklarative Sprachelemente zu<br />

1 In dem Zusammenhang könnten multiparadigmatische Programmiersprachen als unstrukturiert<br />

erscheinen; bei genauerer Betrachtung werden jedoch Paradigmen integriert, um einen eleganten<br />

Problemlösungsweg auch auf konzeptueller Ebene zu ermöglichen.<br />

20


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

vergleichen. Das Schema beleuchtet kurz folgende Aspekte eines Sprachelements<br />

oder einer Technik der Programmiersprache:<br />

Technik:<br />

Zweck:<br />

Problem:<br />

Lösung:<br />

Performanz:<br />

Aussagekräftige Bezeichnung für das Konstrukt<br />

Kurze Beschreibung des Nutzens<br />

Probleme, die damit gelöst werden können<br />

Verwendung dieses Konstruktes<br />

Illustration der Umsetzung / Ausführung des Konstruktes<br />

Alternative: Verwandte Techniken, die ähnliche Probleme lösen können oder<br />

alternativer Ansatz der Lösung<br />

Anhand von Beispielen werden danach Vor- und Nachteile bei der Verwendung<br />

illustriert. Aus den Beispielen muss außerdem die Syntax des Sprachelementes<br />

erkennbar sein.<br />

3.2 Deklarative Elemente von F#<br />

<strong>Die</strong> folgenden Elemente von F#, die deklarativen Charakter haben, lassen sich<br />

wie folgt einteilen:<br />

• Typen: strukturierte Daten und polymorphe Objekte<br />

– Definition und Verwendung von Typen (S. 21)<br />

• Patterns: Arbeiten mit Daten und Objekten sowie Möglichkeiten der Fallunterscheidung<br />

– Pattern matching (S. 26)<br />

– Active patterns (S. 29)<br />

• Interne DSLs: eine Sprache in der Sprache<br />

– Computation expressions (S. 34)<br />

– Quotations (S. 42)<br />

3.2.1 Definition und Verwendung von Typen<br />

Unter einem Datentyp (im folgenden nur Typ) ver<strong>steht</strong> man „eine Menge von<br />

zulässigen Werten als Operanden und Ergebnisse sowie von darauf anwendbaren<br />

Operationen in einer Programmiersprache“ [InformatikLexikon, S. 215].<br />

Typ-Definitionen in F# sind dank der aus ML geborgten Notation aussagekräftiger<br />

als in C#. Insbesondere algebraische Datentypen sind bei der Programmierung<br />

hilfreich und wären in C# und anderen objektorientierten Sprachen aufwendig<br />

zu formulieren.<br />

21


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

In F# gibt es folgende Arten von Typen 2 :<br />

• Funktionstypen (S. 22)<br />

• Algebraische Datentypen und generische algebraische Datentypen (S. 23)<br />

• Datensätze (S. 24)<br />

• Klassen (S. 25)<br />

• Interfaces (S. 25)<br />

• Structs (S. 25)<br />

Funktionstyp<br />

In einer funktionalen Programmiersprache ist das wichtigste Element die Funktion.<br />

Eine Funktion bildet Eingabe-Werte auf Ausgabe-Werte ab.<br />

Funktionen sind Werte von Funktionstypen. Funktionstypen haben die Form<br />

Eingabetyp -> Ausgabetyp, wobei Eingabe- und Ausgabetyp beliebige Typen sind.<br />

Der Typ der Quadrierungsfunktion (x ↦→ x 2 ) ist leicht verständlich: int -> int.<br />

Im Fall von partieller Applikation sehen Funktionstypen etwas interessanter aus.<br />

Arithmetische Operationen (etwa +, -, *, /) und Vergleiche (wie >, <br />

(int -> int). Der Eingabetyp ist int, d.h. der erste Eingabewert ist eine ganze<br />

Zahl und der Ausgabetyp ist eine Funktion vom Typ int -> int. <strong>Die</strong>s stellt den<br />

Typen der Funktion dar, die das zweite Argument erwartet. <strong>Die</strong> Klammerung von<br />

Funktionstypen ist rechtsassoziativ, deshalb wird statt int -> (int -> int) gerne<br />

int -> int -> int geschrieben.<br />

Ein Sonderfall von Funktionstypen sind die Typen von Funktionen höherer Ordnung.<br />

Map funktioniert auf Listen eines beliebigen aber festen Typs (nennen wir<br />

den Elementtyp ’a). Der Funktionstyp von Map lautet (’a -> ’b)-> ’a list -><br />

’b list. Es wird als erstes Argument eine Funktion erwartet, die einen Wert vom<br />

Typ ’a auf einen Wert vom Typ ’b abbildet (man sagt kürzer „ein ’a auf ein ’b<br />

abbildet“. <strong>Die</strong> Klammerung um Funktionstypen bei Parametern ist wichtig.) Als<br />

zweites Argument wird eine Liste von ’as erwartet; wird die Funktion darauf angewendet,<br />

wird eine Liste von ’bs geliefert, deren Elemente aus den Elementen<br />

der Eingabeliste und der Abbildungsfunktion entstehen.<br />

<strong>Die</strong> Definition von Funktionstypen geschieht in F# implizit durch das Definieren<br />

von Funktionen.<br />

2 Enum-, Delegat-, Exception- und Maßeinheit-Typen werden <strong>hier</strong> nicht aufgeführt, weil es sich<br />

um besondere Structs oder Klassen bzw. Maßeinheiten-Metadaten handelt. Genaueres über diese<br />

Typen findet sich in der F#-Spezifikation [FSharpSpec, vgl. S. 115-116].<br />

22


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Algebraischer Datentyp<br />

Algebraische Datentypen (kurz ADT, auch union types) sind Typen, die eine abgeschlossene<br />

Anzahl an Varianten haben. Sind den Varianten keine zusätzlichen<br />

Daten zugeordnet, sind dies Werte des Typs; andernfalls nennt man die instanziierten<br />

Varianten Werte des Typs. Varianten sind oft Daten von Produkttypen<br />

zugeordnet. Produkttypen sind die Typen von n-Tupeln (Typen die durch Bildung<br />

des kartesischen Produktes über n Typen entstehen).<br />

Beispiele sind Bool’sche Werte (Typ bool mit den zwei Varianten bzw. Werten<br />

true und false) oder das Nulltupel (Typ unit mit dem Wert ()), aber auch Datenbehälter<br />

wie etwa der Typ aus einer Variante Punkt (mit allen Werten der Form<br />

Punkt(a, b) mit konkreten Ganzzahlen a und b).<br />

Wenn man von mehr als einer Variante spricht, nennt man diese auch Summentypen<br />

und verwendet sie um „inhaltlich verwandte Elemente, die aber strukturell<br />

unterschiedlich aufgebaut sein können, zusammenzufassen“ [FunktProg, S.<br />

17].<br />

Wichtig sind vor allem generische algebraische Datentypen (kurz GADT), die<br />

beliebige aber festen Elementtypen enthalten können, sodass strukturierte Daten<br />

konstruiert und verarbeitet werden können. Das klassische Beispiel ist eine Liste<br />

eines bestimmten Elementtyps. Eine Liste von ’a (Liste von Elementtyp ’a) lässt<br />

sich so definieren:<br />

• <strong>Die</strong> leere Liste ist eine Liste von ’a.<br />

• Eine Element vom Typ ’a gefolgt von einer Liste von ’a ist eine Liste von<br />

’a.<br />

• Nur diese beiden Varianten erzeugen Listen von ’a.<br />

Den beiden Varianten müssen Namen gegeben werden, um mit ihnen zu arbeiten:<br />

<strong>Die</strong> leere Liste wird Nil und das Paar von Element und Liste wird Cons genannt.<br />

[1,2,3,4] ist eine Liste von Ganzzahlen (int list) und ist aus den beiden Varianten<br />

konstruiert: Cons(1, Cons(2, Cons(3, Cons(4, Nil)))). Eine wichtige Infix-<br />

Schreibweise ist 1::2::3::4::[], wobei [] für Nil und a :: b für Cons(a, b) <strong>steht</strong><br />

und rechtsassoziativ ist (a :: b :: c = a :: (b :: c)). <strong>Die</strong> Liste stellt eine der<br />

wichtigsten Datenstrukturen in der funktionalen Programmierung dar. Sie ist<br />

ein Beispiel für rekursiv definierte Typen; ein anderes Beispiel ist der Typ von<br />

Binärbäumen. Der Binärbaum Node(2, Node(1, Node(0, Empty, Empty), Empty),<br />

Node(4, Node(3, Empty, Empty), Node(5, Empty, Empty))) lässt sich wie in Abbildung<br />

3.1 visualisieren.<br />

Andere wichtige generische algebraische Datentypen sind die Typen von Tupeln<br />

(’a * ’b), Tripeln (’a * ’b * ’c), Quadrupeln (’a * ’b * ’c * ’d) usw., wobei<br />

die Typen der Komponenten beliebig aber fest sind. Algebraische Datentypen mit<br />

einer Variante sind nützlich, aber eher ungebräuchlich, da auf Tupel oder Datensätze<br />

für diesen Zweck zurückgegriffen werden kann. Außerdem wird im Falle<br />

von partiell definierten Funktionen Gebrauch vom generischen Option-Typ gemacht,<br />

der aus den Varianten None oder Some(x) mit Werten x vom Typ ’a be<strong>steht</strong>.<br />

23


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Abbildung 3.1: Ein Binärbaum<br />

Z.B. kann die ganzzahlige Division so dargestellt werden, dass für Werte a und b<br />

das Ergebnis None im Fall von b = 0 und Some(a/b) sonst ist.<br />

Algebraische Datentypen werden in F# folgendermaßen definiert:<br />

type Unit = Unit // eingebaut als Typ unit<br />

type Bool = True | False // eingebaut als primitiver Typ bool<br />

type Punkt = Punkt of int * int<br />

type List = Nil | Cons of ’a * List<br />

// eingebaut als Typ list<br />

type Binary = Empty | Node of ’a * Binary * Binary<br />

type Option = None | Some of ’a // eingebaut als Typ option<br />

type Tupel = Tupel of ’a * ’b // eingebaut als Typ ’a * ’b<br />

type Tripel = Tripel of ’a * ’b * ’c<br />

// eingebaut als Typ ’a * ’b * ’c<br />

Wie eingangs erwähnt und wie aus den Beispielen deutlich wird, ist die Notation<br />

äußerst kompakt. Vergleichbare Definitionen in objektorientierten Sprachen<br />

sind wesentlich länger, weil dem Programmierer auferlegt ist, Klassen, Unterklassen,<br />

Konstruktoren und Felder zu schreiben. Außerdem sind korrekte Equals- und<br />

GetHashCode-Methoden zu implementieren. Gerade bei algebraischen Datentypen<br />

muss sichergestellt werden, dass keine weiteren Varianten durch Unterklassenbildung<br />

hinzugefügt werden können.<br />

Der deklarative Stil der obigen Defintionen ist bemerkenswert. Nicht nur bei der<br />

Definition, sondern auch bei der Verwendung haben algebraische Datentypen<br />

Vorteile gegenüber Klassen, siehe dazu den Eintrag „Algebraischer Datentyp“ im<br />

Abschnitt Pattern Matching (S. 27).<br />

Datensatz<br />

Datensätze (record types) sind Typen, die als Datenbehälter fungieren. <strong>Die</strong> Verwendung<br />

von Datensätzen ist Tupeln vorzuziehen, wenn komplexere Datenstrukturen<br />

als Argument übergeben oder von Funktionen geliefert werden. Tupeln gegenüber<br />

haben die Bestandteile von Datensätzen keine feste Ordnung, jedoch einen<br />

Namen.<br />

Eine Person sollte in einem Programm nicht als Tupel vom Typ (string * string)<br />

dargestellt werden, wobei die Komponenten Vorname und Nachname darstellen<br />

sollen. Stattdessen kann ein Datensatz definiert werden:<br />

24


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

type Person = { Vorname: string; Nachname: string }<br />

// Beispiel für ein Datensatzobjekt:<br />

let MsCarter = { Vorname = ”Samantha”; Nachname = ”Carter” }<br />

<strong>Die</strong> Definition solcher Datenbehälter ist auf das Nötigste reduziert; insbesondere<br />

muss man keine Konstruktoren oder Equals-Methoden implementieren, was fehlerträchtig<br />

sein kann und in vielen objektorientierten Sprachen für jeden Typen<br />

von Hand getan werden muss; eine Betrachtung dazu findet sich im Abschnitt<br />

Imperativ-objektorientierte Typen (S. 47). Zwei zusätzliche Eigenschaften sind<br />

bei Datensätzen zu erwähnen:<br />

1. Muster zur Extraktion von Daten stehen zur Verfügung, siehe dazu den Eintrag<br />

„Datensatz“ im Abschnitt Pattern Matching (S. 27).<br />

2. Geringfügige Änderungen von Datensätzen können mit dem with-Operator<br />

durchgeführt werden, wobei eine neuer Datensatz erstellt wird, welche dem<br />

Vorlage-Datensatz-Objekt bis auf die angegeben Felder gleicht:<br />

let MrsMcKay = { MsCarter with Nachname = ”Mc Kay” }<br />

Klasse und Interface<br />

Eine Klasse ist der Typ eines Objektes. Objekte haben einen internen Zustand, der<br />

nur durch Operationen verändert oder sondiert werden darf. Klassen können voneinander<br />

abgeleitet sein, wodurch die Unterklasse Eigenschaften (Zustand und<br />

Verhalten) von der Oberklasse übernimmt. Der Zustand kann in Unterklassen<br />

erweitert und Verhalten abgeändert werden. <strong>Die</strong> Menge der Operationen einer<br />

Klasse ist ihre Schnittstelle.<br />

Interfaces sind lediglich eine Menge von Operationen ohne Implementierung.<br />

Klassen können Interfaces implementieren, wodurch Objekte dieser Klassen unter<br />

der Sicht des entsprechenden Interfaces benutzt werden können.<br />

Das Aufrufen von Operationen an Objekten und das Auswählen der klassenspezifischen<br />

Methode, um die Operation auszuführen, nennt sich dynamisches Binden.<br />

<strong>Die</strong> Idee ist, dass der Aufruf einer Operation nicht fest an eine Methode<br />

gebunden ist, sondern flexibel zur Laufzeit ausgetauscht werden kann. Das Austauschen<br />

geschieht über Subtyp-Polymorphie (Subtypen sind die oben erwähnten<br />

Unterklassen; Polymorphie <strong>steht</strong> für Vielgestaltigkeit), d.h. Klassenvererbung<br />

und Überschreiben von Methoden. <strong>Die</strong>se Flexibilität ist Grundlage vieler Entwurfsmuster<br />

[Entwurfsmuster, vgl. S. 16] und wird als Technik im Abschnitt<br />

Imperativ-objektorientierte Fallunterscheidungen (S. 51) erläutert.<br />

Struct<br />

Structs stellen Typen von Werten dar, die nicht wie Objekte auf dem Heap sondern<br />

auf dem Aufrufstack gespeichert werden. <strong>Die</strong>ser Aspekt hat und Vor- und<br />

25


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Nachteile, die erst betrachtet werden müssen, wenn Performanz eine Rolle spielt.<br />

<strong>Die</strong>se Betrachtung wird im Abschnitt Imperativ objektorientierte Typen (S. 47)<br />

vorgenommen.<br />

3.2.2 Pattern matching<br />

Technik:<br />

Zweck:<br />

Pattern matching<br />

Fallunterscheidung und Wertextraktion<br />

Problem: Typprüfungen gehen mit Typecasts und Datenextraktion einher, sodass<br />

die zu unterscheidenden Fälle aufwendig zu programmieren sind. Selektor-<br />

Funktionen (OO: Getter) werden exzessiv genutzt, um Daten zu extra<strong>hier</strong>en.<br />

Lösung: Pattern matching auf Berechnungsergebnisse und Parameter anwenden.<br />

Konstrukte: match Ausdruck with Fälle oder function Fälle<br />

Performanz: <strong>Die</strong> Fälle werden in if-else-Ketten umgewandelt, switch wird im<br />

Fall von primitiven Datentypen und algebraischen Datentypen verwendet.<br />

Alternative: if-else-Ketten, explizite Typprüfungen und Datenextraktion, switch,<br />

Try-Methoden, Subtyp-Polymorphie, Visitor-Pattern<br />

Bereits in Miranda war Pattern matching eine Technik, um Fälle zu unterscheiden<br />

und gleichzeitig Werte aus strukturierten Daten zu extra<strong>hier</strong>en. Explizit zu<br />

schreibende Typprädikate und Selektoren wie etwa in Scheme im Sinne von [?]<br />

werden damit unnötig, gleichzeitig geht aber auch die in dort motivierte Repräsentationsflexibilität<br />

verloren. Mehr zu diesem Punkt findet sich in der Evaluation<br />

der Fallunterscheidungen (S. 54).<br />

Um Pattern matching zu verstehen, müssen die verschiedenen Muster (Patterns)<br />

analysiert werden, vom einfachsten <strong>zum</strong> komplexesten:<br />

Variable Wenn auch nicht sofort als Pattern ersichtlich, ist eine Variable ein<br />

Muster, das immer funktioniert und eine Variable bindet. Ein Spezialfall der<br />

Variable ist der Unterstrich, mit dem ausgedrückt wird, dass die Variable<br />

nicht gebunden werden soll.<br />

Literal Literale sind Werte primitiver Datentypen, z.B. true, 42, 0.1, ”string”,<br />

’x’. Sie passen genau auf diese Werte.<br />

N-Tupel Muster der Form (a,b), (a,b,c) usw. passen auf Werte vom entsprechenden<br />

Tupel-Typ. Wichtig ist <strong>hier</strong>, dass a, b und c wiederum Muster sind (in<br />

diesem Fall Variablen).<br />

Liste [] passt genau auf die leere Liste. a::b passt auf ein Cons-Paar (a und b sind<br />

wieder Muster). Listen mit expliziter Länge wie etwa [a;b;c] sind nur eine<br />

alternative Schreibweise für a::b::c::[].<br />

Array Da Arrays immer eine feste Länge haben, wird die Länge im Muster implizit<br />

angegeben: Beispielsweise passt [||] auf das leere Array und [|a;b;c|]<br />

passt genau auf ein Array der Länge 3.<br />

26


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Algebraischer Datentyp <strong>Die</strong> Varianten eines algebraischen Datentyps können<br />

als Muster verwendet werden. Dazu wird der Variantenname mit den zugehörigen<br />

Daten notiert. Für den Typ, der mit type Punkt = Punkt of int<br />

* int definiert wird, wäre ein Muster, das auf einen Wert vom Typ Punkt<br />

passt Punkt(x,y). Auch Punkt tupel ist möglich, da der Inhalt eines Punktes<br />

ein Tupel ist.<br />

Datensatz Ein Datensatz-Muster kann Felder eines Datensatz-Objektes extra<strong>hier</strong>en.<br />

Für den Typ, der mit<br />

type Person = { Name: string; Geburtstag: DateTime }<br />

definiert wird, stellt z.B. { Name = x } ein Muster dar, der den Wert des Feldes<br />

Name an die Variable x bindet. Anstelle von x kann wieder ein Muster<br />

verwendet werden; eine beliebige Anzahl an Feldern (mindestens jedoch<br />

eines) kann so extra<strong>hier</strong>t werden.<br />

Konjunktion (&-Muster) Zwei Muster können mit & verknüpft werden, um ein<br />

neues Muster zu bilden, das passt, wenn beide Bestandteile passen. Ein Beispiel<br />

findet sich bei Active Patterns (S. 34).<br />

Disjunktion (|-Muster) Zwei Muster können mit | verknüpft werden, um ein<br />

neues Muster zu bilden, das passt, wenn mindestens ein Bestandteil passt.<br />

(Trifft das erste Muster zu, passt die Disjunktion; ansonsten wird das zweite<br />

Muster abgeglichen.) Z.B. ist (1|2|3) ein Muster, das auf eine Zahl passt,<br />

die 1, 2 oder 3 ist. Stellt eines der Muster Bindungen her, müssen alle Bestandteile<br />

diese Bindungen herstellen, sodass nach Erfolg des Musters auf<br />

dieselben Variablen zugegriffen werden kann.<br />

Muster mit Bindung Einem Muster kann eine Bindung hinzugefügt werden, sodass<br />

der Wert, auf den das Muster passt, an eine Variable gebunden wird.<br />

<strong>Die</strong> Syntax ist Muster as Bezeichner.<br />

Typtest Das Typtest-Muster passt auf einen Wert, wenn der Laufzeittyp des Wertes<br />

gleich oder ein Subtyp vom angegebenen Typ ist. <strong>Die</strong> Syntax ist :? Typ .<br />

Insbesondere in Verbindung mit einer Bindung (as) kann ein Typtests mit einem<br />

Typecast kombiniert werden. Ein gutes Beispiel ist die Equals(Object)-<br />

Methode:<br />

type Pos(x: int, y: int) =<br />

member this.X = x<br />

member this.Y = y<br />

override this.GetHashCode() = this.X ^^^ this.Y // X xor Y<br />

override this.Equals(other: obj) =<br />

match other with<br />

| :? Pos as other -><br />

this.X = other.X && this.Y = other.Y<br />

| _ -> false<br />

Muster mit Guard Einem Muster kann ein Guard hinzugefügt werden, d.h. ein<br />

Bool’scher Ausdruck, der ausgewertet wird, wenn das Muster passt, und<br />

true sein muss, damit das Muster mit Guard passt. <strong>Die</strong> Syntax ist Muster<br />

when Ausdruck. Ein Muster mit Guard ist kein kompositionierbares Muster.<br />

27


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Active Patterns Active Patterns werden im gleichnamigen Abschnitt (S. 29) erklärt,<br />

wobei sie sogar parametrisiert sein können und zusätzliche Bindungen<br />

herstellen können.<br />

<strong>Die</strong> vorgestellten Patterns werden üblicherweise in Konstrukten wie match Ausdruck<br />

with Fälle verwendet, wobei Fälle eine Auflistung von Fällen der Form Muster<br />

-> Behandlung ist. Da folgende gedankliche Äquivalenzen gelten 3 , können Muster<br />

an vielen Stellen im Quelltext auftauchen:<br />

let Muster = Ausdruck in Körper<br />

≈match Ausdruck with Muster -> Körper<br />

fun (Muster)Parameter -> Körper<br />

≈fun x Parameter -> match x with Muster -> Körper<br />

function Muster -> Körper<br />

≈fun x -> match x with Muster -> Körper<br />

<strong>Die</strong>s stellt die beiden wichtigsten Anwendungsgebiete außerhalb von match-with<br />

dar: lokale Variablen und Parameter. An dieser Stelle sind Muster nur als Alternative<br />

für Typ-/Datenprüfung und Datenextraktion zu sehen, erst im nächsten<br />

Abschnitt Active Patterns (S. 29) wird die damit verbundene Modularität deutlich.<br />

Muster sind deklarativ, weil deutlich formuliert wird, welchen Fall dieses Muster<br />

abdeckt und Zugriff auf Werte gegeben wird, ohne diese explizit extra<strong>hier</strong>en zu<br />

müssen. Der Gegensatz dazu ist z.B. explizite Typprüfung und Wert-Extraktion.<br />

<strong>Die</strong>se und andere Herangehensweisen werden im Teil Imperativ-objektorientierte<br />

Fallunterscheidungen (S. 51) untersucht.<br />

Muster wurden im Prolog-Abschnitt (S. 12) schon für ihre kompakte Darstellung<br />

von Fallunterscheidungen gezeigt; ein wichtiger Unterschied ist jedoch, dass in<br />

F# (anders als in Miranda) Koreferenz in Mustern nicht erlaubt ist. Von in Muster<br />

vorkommenden Variablen mit gleichen Namen wird in Miranda gefordert, dass<br />

sie den gleichen Wert besitzen. In F# muss dies explizit geschrieben werden. Das<br />

Member-Beispiel aus Prolog ist in Miranda dank Koreferenz ähnlich aussagekräftig:<br />

member x [] = false<br />

member x (x:_) = true<br />

member x (_:tail) = member x tail<br />

In F# muss die Gleichheit explizit (z.B. mit einem Guard) geschrieben werden.<br />

Außerdem ist member als Schlüsselwort zur Definition von Methoden reserviert,<br />

weshalb <strong>hier</strong> der Name isMember benutzt wird:<br />

3 <strong>Die</strong>se Äquivalenzen gelten z.B. nicht, wenn in Körper auf in der Funktion definierte<br />

mutable-Variablen zugegriffen wird (bei Klassen-Feldern ist dies wiederum erlaubt), weil für<br />

fun/function gewisse Eigenschaften von Closures eingehalten werden. Details dazu finden sich<br />

in [FSharp, vgl S. 56].<br />

28


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

let rec isMember x = function<br />

| [] -> false<br />

| head :: tail when x = head -> true<br />

| _ :: tail -> isMember x tail<br />

<strong>Die</strong> gezeigte rekursiven Definition lässt sich durch Funktionen höherer Ordnung<br />

in F# kürzer ausdrücken: let isMember x = List.exists((=)x) Hier geschieht die<br />

Iteration der Liste nicht explizit durch Rekursion, sondern es wird Folgendes ausgedrückt:<br />

Ein Wert x ist enthalten, wenn es ein Element gibt, das gleich x ist. <strong>Die</strong><br />

Funktion List.exists führt die Iteration durch.<br />

Pattern matching kann wie viele andere Techniken auch falsch verwendet werden.<br />

Sofern nur die Iteration einer Liste oder eine ähnlich einfache Aufgabe durchgeführt<br />

werden soll, sollte auf Funktionen höherer Ordnung zurückgegriffen werden.<br />

Wenn mit Zahlenbereichen und Relationen wie < gearbeitet wird, sollte<br />

if verwendet werden. In Verbindung mit Option-Werten werden ggf. längere<br />

Pattern-matching-Ketten, wobei eine Computation Expression wie der Option-<br />

Workflow (S. 36) verwendet werden sollte. Der Option-Typ definiert auch Funktionen<br />

höherer Ordnung, die zur Verarbeitung von Option-Werten benutzt werden<br />

können, wobei dies nur selten sinnvoll ist.<br />

Andersherum arbeiten Pattern matching und Funktionen höherer Ordnung sehr<br />

gut zusammen, insbesondere beim Option-Typ. Seq.tryFind predicate sequence<br />

hat den Typ (’a -> bool)-> ’a seq -> ’a option und gibt das erste Element der<br />

Sequenz zurück, die das Prädikat erfüllt (als Some(element)); erfüllt kein Element<br />

das Prädikat, wird None zurückgegeben. Eine sinnvolle Verwendung ist z.B. das<br />

Anwenden der Funktion und sofortige Abgleichen mit den Option-Varianten:<br />

> let gibGeradeZahlAus zahlen =<br />

match Seq.tryFind(fun x -> x % 2 = 0) zahlen with<br />

| Some(geradeZahl) -><br />

printfn ”%d ist die erste gerade Zahl” geradeZahl<br />

| None -> printfn ”Keine Zahl war gerade”;;<br />

val gibGeradeZahlAus : seq -> unit<br />

> gibGeradeZahlAus [1; 2; 3];;<br />

2 ist die erste gerade Zahl<br />

3.2.3 Active Patterns<br />

Technik:<br />

Zweck:<br />

Active Patterns<br />

Funktionale Erweiterung von Pattern Matching<br />

Problem: Nur primitive und algebraische Datentypen können in Pattern Matching<br />

verwendet werden, aber oft wird mit objektorientierten Klassen gearbeitet.<br />

Wiederholende Aufgaben wie Typprüfung und Wertextraktion von Objekten<br />

können in Funktionen gekapselt werden, die häufige Nutzung von Hilfs-<br />

29


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

funktionen verschleiert aber die Intention. Vorhandene Patterns sind nicht<br />

aussagekräftig oder tauchen als Duplikate auf.<br />

Lösung: Active Patterns erweitern die Möglichkeit von Pattern Matching, indem<br />

Funktionen vor oder während des Matchings auf den Ausdruck angewendet<br />

wird. Konstrukte: (|X|), (|X|_|), (|X|Y|)<br />

Performanz:<br />

Hängt von Pattern Matching und benutzten Funktionen ab.<br />

Alternative: Explizite Funktionsaufrufe und explizite Definition von algebraischen<br />

Datentypen für das Ergebnis der Funktionen.<br />

Um Active Patterns zu verstehen, ist es sinnvoll, kleine Beispiele dafür in expliziter<br />

Form zu sehen und diese mit dem entsprechenden Active Pattern zu vergleichen.<br />

Der einfachste Anwendungsfall ist der Klassifizierer.<br />

Klassifizierer<br />

type Vorzeichen = Negativ | Null | Positiv<br />

let vorzeichen x =<br />

if x < 0 then Negativ else if x = 0 then Null else Positiv<br />

let expliziteVerwendungVonFunktionen =<br />

match (vorzeichen -23, vorzeichen 0, vorzeichen 42) with<br />

| (Negativ, Null, Positiv) -> printfn ”Test bestanden.”<br />

| _ -> failwith ”Test nicht bestanden.”<br />

Hier wird zunächst ein algebraischer Datentyp definiert, der das Ergebnis der<br />

Klassifikation bestimmt. Dann wird eine Funktion geschrieben, die eine ganze<br />

Zahl nach ihrem Vorzeichen klassifiziert. In einem kleinen Beispiel wird dies getestet.<br />

Im folgenden wird eine kürzere Variante gezeigt, die Active Patterns benutzt und<br />

dasselbe leistet. Auffällig ist dabei der Name der zuvor vorzeichen genannten<br />

Funktion und das Wegfallen der Typdefinition und der Funktionsaufrufe. Active<br />

Patterns sind lediglich Funktionen, d.h. (|Negativ|Null|Positiv|) ist eine Funktion<br />

mit einem speziellen Namen, die in Pattern Matching automatisch verwendet<br />

wird.<br />

let (|Negativ|Null|Positiv|) x =<br />

if x < 0 then Negativ else if x = 0 then Null else Positiv<br />

let impliziteForm =<br />

match (-23, 0, 42) with<br />

| (Negativ, Null, Positiv) -> printfn ”Test bestanden.”<br />

| _ -> failwith ”Test nicht bestanden.”<br />

An dieser Stelle wird deutlich: Das Muster (Negativ, Null, Positiv) passt auf alle<br />

Tripel-Werte, deren erste Komponente eine negative Ganzzahl ist, deren zweite<br />

0 und deren dritte eine positive Ganzzahl ist. Auch wenn dieses Spielbeispiel<br />

nicht besonders hilfreich erscheint, ließen sich damit schon folgende Refactorings<br />

30


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

durchführen: Muster mit guard der Form ...x... when x > 0 lassen sich durch Benutzung<br />

von (Positiv as x) vereinfachen. Und wo auch immer Funktionen mit<br />

Ganzzahlen arbeiten, die positiv sein müssen, ließe sich ein anfänglicher Test der<br />

Form<br />

let f(x)= if not(x > 0)then invalidArg ”x””x > 0 muss gelten”else Körper<br />

auf der Parameter-Stelle von x in der Form let f(Positiv as x)= Körper einbauen<br />

(die Fehlernachricht ist dann allerdings nicht aussagekräftig).<br />

Der erste Vorschlag führt zur zweiten Form von Active Patterns: Partial Active<br />

Patterns, die in Kürze gezeigt werden.<br />

Der zweite genannte Anwendungsfall, die Validierung eines Argumentes, führt zu<br />

einem Active Pattern, welches eine Exception werfen kann. <strong>Die</strong>se Art von Active<br />

Pattern nenne ich Validierer. In der folgenden Variante handelt es sich um ein<br />

Parameterised Active Pattern, da name ein Parameter ist, der wie beispielsweise<br />

”n” in (NichtNegativ ”n”n) übergeben wird.<br />

Validierer und Parameterised Active Pattern<br />

let (|NichtNegativ|) name x =<br />

if x >= 0 then x<br />

else invalidArg name (name + ” darf nicht negativ sein.”)<br />

let kleinerGauß(NichtNegativ ”n” n) = n * (n+1) / 2<br />

Eine sehr einfache Verwendung der Form (|X|) ist eine Abbildung, die immer<br />

gelingt und einen Wert liefert:<br />

Konvertierter<br />

let (|Betrag|) x = abs x<br />

let (|AlsString|) x = string x<br />

let (|AlsDouble|)(x: obj) = System.Convert.ToDouble(x)<br />

Auffällig ist vielleicht die Verwendung von Funktionen, die lediglich angewendet<br />

werden. Ein Muster der Form let (|Apply|)f x = f x könnte diese beiden<br />

gezeigten ersetzen (als Muster (Betrag betrag) oder (Apply abs derBetrag) und<br />

(AlsString text) oder (Apply string derText)). <strong>Die</strong>ses Active Pattern ist im Allgemeinen<br />

nicht sinnvoll, weil Active Patterns die Lesbarkeit verbessern sollen,<br />

indem sprechende Namen für häufige Umwandlungen und Prüfungen verwendet<br />

werden.<br />

Parser und Partial Active Patterns<br />

Partial Active Patterns zeichnen sich dadurch aus, dass sie partiell definiert sind.<br />

<strong>Die</strong>s wird durch die Verwendung des Option-Typs als Rückgabetyp ausgedrückt<br />

und im Namen durch einen abschließenden Unterstrich gekennzeichnet.<br />

open System // Dort ist der Typ Int32 definiert<br />

let (|AlsGanzzahl|_|) x =<br />

31


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

match Int32.TryParse(x) with<br />

| true, wert -> Some(wert)<br />

| _ -> None<br />

let test =<br />

match ”24” with<br />

| AlsGanzzahl x -> ”die Ganzzahl ” + string x<br />

| _ -> ”keine Ganzzahl”<br />

Als Nächstes zeige ich, dass die zwei Konzepte partielle Definition und Klassifikation<br />

kollidieren, insbesondere mit der beliebigen Kombinierbarkeit von Patterns:<br />

<strong>Die</strong> Variante (|X|Y|_|) darf (und kann glücklicherweise) nicht verwendet werden,<br />

wie folgendes hypothetisches Beispiel zeigt. Im Folgenden wird auch auf die Ausführungsreihenfolge<br />

eingegangen, insbesondere auf den Zeitpunkt, wann die als<br />

Active Patterns definierten Funktionen auf den Wert angewendet werden.<br />

open System // Dort sind die Typen Int32 und Double definiert<br />

let (|Ganzzahl|Kommazahl|_|) x =<br />

// Anm.: <strong>Die</strong>ser Funktionsname wird vom Parser nicht akzeptiert.<br />

match Int32.TryParse(x) with // Versuche Wert als Ganzzahl zu parsen:<br />

| true, wert -> Some(Ganzzahl(wert)) // OK: Ganzzahl<br />

| _ -> match Double.TryParse(x) with // Ansonsten als Kommazahl:<br />

| true, wert -> Some(Kommazahl(wert)) // OK: Kommazahl<br />

| _ -> None // Sonst: weder Ganz- noch Kommazahl<br />

let test =<br />

match ”24” with // führt (|Ganzzahl|Kommazahl|_|) aus,<br />

// liefert Some(Ganzzahl(42))<br />

| Ganzzahl 42 -> ”die Ganzzahl 42” // Scheitert bei match 24 with 42.<br />

| Kommazahl x -> ”irgend eine Kommazahl” // Wird ignoriert, denn<br />

// die Zahl wurde als Ganzzahl, nicht als Kommazahl klassifiziert.<br />

// Intuitiv wäre ein erneutes Parsen, das Kommazahl(24.0) liefert.<br />

| _ -> ”gar keine Zahl” // <strong>Die</strong>ser Fall trifft zu!<br />

Stattdessen werden <strong>hier</strong> die kleinstmöglichen Einheiten, die als Partial Active<br />

Patterns definiert sind, (|Ganzzahl|_|) und (|Kommazahl|_|) verwendet, die sich<br />

erwartungsgemäß verhalten.<br />

<strong>Die</strong>se Varianten stellen durch die Verwendung Klassifizierer dar, in jedem Fall<br />

aber sind es Parser.<br />

open System // Dort sind die Typen Int32 und Double definiert<br />

let (|Ganzzahl|_|) x =<br />

match Int32.TryParse(x) with<br />

| true, wert -> Some(Ganzzahl(wert))<br />

| _ -> None<br />

let (|Kommazahl|_|) x =<br />

match Double.TryParse(x) with<br />

| true, wert -> Some(Kommazahl(wert))<br />

| _ -> None<br />

let test =<br />

match ”24” with<br />

32


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

| Ganzzahl 42 -> ”die Ganzzahl 42” // Führt (|Ganzzahl|_|) aus,<br />

// liefert Some(24), scheitert bei match 24 with 42. Nächster Fall:<br />

| Kommazahl x -> ”irgend eine Kommazahl” // Führt (|Kommazahl|_|),<br />

// liefert Some(24.0), trifft zu und bindet 24.0 an x.<br />

| _ -> ”gar keine Zahl”<br />

Übersicht der Anwendungsfälle<br />

Durch diese Anwendungsfälle lassen sich folgende Nutzungen von Active Patterns<br />

herauskristallisieren:<br />

(|X|) Konvertiere zu X (Konvertierer), Überprüfe auf Eigenschaft X (Validierer)<br />

und wirf eine Exception im Fehlerfall.<br />

(|X|Y|) Klassifiziere nach Eigenschaften X, Y, ... (bis zu 7 Eigenschaften sind<br />

möglich) (Klassifizierer)<br />

(|X|_|) Klassifiziere nach Eigenschaft X oder scheitere (Klassifizierer), parse Wert<br />

als X (Parser)<br />

<strong>Die</strong> Benutzung von Active Patterns kann zu verständlicherem Code führen, indem<br />

Pattern Matching damit auf beliebigen Datentypen benutzt werden kann.<br />

In der Arbeit über Active Patterns werden unter anderem für häufig verwendete<br />

objektorientierten Typen wie Type und XmlDocument Active Patterns definiert,<br />

die eine Verwendung der Objekte dieses Typs wie Varianten eines algebraischen<br />

Datentypen zulässt [ActivePattern, vgl. S. 4, 7-8].<br />

Ein Beispiel, das besonders durch Active Patterns deklarativ ist, sind diese drei<br />

Definitionen, die die Verarbeitung von XML-Dokumenten enorm erleichtert. Hier<br />

werden für die Typen der System.Linq.XObject-Hierarchie Active Patterns definiert.<br />

Das anschließende Beispiel zeigt, wie diese zu verwenden sind.<br />

open System.Xml.Linq<br />

// (|Node|_|): string -> XNode -> XNode seq<br />

let (|Node|_|)(name: string)(node: XNode) =<br />

match node with<br />

| :? XElement as element<br />

when element.Name.LocalName = name -><br />

Some(element.Nodes())<br />

| _ -> None<br />

// (|Text|_|): XNode -> string option<br />

let (|Text|_|)(node: XNode) =<br />

match node with<br />

| :? XElement -> None<br />

| _ -> Some(node.ToString())<br />

// (|Attribute|_|): string -> XNode -> string option<br />

let (|Attribute|_|)(name: string)(node: XNode) =<br />

match node with<br />

| :? XElement as element -><br />

33


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

match element.Attribute(XName.Get(name)) with<br />

| null -> None<br />

| x -> Some(x.Value)<br />

| _ -> None<br />

let rec traverseAll = Seq.iter traverseNode<br />

and traverseNode = function<br />

| Text text -> printfn ” %s” (text.Trim())<br />

| Node ”Matches” children -> traverseAll children<br />

| Node ”Match” children & Attribute ”Winner” winner<br />

& Attribute ”Loser” loser & Attribute”Score” score -><br />

printfn ”%s won against %s with score %s” winner loser score<br />

traverseAll children<br />

traverseNode(XElement.Load(”matches.xml”))<br />

Für das Beispieldokument matches.xml:<br />

<br />

<br />

Description of the first match...<br />

<br />

<br />

Description of the second match...<br />

<br />

<br />

erscheint diese Ausgabe:<br />

A won against B with score 1:0<br />

Description of the first match...<br />

A won against C with score 1:0<br />

Description of the second match...<br />

Durch dieses Beispiel wird vor allem die Kompositionierbarkeit (Konjunktion mit<br />

&) von Active Patterns deutlich. Das folgende Muster passt genau auf einen Knoten<br />

Match mit den Attributen Winner, Loser sowie Score und extra<strong>hier</strong>t gleichzeitig<br />

Kindelemente des Knotens und die Attributwerte:<br />

Node ”Match” children & Attribute ”Winner” winner & Attribute ”Loser”<br />

loser & Attribute”Score” score<br />

<strong>Die</strong>ser Ansatz führt zu einer weiteren Denkweise hinter deklarativer Programmierung:<br />

Domänenspezifische Sprachen (domain-specific languages, DSL). <strong>Die</strong> Regeln,<br />

die zuvor definiert wurden stellen eine interne DSL dar.<br />

3.2.4 Computation Expressions<br />

Technik: Computation Expressions<br />

Zweck: Alternative Auswertung von F#-Konstrukten<br />

34


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Problem: Grundlegende Konstrukte wie Variablenbindungen und Schleifen werden<br />

standardmäßig sequenziell ausgeführt, ohne dass darauf Einfluss genommen<br />

werden kann. Eine parallele Ausführung, Logging, schrittweiseunterbrechbare<br />

Ausführung oder verzögerte Ausführung müssen explizit behandelt<br />

werden.<br />

Lösung: Computation expressions bieten die Möglichkeit, über Continuations die<br />

Ausführung zu beeinflussen. Das primäre Konstrukt dafür ist der Computation<br />

Builder.<br />

Performanz: Hängt von den Methoden des Builders ab; führt zusätzlich Funktionsobjekte<br />

für die Continuations ein. 4<br />

Alternative: Alternative Ausführung muss explizit formuliert werden, was aufwendig<br />

oder lästig sein kann.<br />

Um Computation Expressions zu verstehen, muss man die Funktionsweise des<br />

Computation Builders verstehen. Ein Computation Builder ist ein Objekt einer<br />

Klasse, die mindestens diese beiden Methoden besitzt:<br />

Bind(value, continuation) Für das Konstrukt let! identifier = expression in<br />

body. <strong>Die</strong>s ermöglicht Kontrolle über Wertbindungen und die weitere Ausführung<br />

nach dieser Bindung.<br />

Return(value) Für das Konstrukt return expression. Damit wird die Ausführung<br />

der Computation Expression beendet und liefert einen Wert des gesamten<br />

Ausdrucks der Computation Expression.<br />

Ein häufiges Idiom in der imperativen Programmierung sind null-Prüfungen. Da<br />

in der funktionalen Programmierung null kein gültiger Wert eines Typs ist, wird<br />

der Option-Typ (siehe Option-Typ im Abschnitt Typen (S. 24)) verwendet. <strong>Die</strong><br />

Intention des fehlenden Wertes wird dadurch deutlicher, anstatt für alle Referenz-<br />

Typ null als Wert zu erlauben.<br />

Der Option-Typ enthält zwei Varianten: je eine Variante für einen undefinierten<br />

oder definierten Wert.<br />

type Option = None | Some of ’a<br />

Um mit diesem Typ zu arbeiten, muss mithilfe von Pattern matching unterschieden<br />

werden, ob Zwischenergebnisse definiert sind und ggf. die Berechnung mit<br />

dem undefinierten Wert zu beenden. Als Beispiel dient <strong>hier</strong> eine Funktion, die<br />

zwei Zahlen aus der Konsole liest und die Summe zurückgibt. Nur für den Fall,<br />

dass beide Eingaben Zahlen darstellen, ist das Ergebnis definiert.<br />

open System<br />

/// readIntegerOptionFromConsole: unit -> int option<br />

let readIntegerOptionFromConsole() =<br />

match Int32.TryParse(Console.ReadLine()) with<br />

4 Continuations können in Einzelfällen zu Performanz-Problemen führen (z.B.<br />

http://www.quanttec.<strong>com</strong>/fparsec/users-guide/where-is-the-monad.html#why-the-monadicsyntax-is-slow).<br />

35


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

| true, value -> Some(value)<br />

| _ ->None<br />

match readIntegerOptionFromConsole() with<br />

| None -> None<br />

| Some(firstValue) -><br />

match readIntegerOptionFromConsole() with<br />

| None -> None<br />

| Some(secondValue) -> Some(firstValue + secondValue)<br />

Da bei der Programmierung mit externen Datenquellen (wie etwa der Konsole,<br />

Dateien oder Webinhalten) diese Daten nicht immer ein gültiges Format haben,<br />

ist der Code, der mit diesen Daten arbeitet, mit Gültigkeits-Prüfungen versehen.<br />

Im Idealfall wird in objektorientierten Programmiersprachen mit Exceptions gearbeitet,<br />

sodass der korrekte Arbeitsablauf innerhalb eines Try-Konstrukts geschrieben<br />

werden kann und im Falle eines Fehlers die Fehlerbehandlung durchgeführt<br />

wird. In der Tat ließe sich diese Methode auch für das angeführte Beispiel verwenden:<br />

open System<br />

/// readIntegerFromConsole: unit -> int (throws FormatException)<br />

let readIntegerFromConsole() = Int32.Parse(Console.ReadLine())<br />

try<br />

let first = readIntegerFromConsole()<br />

let second = readIntegerFromConsole()<br />

Some(first + second)<br />

with :? FormatException as ex -> None<br />

In der funktionalen Programmierung ist jedoch die Verwendung des Option-Typs<br />

verbreiteter, weil schon über den Typ wie etwa int option ersichtlich ist, dass dieser<br />

Vorgang fehlschlagen kann. Bei Exceptions ist dies nur durch Dokumentation<br />

ersichtlich oder im Fall von Java mit checked exceptions, die zur Methodensignatur<br />

zählen und im verwendenden Code abgefangen werden müssen.<br />

In beiden Fällen ist die Fehlerbehandlungsstrategie ersichtlich. Mithilfe von Computation<br />

Expressions ist eine alternative Auswertung möglich, sodass das Abfangen<br />

von Exceptions oder Prüfen auf definierte Werte ein Belang ist, der nicht<br />

explizit im Code wie oben formuliert wird, sondern Aufgabe des Computation<br />

Builders ist.<br />

Im Fall des Option-Typs ließe sich der Option-Workflow definieren:<br />

type OptionBuilder() =<br />

member this.Bind(value, continuation) =<br />

match value with<br />

| None -> None<br />

| Some(definedValue) -> continuation(definedValue)<br />

member this.Return(value) = Some(value)<br />

let optional = OptionBuilder()<br />

<strong>Die</strong>ser lässt sich dann wie folgt verwenden:<br />

36


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

optional {<br />

let! first = readIntegerOptionFromConsole()<br />

let! second = readIntegerOptionFromConsole()<br />

return first + second<br />

}<br />

Der obenstehende Code benutzt die Methoden des OptionBuilder-Typs. Jedes Vorkommen<br />

von let! Pattern = Expression in Body (in kann auch durch einen Zeilenumbruch<br />

ersetzt werden) wird durch optional.Bind(Expression, fun Pattern<br />

-> Body) ersetzt. Ebenso wird return Value in optional.Return(Value) übersetzt.<br />

Hier wird lediglich syntaktischer Zucker verwendet, der unter anderem Ähnlichkeit<br />

mit let-Bindungen hat und beim Kompilieren durch Methodenaufrufe des<br />

Computation Builders ersetzt wird. Wichtige Beispiele dieser Technik sind seq<br />

für Sequenz-Literale, async für nebenläufige Programmausführung und query für<br />

Datenbank-Abfragen. Wie eingangs erwähnt, ist vereinfachte nebenläufige Programmausführung<br />

ein Vorteil von deklarativen Programmiersprachen. <strong>Die</strong>s wird<br />

insbesondere deutlich, wenn die Integration von nebenläufigen Methodenaufrufen<br />

durch einen Computation Builder komfortabler gemacht wird. Andere Sprachen<br />

greifen für solche Zwecke auf neue Schlüsselwörter, Syntax-Erweiterungen<br />

und Makros zurück. Auch in der Hinsicht ist eine selbstprogrammierbare alternative<br />

Ausführungsumgebung praktisch.<br />

Insbesondere bei Datenbankabfragen ist es vorteilhaft, wenn der Programmierer<br />

keine SQL-Strings manipuliert, um eine Anfrage zu erstellen, sondern stattdessen<br />

in einer dafür erstellten Auswertungsumgebung Queries schreibt, die entsprechende<br />

Datenbank-Typen verwenden.<br />

let articles = query {<br />

for article in db.Articles do<br />

sortBy article.Price<br />

select (article.Name, article. Price)<br />

}<br />

for (name, price) in articles do printfn ”%s costs %f €” name price<br />

sortBy und select sind sogenannte Custom operations, die wie kontextabhängige<br />

Schlüsselwörter einer Programmiersprache wirken, jedoch nur spezielle Methoden<br />

des Computation Builders sind.<br />

Im Fall des Query-Builders wird auch auf Quotations zurückgegriffen, die im<br />

nächsten Kapitel behandelt werden und den Einstieg in Metaprogrammierung<br />

darstellen.<br />

Dazu wird im Computation Builder die Methode Quote eingeführt [FSharpSpec,<br />

S. 62].<br />

37


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

3.3 Domänenspezifische Sprachen<br />

3.3.1 Was sind DSLs?<br />

Domänenspezifische Sprachen (domain specific languages, kurz DSLs) sind Sprachen,<br />

in denen nur Probleme eines Fachgebietes (einer Domäne) formuliert werden<br />

können. Aufgaben außerhalb des Anwendungsgebietes sollen nicht in DSLs<br />

gelöst werden. <strong>Die</strong>ser Punkt unterscheidet sie stark von generellen Programmiersprachen<br />

(general purpose languages, kurz GPLs), die alle programmierbaren Probleme<br />

darstellen können.<br />

Man unterscheidet interne und externe DSLs. Interne sind solche, die innerhalb<br />

einer anderen Programmiersprache auftreten, also mit mitteln der Programmiersprache<br />

formuliert sind. Externe DSLs sind jene, die nicht die Mittel einer „Host“-<br />

Programmiersprache verwenden sondern eine eigene Syntax aufweist. Mit externen<br />

DSLs erstellte Texte (bzw. Skripte oder Beschreibungen) werden oft in<br />

externen (Text-)Dateien geschrieben, wobei es Ausnahmen wie SQL und Regex<br />

gibt, die auch in anderen Programmiersprachen als String-Inhalte verwendet werden.<br />

Als Beispiel für eine interne DSL dient <strong>hier</strong> „Miss Grant’s Controller“ [DSLs], ein<br />

Sicherheitssystem, das ein Fach öffnet, nachdem eine bestimmte Folge von Aktionen<br />

durchgeführt wurde. In F# werden die Aktionen, Zustandsnamen, Codes und<br />

Ereignisse als algebraische Datentypen (S. 23) definiert (im Original werden diese<br />

als Strings modelliert, was für externe DSLs sehr flexibel ist, für interne DSLs<br />

hingegen Typsicherheit wichtiger ist). Ein Zustand und das System selbst werden<br />

als Datensatz (S. 24) definiert. <strong>Die</strong> Konstruktion des Systems geschieht über<br />

Datensatz-Konstruktoren, Listenliterale und mithilfe von Operatoren. 5<br />

type condition = DoorClosed | DrawerOpened | LightOn<br />

| DoorOpened | PanelClosed<br />

and actions = UnlockPanel | LockPanel | LockDoor | UnlockDoor<br />

and codes = D1CL | D2OP | L1ON | D1OP | PNCL<br />

| PNUL | PNLK | D1LK | D1UL<br />

and stateName = Idle | Active | WaitingForLight<br />

| WaitingForDrawer | UnlockedPanel<br />

and state = {<br />

name: stateName;<br />

actions: actions list;<br />

transitions: (condition * stateName) list<br />

}<br />

and machine = {<br />

events : (condition * codes) list<br />

resetEvents: condition list<br />

<strong>com</strong>mands : (actions * codes) list<br />

states : state list<br />

}<br />

5 Eine Beispielprogramm, das den Nutzer durch den resultierenden Automaten dieses Systems<br />

navigieren lässt, findet sich im Anhang im Abschnitt Miss Grant’s Controller (S. 70).<br />

38


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

let inline (=>) a b = (a, b)<br />

let inline (:=) name (actions, transitions) =<br />

{ name = name; actions = actions; transitions = transitions }<br />

let machine = {<br />

events = [<br />

}<br />

DoorClosed => D1CL<br />

DrawerOpened => D2OP<br />

LightOn => L1ON<br />

DoorOpened => D1OP<br />

PanelClosed => PNCL<br />

];<br />

resetEvents = [ DoorOpened ];<br />

<strong>com</strong>mands = [<br />

UnlockPanel => PNUL<br />

LockPanel => PNLK<br />

LockDoor => D1LK<br />

UnlockDoor => D1UL<br />

];<br />

states = [<br />

Idle := [UnlockDoor; LockPanel]<br />

=> [DoorClosed => Active]<br />

Active := []<br />

=> [DrawerOpened => WaitingForLight;<br />

LightOn => WaitingForDrawer]<br />

WaitingForLight := []<br />

=> [LightOn => UnlockedPanel]<br />

WaitingForDrawer := []<br />

=> [DrawerOpened => UnlockedPanel]<br />

UnlockedPanel := [UnlockPanel; LockDoor]<br />

=> [PanelClosed => Idle]<br />

]<br />

<strong>Die</strong> Verwendung von DSLs lässt sich auch als Teil von language-oriented programming<br />

ansehen, einem Paradigma, das darauf basiert, eine geeignete Notation zur Problembeschreibung<br />

zu entwickeln und die Software dann mit dieser Notation zu<br />

entwickeln. <strong>Die</strong>s wird in der Arbeit, die diesen Begriff maßgeblich geprägt hat,<br />

auch durch kürzeren Quellcode motiviert:<br />

„In <strong>com</strong>puter science it is a great advantage to have a suitable notation in which<br />

to express certain classes of algorithms, rather then writing yards of source code.“<br />

[LanguageOrientedProg, S. 9]<br />

3.3.2 Reguläre Ausdrücke<br />

Eine sehr beliebte externe DSL sind reguläre Ausdrücke (regular expressions, kurz<br />

regex), mit denen sich Text nach bestimmten Mustern durchsuchen lässt. Beispiele<br />

für Muster sind konkrete Buchstaben, Ziffern, Leerzeichen; hinter diese Mustern<br />

kann eine Vorkommens-Einschränkung notiert werden (z.B. einmal oder keinmal,<br />

mindestens einmal oder beliebig oft). <strong>Die</strong> Integration von regulären Ausdrücken<br />

39


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

in Programmiersprachen ist vielfältig. Hier soll mithilfe von Active Patterns eine<br />

möglichst intuitive Verwendung erreicht werden.<br />

Regex über Active Patterns:<br />

> open System.Text.RegularExpressions<br />

let (|RegexGroups|_|) pattern text =<br />

let regex = Regex.Match(text, pattern)<br />

if regex.Success then<br />

Some(List.tail [ for group in regex.Groups -> group.Value ])<br />

else None;;<br />

val (|RegexGroups|_|) : pattern:string -> text:string -> string list<br />

option<br />

> match ”12+13” with<br />

| RegexGroups ”(\d+)\+(\d+)” [a; b] -><br />

sprintf ”Zwei Zahlen: %s und %s” a b<br />

| _ -> ”keine zwei Zahlen”;;<br />

val it : string = ”Zwei Zahlen: 12 und 13”<br />

Weitere Techniken zur Programmierung von DSLs mit F# sind möglich, die <strong>hier</strong><br />

nicht aufgezählt werden können. Einige werden in [RealWorldFP, vgl. S. 425,<br />

433, 451] angesprochen und umgesetzt (neue Operatoren, Typ-Augmentation,<br />

Operator-Lifting). Genaueres zu Operatoren findet sich in [FSharp, S. 119]. 6<br />

DSLs und funktionale Programmiersprachen sind seit LISP engverwandt. Martin<br />

Fowler merkt in seinem Buch [DSLs, S. 163] an, nicht genug über DSLs und funktionale<br />

Programmierung zu wissen, um es in dem Buch darzustellen, weshalb sich<br />

das Buch auf DSL-Techniken für objektorientierte Programmiersprachen konzentriert.<br />

Eine häufig anzutreffende Technik, die für objektorientierte interne DSLs verwendet<br />

wird, sind sogenannte Fluent Interfaces, d.h. Objekte, die Methodenketten<br />

erlauben, wodurch nacheinander Eigenschaften spezifiziert werden oder Aktionen<br />

ausgeführt werden. Um z.B. für eine Klasse mit zwei Feldern int x, y eine<br />

ToString-Methode zu schreiben, ließe sich der StringBuilder verwenden. Der Trick<br />

liegt darin, dass die Append-Methode dasselbe Exemplar zurückgibt, sodass die<br />

nächste Methode aufgerufen werden kann. <strong>Die</strong> letzte Zeile der MEthode lautet<br />

also return this;.<br />

class Point {<br />

public int X, Y;<br />

public override string ToString() {<br />

return new StringBuilder()<br />

.Append(”[”).Append(X)<br />

.Append(” | ”).Append(Y)<br />

.Append(”]”).ToString();<br />

}<br />

6 Eine praxisnahe Übersicht über DSL-Techniken bietet https://github.<strong>com</strong>/dungpa/dsls-inaction-fsharp/blob/master/DSLCheatsheet.md.<br />

40


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

}<br />

Im folgenden Kapitel geht es das Konzept Metaprogrammierung, das sich mit Programmerzeugung,<br />

-Analyse und -Transformation befasst, was für DSLs essenziell<br />

ist. Außerdem wird mit Quotations eine mächtige Technik für interne DSLs eingeführt.<br />

3.4 Metaprogrammierung<br />

3.4.1 Was ist Metaprogrammierung?<br />

Für Metaprogrammierung gibt es verschiedene Definitionen und Erklärungen; die<br />

Folgenden stammen aus dem Buch [MetaprogDotNet].<br />

1. „A <strong>com</strong>puter program that writes new <strong>com</strong>puter programs.“ [MetaprogDotNet,<br />

S. 6]<br />

• Code-Generierung ist in der Tat ein wichtiges Gebiet der Metaprogrammierung.<br />

Immerhin müssen Compiler den Quellcode der Programmiersprache<br />

in Maschinen-nahen Code umsetzen. Dennoch schreiben nur<br />

wenige Programmierer jemals selbst einen Compiler. Nur selten wird<br />

Code generiert, z.B. im Fall von Datenbank-Programmierung und der<br />

Generierung von Klassen aus dem Datenbank-Schema. Eine anderer anwendungsbezogener<br />

Aspekt von Metaprogrammierung ist Analyse oder<br />

Inspektion von Programmen (bzw. von Objekten) zur Laufzeit (dies ist<br />

das Gebiet Reflection von Programmiersprachen).<br />

2. „Try to think of it [metaprogramming] as after-programming or besideprogramming.<br />

The Greek prefix meta allows for both of those definitions<br />

to be correct. Most of the examples in this book demonstrate programming<br />

after traditional <strong>com</strong>pilation has occurred, or by using dynamic code that<br />

runs alongside other processes.“ [MetaprogDotNet, S. 6]<br />

• <strong>Die</strong> Umschreibung mit „Danach-Programmierung“ ist bei Codetransformation<br />

sehr passend, weil vom Benutzer geschriebener Code vor<br />

dem eigentlichen Kompilieren um Aspekte wie Persistenz von Objekten<br />

7 oder Datenprüfungen 8 erweitert wird. „Nebenbei-Programmierung“<br />

kann das Arbeiten mit generiertem Code sein, d.h. beim Programmieren<br />

kann auf generierte Klassen und Methoden zugegriffen werden. Das<br />

ist insbesondere wichtig bei GUI-Programmierung, wenn GUI-Designer<br />

Code erzeugen, den der Programmierer sofort verwenden muss, z.B.<br />

Referenzen auf Steuerelemente; oder auch bei aus Datenbank-Schemata<br />

generierten Klassenbibliotheken.<br />

7 Wie etwa für das Serializable-Attribut bei der Programmiersprache Nemerle:<br />

http://nemerle.org/wiki/index.php?title=Macros_tutorial#Macros_in_custom_attributes.<br />

8 Wie in Design-by-Contract-Frameworks für NotNull-Attribute.<br />

41


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Im Folgenden wird Metaprogrammierung anhand von Syntaxbäumen erklärt, weil<br />

diese auf anschauliche Weise zeigen, dass Programmcode Daten sind und die übliche<br />

Datenverarbeitung dadurch zur Verarbeitung von Programmen wird.<br />

3.4.2 Quotations<br />

Es kann vorteilhaft sein, auf Teile des Quellcodes als Objekt zugreifen zu können,<br />

z.B. um diesen zu analysieren, zu transformieren, zu persistieren oder zu übertragen.<br />

In F# sind Quotations Ausdrücke der Form . Innerhalb dieser<br />

Klammern lässt sich Quellcode schreiben, der nicht ausgewertet oder kompiliert<br />

wird, sondern dessen Syntaxbaum erzeugt wird. <strong>Die</strong> Einschränkung ist, dass keine<br />

Typen und Module deklariert werden können. Wenn Funktionen geschrieben<br />

werden, lässt sich über des ReflectedDefinition-Attributes ausdrücken, dass auch<br />

auf die „quotierte“ Form der Funktion zugegriffen werden kann, wie folgendes<br />

Beispiel zeigt:<br />

> []<br />

let f(x: int) = 2 * x;;<br />

val f : int -> int<br />

> open Microsoft.FSharp.Quotations<br />

open Microsoft.FSharp.Quotations.Patterns<br />

open Microsoft.FSharp.Quotations.DerivedPatterns;;<br />

> match with<br />

| Lambda(param,<br />

Call(target, MethodWithReflectedDefinition def, args)) -><br />

def;;<br />

warning FS0025: In<strong>com</strong>plete pattern matches on this expression.<br />

val it : Expr = Lambda (x,<br />

Call (None, Int32 op_Multiply[Int32,Int32,Int32](Int32, Int32),<br />

[Value (2), x]))<br />

{CustomAttributes = ...;<br />

Type = Microsoft.FSharp.Core.FSharpFunc‘2[System.Int32,System.Int32];}<br />

Mithilfe von Quotations (S. 42) lassen sich F#-Ausdrücke möglich, die keinem<br />

typischen F#-Programm ähneln. Z.B. lässt sich das Member-Prolog-Prädikat (S.<br />

12) bei geeigneter Definition von memb, E, __ und R in F# schreiben 9 . <strong>Die</strong>ses Programm<br />

ließe sich in den entsprechenden Prolog-Code transformieren. <strong>Die</strong>s ist ein<br />

Schritt in polyglotte (mehrsprachige) Programmierung, bei der man verschiedene<br />

kompatible Programmiersprachen zur Lösung eines Problems verwendet.<br />

prolog


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

schnell zur Generierung von fehlerhaftem Code führen kann. Andere Aufgaben<br />

wie Code-Optimierung und -Transformation können nicht sinnvoll auf der Basis<br />

von Strings durchgeführt werden, weil Fallunterscheidungen anhand von Strings<br />

fehlerträchtig und aufwendig sind. Syntaxbäume werden z.B. bei der Überführung<br />

von Ausdrücken in der Programmiersprache in SQL-Befehle verwendet.<br />

Metaprogrammierung und DSLs stehen in starkem Zusammenhang, in dem oben<br />

genannten Buch werden DSLs durch Metaprogrammierungstechniken ermöglicht<br />

[MetaprogDotNet, vgl. S. 5]<br />

Quotations und Computation-Expressions werden oft für Metaprogrammierungszwecke<br />

eingesetzt. Ein beliebtes Beispiel sind Datenbank-Queries, die in SQL umgesetzt<br />

werden können.<br />

query{ for customer in db.Customers do<br />

where customer.ID = 42<br />

select customer.Name<br />

}<br />

<strong>Die</strong>ser Ausdruck kann innerhalb der Entwicklungsumgebung geschrieben werden,<br />

wobei nur gültige Abfragen formuliert werden können. Der Ausdruck kann in<br />

den SQL-Befehl select Name from Customers where ID = 42 übersetzt werden.<br />

Mit Metaprogrammierung ist es möglich, von Computer-nahen Möglichkeiten zu<br />

abstra<strong>hier</strong>en. Das gilt insbesondere für konkrete Techniken der Fallunterscheidungen,<br />

die in den Abschnitten Pattern matching (S. 26) und Imperativ-objektorientierte<br />

Fallunterscheidungen (S. 51) betrachtet wurden. Damit wird die Aufgabe der Umsetzung<br />

auf einen späteren Zeitpunkt verschoben.<br />

Somit kann die Ausführungsumgebung eine andere sein als die standardmäßig<br />

verwendete; dies wurde bereits mit einem F#-zu-JavaScript-Übersetzer 10 und<br />

Ausführung einer Untermenge von F#-Konstrukten auf Grafikkarten durchgeführt<br />

11 . Details zu der GPU- und SQL-Übersetzung finden sich in [FSharpMeta,<br />

S. 48, 50].<br />

10 http://fsharp.org/use/html5/<br />

11 http://fsharp.org/use/gpu/<br />

43


4<br />

Imperativer Stil<br />

In diesem Teil geht es um die imperativen Pendants zu einigen vorgestellten deklarativen<br />

Elementen. <strong>Die</strong>se sind nicht nur deshalb wichtig, um mit Programmierern,<br />

die vorwiegend imperativ geprägte Programmiersprachen verwenden,<br />

über Quellcode zu reden und zu diskutieren. <strong>Die</strong> Details der Umsetzung deklarativer<br />

Elemente sind für Programmierer interessant, die eine Programmiersprache<br />

nicht nur verwenden sondern auch erweitern wollen oder sich für die technische<br />

Umsetzung interessieren.<br />

<strong>Die</strong>ses Kapitel ist folgendermaßen strukturiert:<br />

• In dem Abschnitt „Imperative Konsistenzprüfungen“ (S. 46) geht es um Veränderlichkeit<br />

von Daten und den damit einhergehenden Problemen und<br />

Maßnahmen.<br />

• Der Abschnitt „Imperativ-objektorientierte Typen“ (S. 47) zeigt kurz, was<br />

bei der Definition von Datensatz-Typen in imperativ-objektorientierten Programmiersprachen<br />

zu beachten ist.<br />

• Im Abschnitt „Imperativ-objektorientierte Fallunterscheidungen“ (S. 51) geht<br />

es um Möglichkeiten der Kontrollflussmanipulation. Neben der Durchführung<br />

von Berechnungen und Lese-/Schreiboperationen ist die Veränderung<br />

des Kontrollflusses eine der Hauptaufgaben des Prozessors. Dank der Idee<br />

von Fallunterscheidungen und Schleifen werden Sprungbefehle nicht vom<br />

Programmierer geschrieben, sondern vom Compiler. Nur Fallunterscheidungen,<br />

keine Schleifen werden <strong>hier</strong> betrachtet. Eine Evaluation der Techniken<br />

findet im darauffolgenden Kapitel Evaluation der Fallunterscheidungstechniken<br />

(S. 54) statt.<br />

4.1 Motivation<br />

Gängige Algorithmen sind oft in Pseudocode notiert (wie etwa in [Cormen]) und<br />

werden vorwiegend mit imperativ geprägten Sprachen implementiert. Vor allem<br />

44


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Arrays werden für effiziente Algorithmen verwendet. Für andere Anwendungsgebiete<br />

wie Matrizen-Rechnungen oder die Initialisierung von komplexen Datenstrukturen<br />

eignet sich die imperative Herangehensweise, was vor allem durch<br />

Performance-Verbesserungen motiviert wird [FSharp, vgl. S. 49].<br />

In F# lassen sich auch imperative Konstrukte wie while oder for benutzen, jedoch<br />

ist das Verlassen einer Schleife mit einem Sprungbefehl (wie return oder break)<br />

nicht möglich. <strong>Die</strong> Verwendung von if then else ist grundsätzlich nicht imperativ;<br />

sie wird <strong>hier</strong> jedoch als solche gezählt, da sie in Form des If statements aus<br />

der imperativen Programmierung besonders häufig auftritt.<br />

Es folgt als Beispiel eine vereinfachte Version des Algorithmus counting sort [Cormen,<br />

vgl. S. 194-195], der Ganzzahlen in einer Laufzeit von O(n) sortiert. Der Code ist<br />

in F# geschrieben und ist ein gutes Beispiel für ein effizienten imperativen Algorithmus.<br />

Zuweisungen in F# werden mit dem Pseudocode-Pfeil let SortInPlace numbers =<br />

let maximum = Array.max numbers<br />

let occurences = Array.zeroCreate(maximum + 1)<br />

for num in numbers do<br />

occurences.[num]


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

4.2 Imperative Konsistenzprüfungen<br />

Fehlende Konsistenzprüfungen sind eine gefährliche Fehlerquelle; täglich werden<br />

Patches bereitgestellt, bei denen Sicherheitslücken oder Programmabstürze durch<br />

Konsistenzprüfungen vermieden werden (häufige Fehler sind fehlende Längen-<br />

Prüfung oder Null-Checks).<br />

In der Tat obliegt es in der imperativ-objektorientierten Programmierung dem<br />

Programmierer, seine Methoden mit Konsistenzprüfungen zu versehen, die sicherstellen,<br />

dass Parameter von Referenz-Typen nicht null sind und dass Arrays<br />

und Strings die richtige Länge haben, um darauf zu arbeiten. Ein Denkmodell,<br />

das diese Prüfungen konsequent fordert und explizit Vor- und Nachbedingungen<br />

für Methoden verlangt, ist das Vertragsmodell (Design by contract).<br />

Neben Parameterprüfungen, insbesondere auf Null-Werte, benötigt man Konsistenzprüfungen,<br />

die sicherstellen, dass sich eine Datenstruktur in einem gültigen<br />

Zustand befindet. <strong>Die</strong>s ist insbesondere wegen einer Eigenschaft von imperativorientierten<br />

Datenstrukturen notwendig: Veränderlichkeit.<br />

Obwohl Felder und Einträge einer Datenstruktur geändert werden können, gibt<br />

es meistens Invarianten, die angeben, in welchem Rahmen Änderungen zu einem<br />

korrekten Nachfolgezustand führen. Beispielsweise muss bei der Arbeit mit<br />

Suchbäumen, die auf veränderbaren Pointern basieren, darauf geachtet werden,<br />

dass konsequent alle Pointer geändert werden, um die Suchbaumeigenschaften<br />

des Baumes nicht zunichte zu machen.<br />

„Programmierdisziplin“ ist das Stichwort, das sich z.B. auch auf saubere Arbeit<br />

mit Pointern bezieht. Ein Kommentar dazu aus einem funktionalen Lehrbuch:<br />

„Bei imperativen Sprachen muss man nahezu beliebige Konglomerate von untereinander<br />

verzeigerten Zellen managen, was auch bei größter Selbstdisziplin zu<br />

komplexen Fehlern führt.“ [FunktProg, S. 235].<br />

Imperative Programme sind vor allem durch Veränderlichkeit schnell, genauer<br />

gesagt durch selektive Änderung (selective update) [FunktProg, S. 236]. Eine mögliche<br />

Sichtweise auf den Zusammenhang zwischen funktionaler Sicherheit und<br />

imperativer Effizienz ist in Abbildung 4.1 dargestellt.<br />

Konsistenzprüfungen sind eine Technik, um qualitative Software zu schreiben.<br />

Es gehört <strong>zum</strong> defensiven Programmierstil, einem weiteren qualitätsbewussten<br />

Denkmodell, die Integrität von Parametern zu prüfen, bevor auf ihnen gearbeitet<br />

wird.<br />

Zwei bekannte Beispiele, welche die Gefahr von fehlenden Prüfungen deutlich<br />

zeigen, sind der Ariane-5- und der Mars-Climate-Orbiter-Unfall:<br />

• Im Falle des „Ariane 5 Flight 501“ wurde eine Überlaufprüfung für die horizontale<br />

Beschleunigung vergessen (die interessanterweise für die vertikale<br />

Beschleunigung durchgeführt wurde).<br />

• Im Falle des Mars-Climate-Orbiter-Unfalls haben zwei Entwicklerteams mit<br />

unterschiedlichen Maßeinheiten gearbeitet, der SI-Einheit Newton im einen<br />

und Pound-Force im anderen Team.<br />

46


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Abbildung 4.1: Funktionale Qualität und imperative Effizienz als gegensätzliche<br />

Enden eines Spektrums [FunktProg, S. 236].<br />

Gerade für solche Anwendungen ist Code-Qualität von enormer Wichtigkeit. In<br />

modernen Programmiersprachen gibt es beispielsweise Festkommazahlen mit hoher<br />

Genauigkeit und Langzahlarithmetik (big num) mit beliebiger Genauigkeit.<br />

Andererseits gibt es Maßeinheiten (units of measurement), damit statt auf einfachen<br />

Zahlen zur Entwicklungszeit mit SI-Einheiten gearbeitet werden kann.<br />

4.3 Imperativ-objektorientierte Typen<br />

<strong>Die</strong> wichtigsten Arten von Typen habe ich im gleichnamigen Abschnitt (S. 21)<br />

eingeführt. An dieser Stelle werde ich nur die Definition von Datensatz-Typen (S.<br />

24) behandeln, d.h. Typen, die nur als Datenbehälter fungieren. Dazu verwende<br />

ich die Sprache C#, die imperativ-objektorientiert geprägt ist.<br />

Schon bei diesen einfachen Typen stellen sich Fragen, wie ein solcher Type zu<br />

definieren ist, was insbesondere mit der Verwendung des Typen zusammenhängt.<br />

Für einen veränderlichen Punkt im zweidimensionalen Raum darstellt, könnte die<br />

einfachste Typ-Definition lauten: struct Punkt { public double X, Y; }.<br />

Viele Designentscheidungen werden allein durch diese kurze Definition getroffen:<br />

• Das Schlüsselwort struct gibt an, dass der Typ nicht auf dem Heap sondern<br />

auf dem Stack alloziert wird. Das bedeutet, dass der Garbage Collector<br />

Exemplare dieses Typs nicht abräumen muss, sondern diese beim Verlassen<br />

des Geltungsbereichs der Variable gelöscht werden. Zuweisungen von<br />

Variablen (lokale Variablen, Felder, Parameter) führen zur Kopie des Exemplars.<br />

Es können keine Subtypen von Structs erstellt werden.<br />

• Der Typ enthält zwei Felder vom Typ double, d.h. Gleitkommazahlen mit<br />

doppelter Genauigkeit (64 Bit).<br />

Mindestens genauso wichtig sind die Designentscheidungen, die durch das Auslassen<br />

von Methoden und Interfaces und Attributen getroffen wurden:<br />

47


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

• Der Typ besitzt keinen Konstruktor, der die Felder initialisiert.<br />

• Der Typ ist nicht als serialisierbar gekennzeichnet.<br />

• Der Typ verwendet unter Umständen eine ineffiziente Equals-Methode.<br />

• Der Typ besitzt keine sinnvolle ToString-Implementierung.<br />

<strong>Die</strong> Folgen sind noch drastischer, wenn statt struct das Schlüsselwort class verwendet<br />

wird (class Punkt { public double X, Y; }):<br />

• Der Typ wird auf dem Heap alloziert, somit ist jede Variable vom Typ Punkt<br />

eine Referenz auf ein Objekt, das sich an einer Speicheradresse befindet. Das<br />

führt zu Aliasing-Effekten, d.h. Änderungen an einem Objekt hinter einer<br />

Referenz werden auch bei anderen Referenzen auf dasselbe Objekt sichtbar.<br />

Außerdem wird der Garbage Collector beansprucht, wenn Exemplare des<br />

Typs nicht mehr verwendet werden.<br />

• Eine Methode <strong>zum</strong> Kopieren eines Objektes ist gegebenenfalls auch sinnvoll<br />

und müsste implementiert werden. Bei möglicher Unterklassenbildung wird<br />

dies erschwert.<br />

• Der Typ besitzt keine sinnvolle Standardimplementierung der Equals-Methode,<br />

denn es wird auf Gleichheit von Speicheradressen geprüft, was dazu führt,<br />

dass zwei Punkte a und b mit denselben Koordinaten (d.h. a.X == b.X und<br />

a.Y == b.Y) nicht a.Equals(b) erfüllen). <strong>Die</strong> Equals-Methode muss von Hand<br />

programmiert werden. Bei möglicher Unterklassenbildung wird die korrekte<br />

Implementierung erschwert.<br />

• <strong>Die</strong> Verwendung des ==-Operators (sowie !=) verwenden nicht die Equals-<br />

Methode sondern prüfen Speicheradressen. Der Gleichheits-Operator (sowie<br />

Ungleichheits-Operator) kann für den Datensatz-Typ definiert werden.<br />

• Der Typ kann als Oberklasse dienen, d.h. es können von Punkt abgeleitete<br />

Klassen erstellt werden. <strong>Die</strong>ses Verhalten kann mit dem Schlüsselwort<br />

sealed unterbunden werden.<br />

Wenn also sinnvoll mit Datensatz-Typen gearbeitet werden soll, müssen diese<br />

Probleme gelöst werden. Dazu müssen der Konstruktor und folgende Methoden<br />

implementiert werden: Equals, GetHashCode, ToString und eventuell Clone. Außerdem<br />

kann das Serializable-Attribut an die Typdefinition geschrieben werden,<br />

um anzugeben, dass der Typ serialisierbar ist. Sollen noch Konsistenzprüfungen<br />

für die Felder eingebaut werden, müssen die Felder durch Properties gekapselt<br />

werden. Für einige Typen sind eventuell Vergleichsrelationen wie < gewünscht,<br />

weshalb die im IComparable-Interface definierte Methode CompareTo implementiert<br />

werden kann (und bei Bedarf die Operatoren = implementiert<br />

werden können).<br />

Es ergeben sich also allein für Datensatz-Typen eine Liste von Anforderungen,<br />

die erfüllt werden müssen, damit der Typ korrekt verwendet werden kann. Der<br />

angedeutete Code, der die oben angeführten Punkte erfüllt, muss für jeden Typ<br />

sinnvoll neu implementiert werden, was zu Boilerplate-Code führt. Im Fall von<br />

48


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

komplexeren Typen, die nicht nur Datensätze darstellen und in einer Klassen<strong>hier</strong>archie<br />

stehen, ergeben sich viele weitere Anforderungen.<br />

<strong>Die</strong>se Belange (Vergleich, Persistenz, Verwendung von Operatoren, Verwendung<br />

in Klassen<strong>hier</strong>archien) sind für objektorientierte Programmierer Voraussetzung,<br />

um verwendbare Typen zu schreiben.<br />

4.3.1 Modellieren einer festen Anzahl an Varianten<br />

Soll eine feste Anzahl an Varianten in einer objektorientierten Programmiersprache<br />

definiert werden, kann dies mithilfe von Sichtbarkeitsmodifizierern für Konstruktoren<br />

erreicht werden. Unterklassenbildung ist mit Konstruktorverkettung<br />

verbunden, was in diesem Fall ausgenutzt wird.<br />

• Markieren des Konstruktors als private: Nur innere Klassen können aufgrund<br />

der Sichtbarkeitsregel für private Unterklassen werden. <strong>Die</strong>s entspricht<br />

dem typesafe enum pattern [EffectiveJava]:<br />

public abstract class CollectionFactory<br />

{<br />

private CollectionFactory() { }<br />

public abstract ICollection Create();<br />

class ArrayListFactory : CollectionFactory<br />

{ public override ICollection Create()<br />

{ return new List(); } }<br />

public static readonly CollectionFactory<br />

ArrayList = new ArrayListFactory();<br />

}<br />

class LinkedListFactory : CollectionFactory<br />

{ public override ICollection Create()<br />

{ return new LinkedList(); } }<br />

public static readonly CollectionFactory<br />

LinkedList = new LinkedListFactory();<br />

• Markieren des Konstruktors als internal: So können nur Klassen desselben<br />

Kompilats (die also im selben Projekt kompiliert werden) auf ihn zugreifen:<br />

public abstract class CollectionFactory<br />

{<br />

internal CollectionFactory() { }<br />

public abstract ICollection Create();<br />

}<br />

public class ArrayListFactory : CollectionFactory<br />

{ public override ICollection Create()<br />

{ return new List(); } }<br />

public class LinkedListFactory : CollectionFactory<br />

{ public override ICollection Create()<br />

{ return new LinkedList(); } }<br />

49


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

4.3.2 Bezug zu Typ-Definitionen in F#<br />

Datensatz-Definitionen (und Algebraische-Datentyp-Definitionen) in F# erfüllen<br />

bereits standardmäßig viele dieser Punkte: strukturelle Gleichheit, d.h. Equals<br />

unter Berücksichtigung aller Felder wird automatisch generiert, ebenso GetHash-<br />

Code; struktureller Vergleich (CompareTo) wird generiert (Felder werden nacheinander<br />

verglichen, beim ersten Unterschied <strong>steht</strong> das Vergleichsergebnis fest);<br />

ein Konstruktor wird erzeugt; der Typ wird als serialisierbar gekennzeichnet.<br />

Was die Verwendung des Typen angeht, wurde einerseits bereits auf das Pattern<br />

matching auf Datensätzen (S. 24) eingegangen. Zudem werden die Vergleichsoperatoren<br />

=, ( <strong>steht</strong> für ungleich) in Equals-Aufrufe und = in Aufrufe<br />

der CompareTo-Methode umgewandelt.<br />

Ein Nachteil bei Datensätzen in F# ist die Einschränkung, dass Datensätze zu<br />

Klassen kompiliert werden und nicht wahlweise zu Structs. Das kann in einigen<br />

Fällen zu unnötiger Beanspruchung des Garbage Collectors führen, insbesondere<br />

bei großen Datenmengen.<br />

4.3.3 Structs<br />

Ein kleiner Test soll dies veranschaulichen. Es werden Datencontainer für zweidimensionale<br />

Punkte erstellt, deren Komponenten Ganzzahlen 32-Bit-Genauigkeit<br />

sind. Eine Typ-Definition, die eine Datensatz-Klasse definiert, und eine, die einen<br />

explizit Structs erzeugt. Als einzige Operation darauf ist die Multiplikation mit<br />

einem Skalar definiert, die einen neuen Punkt mit multiplizierten Koordinaten<br />

erzeugt.<br />

> #time;;<br />

--> Timing now on<br />

> type Pos = { X: int; Y: int }<br />

with static member (*)(p: Pos, s: int) =<br />

{ X = p.X * s; Y = p.Y * s }<br />

[]<br />

type PosS =<br />

val X: int<br />

val Y: int<br />

new(x, y) = { X = x; Y = y }<br />

static member (*)(p : PosS, s) = PosS(p.X * s, p.Y * s);;<br />

...<br />

> let erzeugeObjekte =<br />

let ausgangsArray = Array.init 1000000 (fun i -> { X = i * 4 + 3;<br />

Y = i / 2 })<br />

let gemappt = Array.map(fun p -> p * 13) ausgangsArray<br />

”Ende des Testes”;;<br />

Real: 00:00:00.492, CPU: 00:00:00.265, GC gen0: 6, gen1: 3, gen2: 0<br />

...<br />

> let erzeugeStructs =<br />

let ausgangsArray = Array.init 1000000 (fun i -> PosS(i * 4 + 3, i<br />

/ 2))<br />

50


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

let gemappt = Array.map(fun p -> p * 13) ausgangsArray<br />

”Ende des Testes”;;<br />

Real: 00:00:00.015, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0<br />

...<br />

<strong>Die</strong> Zeitmessung hinter Real gibt die gemessene Zeit zwischen Ausführungsanfang<br />

und -ende an. <strong>Die</strong> Einträge nach GC geben die Anzahl der Garbage Collections an,<br />

die durchgeführt wurden.<br />

Für den Objekt-Test: 492 ms, 6 Generation-0-Garbage-Collections, 3 Generation-<br />

1-Garbage-Collections (9 GCs insgesamt). Für den Struct-Test: 15 ms, keine Garbage<br />

Collection.<br />

<strong>Die</strong> genaue Zeit und Anzahl von Garbage Collections ist von mehreren Faktoren<br />

abhängig. Garbage Collections sollten bei Performanz-kritischen Anwendungen<br />

vermieden werden, weil die Ausführung des eigentlichen Programmcodes während<br />

einer solchen pausiert wird und für die Laufzeit nachteilige Effekte auftreten,<br />

die mit Speicher- und Datenmanagement (Lokalitätseigenschaft) zu tun haben.<br />

Auffällig ist aber, dass im Falle der Structs keine Garbage Collection durchgeführt<br />

wurde, weil dort nur zwei Objekte, die beiden Arrays erzeugt wurden. Im<br />

Gegensatz dazu wurden im anderen Test zwei Millionen Pos-Objekte erstellt (eine<br />

Million für das Ausgangs-Array und eine Million nach durchgeführter Multiplikation).<br />

Es lohnt sich also, Structs verwenden zu können, auch wenn dadurch auf deklarative<br />

Typ-Definitionen und -Operationen verzichtet werden muss. In diesem Zusammenhang<br />

muss auch bedacht werden, dass das Verwenden einer Liste zur Erzeugung<br />

von vielen Cons-Zellen-Objekte führen kann (z.B. werden für [0..1000000]<br />

1000000 Conszellen erzeugt). Mit Messungen und Programmcodeanalyse kann<br />

auf diese Aspekte eingegangen werden.<br />

4.4 Imperativ-objektorientierte Fallunterscheidungen<br />

Fallunterscheidungen sind ein zentrales Element der Programmierung. Deshalb<br />

ist es wichtig, die Möglichkeiten zu kennen, um die beste auszuwählen und gleichzeitig<br />

einzuschätzen, welche Folgen deren Benutzung hat.<br />

Im Abschnitt Pattern matching (S. 26) wurde auf das gleichnamige Konstrukt eingegangen.<br />

Es ist nicht verwunderlich, dass diese Ausdrücke in Code niedrigerer<br />

Abstraktion umgewandelt werden müssen, um vom Computer durchgeführt werden<br />

zu können. Pattern matching wird in switch-Aufrufe umgewandelt sofern dies<br />

möglich ist.<br />

51


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

4.4.1 Mittel der Fallunterscheidungen reiner imperativer Programmierung<br />

if then else <strong>Die</strong>ses Konstrukt ist geeignet, wenn ein Code-Block abhängig von<br />

einer Bedingung hinter if in Form eines Bool’schen Ausdrucks ausgeführt<br />

werden soll. Der Block hinter then wird ausgeführt, wenn der Wert der Bedingung<br />

true ist, ansonsten wird die Alternative hinter else ausgeführt.<br />

Durch Schachtelung und Verkettung können beliebig komplexe Entscheidungsbäume<br />

programmiert werden, wobei ab einer schwer überschaubaren<br />

Größe von „Spaghetti-Code“ [InformatikLexikon, S. 842] die Rede ist.<br />

Ternärer Operator ? : Eine Variante von if then else, mit dem ternärem Operator<br />

Bedingung ? Folge : Alternative geschrieben wird, Ausdrücke aufnimmt<br />

und einen Ausdruck produziert.<br />

Switch Mit dem Konstrukt switch wird eine Sprungtabelle aufgebaut, die insbesondere<br />

dann sinnvoll ist, wenn als Werte Ganzzahlen verwendet werden.<br />

Try-Methoden Methoden mit mehreren Rückgabewerten werden gelegentlich<br />

über sogenannte Out-Parameter verwendet. Ein eingängiges Beispiel ist die<br />

DivRem-Methode, die den ganzzahligen Quotienten und Divisionsrest zurückgibt:<br />

int DivRem(int a, int b, out int rem){ rem = a % b; return a / b; }<br />

Verwendet wird dies in C# wie folgt:<br />

int rem; int quotient = DivRem(21, 5, out rem); //quotient = 4, rem = 1<br />

In F# ist die Nutzung von veränderbaren Variablen zwar möglich, sollte<br />

aber für solche eigentlich reinen Funktionen, die lediglich mehrere Rückgabewerte<br />

liefern, nicht verwendet werden. Daher können Out-Parameter<br />

wie zusätzliche Rückgabewerte behandelt werden:<br />

let rem, quotient = DivRem(21, 5)<br />

Wenn das Ergebnis partiell definiert ist, lässt sich über einen Bool’schen<br />

Rückgabe dies mitteilen. Das bereits verwendete Beispiel ist folgende in<br />

dem Typen int definierte Methode:<br />

static bool TryParse(string text, out int result)<br />

Try-Methoden entsprechen dem Pattern matching am ehesten, weil sowohl<br />

eine Unterscheidung der Fälle als auch eine Bindung von Variablen durchgeführt<br />

wird.<br />

4.4.2 Mittel der imperativ-objektorientierten Programmierung<br />

Subtyp-Polymorphie Im Vergleich <strong>zum</strong> Pattern matching <strong>steht</strong> die Funktionalität<br />

in verschiedenen Klassen, die jeweils den Fall für den eigenen Typ behandelt,<br />

anstatt in einer Funktion, welche die Typen (Varianten des algebraischen<br />

Datentyps (S. 23)) unterscheidet und somit alle Fälle behandelt.<br />

<strong>Die</strong> konkreten Klassen besitzen einen gemeinsamen Obertyp und implementieren<br />

oder überschreiben eine Methode. Das im Abschnitt Klasse (S. 25)<br />

erwähnte dynamische Binden bezieht sich in gängigen objektorientierten<br />

52


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Abbildung 4.2: Entscheidungsbaum für Fallunterscheidungen.<br />

Programmiersprachen auf Single Dispatch, d.h. die Operation hängt vom<br />

Typ des Empfängers ab. <strong>Die</strong>se Vorgehensweise ist in der Objektorientierung<br />

zentral, insbesondere für Parameter einer Methode den passenden Obertyp<br />

zu wählen. Damit wird nur auf die Schnittstelle, nicht auf eine konkrete<br />

Implementierung, hin implementiert.<br />

Entwurfsmuster Besucher (Visitor) Das Entwurfsmuster Besucher (auch unter<br />

Visitor bekannt) [Entwurfsmuster, S. 269] benutzt den Double-Dispatch-<br />

Mechanismus. Bei Double Dispatch hängt die Operation von den Typen<br />

zweier Empfänger ab [Entwurfsmuster, S. 277]. Sprachen wie CLOS verwenden<br />

Multiple Dispatch, bei dem die Operation vom Typ des Empfängers<br />

und den Typen aller Parameter abhängt.<br />

4.4.3 Wann ist welche Technik zu benutzen?<br />

<strong>Die</strong> gerade genannten Techniken sollen im Abschnitt „Evaluation der Fallunterscheidungstechniken“<br />

(S. 54) in einem überschaubaren Beispiel gegenübergestellt<br />

werden. Es geht um einen Interpreter, der einfache Ausdrücke auswertet, die<br />

aus Funktionsapplikation, Variablenbindung, Fall-Unterscheidung und Lambda-<br />

Ausdrücken bestehen.<br />

<strong>Die</strong> vollständige Implementierung findet sich im Anhang im Abschnitt „Beispiel<br />

Evaluator“ (S. 75).<br />

Eine Übersicht über die Techniken gibt das Diagramm 4.4.3.<br />

53


5<br />

Evaluation der<br />

Fallunterscheidungstechniken<br />

Wie im Abschnitt Imperativ-objektorientierte Fallunterscheidungen (S. 51) erwähnt,<br />

gibt es mehrere Möglichkeiten, Fälle zu unterscheiden und die Anwendungs-<br />

Logik für jeden Fall zu programmieren. Ich habe diese Konstrukte und Techniken<br />

genannt:<br />

• if then else und ternärer Operator ? :<br />

• switch<br />

• Try-Methoden<br />

• Polymorphie<br />

• Visitor<br />

• Pattern matching<br />

Das folgende Beispiel soll diese Techniken in Aktion zeigen und Vor- und Nachteile<br />

am Quellcode konkretisieren. Der gesamte Quellcode findet sich im Anhang<br />

im Evaluator-Abschnitt (S. 75).<br />

Es geht um die Implementierung eines einfachen Interpreters/Evaluators, der<br />

einfache Ausdrücke auswertet, die aus Funktionsapplikation, Variablenbindung,<br />

Fall-Unterscheidung und Lambda-Ausdrücken bestehen. Unter Verwendung der<br />

Scheme-Sytax geht es um Ausdrücke der folgenden Form:<br />

• Konstante, z.B. -15, 0, 42, true, false<br />

• Variable, z.B. x, eineVariable<br />

• Fallunterscheidung (if Dann Sonst)<br />

• Bindung (let ([Variablenname Wert])Ausdruck)<br />

• Lambda-Ausdruck (lambda (Parameter1 Parameter2 ...)Ausdruck)<br />

• Applikation (Funktionsausdruck Argument1 Argument2 ...)<br />

• Vordefinierte Funktionen +, =, <<br />

54


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Im Anhang finden sich ebenfalls Test-Klassen (S. 93), welche die Konstrukte testet.<br />

Dazu wird auf dynamische Typisierung (dynamic in C#) zurückgegriffen, da<br />

sich die verschiedenen Implementierungen keine gemeinsame Schnittstelle teilen<br />

1 . Da die Struktur in allen Klassen dieselbe ist, können Ausdrücke in der Test-<br />

Klasse mithilfe von Konstruktoren der Typparameter (über das Constraint where<br />

T: new()) und Feldzuweisungen (etwa variable.Name = ”x”) Ausdrücke zusammengesetzt<br />

werden.<br />

Ich fange mit der deklarativen Definition der Typen in F# an:<br />

/// Stellt einen Ausdruck oder ein Ergebnis einer<br />

/// simplen Programmiersprache dar.<br />

type Code =<br />

/// Number(Konstante Ganzzahl)<br />

| Number of int<br />

/// Bool(Konstanter Wahrheitswert)<br />

| Bool of bool<br />

/// Var(Name)<br />

| Var of string<br />

/// Binding(Name, Wert, Ausdruck)<br />

| Binding of string * Code * Code<br />

/// Conditional(Bedingung, Folge, Alternative)<br />

| Conditional of Code * Code * Code<br />

/// Application(Funktion, Argument-Liste)<br />

| Application of Code * Code list<br />

/// Lambda(Parameter-Namen, Ausdruck)<br />

| Lambda of string list * Code<br />

/// Closure = Lambda + Variablenbindungen<br />

| Closure of Map * string list * Code<br />

/// BuiltInFunc(F#-Funktion) (die Funktion erhält<br />

/// ausgewertete Argumente)<br />

| BuiltInFunc of (Code list -> Code)<br />

<strong>Die</strong> Bedeutung der Daten, die den einzelnen Varianten zugeordnet sind, lässt sich<br />

aus der reinen Definition nur schwer ablesen. Deshalb <strong>steht</strong> dies in der Dokumentation<br />

der Varianten. Bei Pattern matching ist darauf zu achten, dass den Variablen,<br />

an welche die Datenfelder gebunden werden sprechende Namen gegeben<br />

werden (z.B. Binding(name, value, body) statt Binding(x, y, z)).<br />

5.1 Pattern matching<br />

Der Evaluator in F# basiert auf Pattern matching und verwendet immutable maps<br />

zur Speicherung von Variablenbindungen.<br />

module Evaluator =<br />

let rec eval env expr =<br />

match expr with<br />

1 Das wäre über diverse Interfaces wie IBinding möglich gewesen, hätte aber die Implementierungen<br />

ein wenig umfangreicher gemacht.<br />

55


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

| Number _ | Bool _ | BuiltInFunc _ | Closure _ -> expr<br />

| Lambda(param, body) -> Closure(env, param, body)<br />

| Var name -> Map.find name env<br />

| Binding(name, value, body) -><br />

eval (Map.add name (eval env value) env) body<br />

| Conditional(condition, thenExpr, elseExpr) -><br />

match eval env condition with<br />

| Bool false -> eval env elseExpr<br />

| _ -> eval env thenExpr<br />

| Application(func, args) -><br />

match eval env func with<br />

| Closure(extendedEnv, names, body) -><br />

let addToMap map (key, value) = Map.add key value map<br />

let newEnv =<br />

List.fold addToMap extendedEnv<br />

(List.zip names (List.map(eval env) args))<br />

eval newEnv body<br />

| BuiltInFunc(func) -><br />

func(List.map(eval env) args)<br />

| other -> failwithf ”Only closures and built-in functions<br />

can be applied, found: %A” other<br />

let Evaluate(expr) = eval Map.empty expr<br />

<strong>Die</strong> Funktionsweise des Evaluators ist leicht erklärt:<br />

1. Number ...: Ein konstanter atomarer Wert (Zahl oder Wahrheitswert) oder<br />

eine Funktion wird unausgewertet zurückgegeben.<br />

2. Lambda: Ein Lambda-Ausdruck wird mit allen bis dahin erzeugten Variablenbindungen<br />

als Closure zurückgegeben.<br />

3. Var: Eine Variable wird in den Variablenbindungen nachgeschlagen. Es kommt<br />

zu einem Laufzeitfehler, wenn der Wert <strong>zum</strong> Variablenbezeichner nicht gefunden<br />

wurde.<br />

4. Binding: Eine Variablenbindung wird hergestellt und der Code nach der Bindung<br />

wird ausgeführt.<br />

5. Conditional: <strong>Die</strong> Bedingung wird ausgewertet: Ist sie falsch (Bool false),<br />

wird der Alternativ-Zweig ausgewertet; alles außer falsch wird als wahr<br />

interpretiert und führt zur Auswertung des Folge-Zweiges.<br />

6. Aplication: Der Funktionsausdruck wird ausgewertet:<br />

(a) Closure: Ist er eine Closure, werden die Argumente mit den aktuellen<br />

Variablenbindungen ausgewertet. <strong>Die</strong>se werden an die Parameter innerhalb<br />

der zusätzlichen Closure-Variablenbindungen gebunden und<br />

der Körper der Funktion wird mit diesen Bindungen ausgewertet.<br />

(b) BuiltInFunc: Ist er eine „eingebaute“ Funktion, wird diese auf die ausgewerteten<br />

Argumente angewendet.<br />

(c) Sonst: Ist der ausgewertete Ausdruck keine Funktion, gibt es einen<br />

Laufzeitfehler, der darauf hinweist, dass der zu applizierende Ausdruck<br />

ein Funktionsausdruck sein muss.<br />

56


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Pattern matching hat den großen Vorteil, dass es intuitiv zu verwenden ist. <strong>Die</strong> Erweiterung<br />

um neue Fälle wurde bereits durch die Algebraische-Datentyp-Definition<br />

insofern eingeschränkt, als dass der einzige Erweiterungspunkt die eingebauten<br />

Funktionen (BuiltInFunc) sind. <strong>Die</strong>se enthalten eine Funktion, die zusätzliche<br />

Funktionalität enthält. <strong>Die</strong>s ist Komposition auf der Ebene algebraischer Datentypen<br />

und wird <strong>zum</strong> Ende des Kapitels noch einmal zusammengefasst.<br />

In [SICP, vgl. S. 120] werden Scheme-Typen durch diese Funktionen definiert:<br />

1. Konstruktor, z.B. (erzeuge-punkt x y)<br />

2. Selektor, z.B. (punkt-x einPunkt) und (punkt-y einPunkt)<br />

3. Typ-Prädikat, z.B. (punkt? einPunkt)<br />

<strong>Die</strong> Forderungen für diese Funktionen sind:<br />

(punkt-x (erzeuge-punkt x _)) = x<br />

(punkt-y (erzeuge-punkt _ y)) = y<br />

(punkt? (erzeuge-punkt _ _)) = true<br />

<strong>Die</strong>s hat den Vorteil, dass nur diese Funktionen zur Verfügung stehen müssen und<br />

die interne Repräsentation der Datenstrukturen dahinter verborgen wird. So wird<br />

eine starke Repräsentationsflexibilität erreicht. In imperativ-objektorientierten<br />

Programmiersprachen kann dies z.B. auch mit Interfaces erreicht werden, die nur<br />

Selektor-Methoden (Getter) bereitstellen. In objektorientierten Systemen sind Interfaces<br />

für reine Datenklassen nicht sehr üblich, dieselbe Flexibilität kann mit<br />

ihnen dennoch dargestellt werden:<br />

1. Konstruktor, z.B. new Punkt(x, y), wobei Punkt das Interface IPunkt implementiert<br />

2. Selektoren, z.B. interface IPunkt { int X { get; } int Y { get; } }<br />

3. Typ-Prädikat, z.B. einPunkt is IPunkt<br />

<strong>Die</strong> Forderungen sind analog:<br />

new Punkt(x, _).X = x<br />

new Punkt(_, y).Y = y<br />

new Punkt(_, _) is IPunkt<br />

Mit algebraischen Datentypen und Pattern matching fallen diese Schritte <strong>zum</strong><br />

intuitiven Muster und Konstruktor zusammen, bieten dadurch allerdings keine<br />

Repräsentationsflexibilität.<br />

1. Der algebraische Datentyp type Punkt = Punkt of int * int führt <strong>zum</strong> Muster<br />

(= Typ-Prädikat + Selektor) und Konstruktor Punkt(x,y).<br />

2. Der Datensatztyp type Punkt = { X: int; Y: int } führt <strong>zum</strong> Muster und<br />

Konstruktor { X = x; Y = y }.<br />

3. Der Tupeltyp int * int besitzt das Muster und den Konstruktor (x,y).<br />

Auf dieselbe Weise, mit der in Scheme dem Programmierer Konstruktoren und<br />

Selektoren über ein Modul zur Verfügung gestellt werden müssen, kann auch ein<br />

57


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Active Pattern (|Punkt|) zur Verfügung gestellt werden, das einen nicht näher<br />

spezifizierten Typen ^a in ein Tupel int * int umwandelt.<br />

let inline (|Punkt|)(p: ^a) =<br />

let x = (^a: (member get_X: unit -> int) p)<br />

let y = (^a: (member get_Y: unit -> int) p)<br />

(x, y)<br />

<strong>Die</strong>ser Typ muss die Properties X und Y vom Typ int besitzen. Sofern dies vom<br />

Typsystem nachgewiesen werden kann, lässt sich dieses Muster als Selektor verwenden.<br />

Der Konstruktor kann wie im objektorientierten Fall eine Klasse mit entsprechenden<br />

Properties sein.<br />

Repräsentationsflexibilität in F# kann mit objektorientierten Interfaces oder mit<br />

generischen Funktionen erreicht werden. Typ-Prädikate sind in diesem Fall in<br />

einer statisch typisierten Programmiersprachen nicht notwendig. Zwei verschiedene<br />

Repräsentationsformen und eine Funktion, die beide Repräsentationen gleichermaßen<br />

verwenden kann, sind <strong>hier</strong> aufgeführt.<br />

// ADT = Algebraischer Datentyp<br />

type ADTPunkt = ADTPunkt of int * int with<br />

member this.X = let(ADTPunkt(x,_)) = this in x<br />

member this.Y = let(ADTPunkt(_,y)) = this in y<br />

type DatensatzPunkt = { X: int; Y: int }<br />

// Beispiel-Funktion length: ^a -> int * int<br />

// when ^a : (member get_X : ^a -> int)<br />

// and ^a : (member get_Y : ^a -> int)<br />

let inline length(Punkt(x, y)) =<br />

sqrt(float(x * x + y * y))<br />

// Verwendung<br />

printfn ”%d” (length { X = 1; Y = 1 }) // gibt 1.414214 aus<br />

printfn ”%d” (length(ADTPunkt(1, 1))) // ebenso<br />

5.2 Subtyp-Polymorphie<br />

<strong>Die</strong> intuitive Herangehensweise bei objektorientierter Programmierung ist Subtyp-<br />

Polymorphie, die sich wie in Abbildung 5.1 darstellen lässt und im Polymorphie-<br />

Abschnitt im Anhang (S. 85) zu finden ist. Wie im Diagramm dargestellt wird eine<br />

Oberklasse erstellt, die zwei Operationen enthält: Evaluate und Apply, die beide<br />

Standard-Implementierungen besitzen (<strong>hier</strong>: keine weitere Evaluation und keine<br />

Applizierbarkeit). In Unterklassen werden Methoden implementiert, um dieses<br />

Verhalten für konkrete Fälle umzusetzen, wie etwa Variablen, die nachgeschlagen<br />

werden.<br />

58


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Abbildung 5.1: Auschnitt der Klassen<strong>hier</strong>archie für Ausdrücke einer simplen Programmiersprache.<br />

5.3 Philip Wadlers expression problem<br />

Ein wichtiger Aspekt bei dieser Diskussion ist Erweiterbarkeit, sowohl um neue<br />

Fälle (Varianten) und Operationen (Funktionen). Eine 1998 entstandene Bezeichnung<br />

für diesen Sachverhalt ist Philip Wadlers expression problem:<br />

„The Expression Problem is a new name for an old problem. The goal is to<br />

define a datatype by cases, where one can add new cases to the datatype and<br />

new functions over the datatype, without re<strong>com</strong>piling existing code, and while<br />

retaining static type safety (e.g., no casts). For the concrete example, we take<br />

expressions as the data type, begin with one case (constants) and one function<br />

(evaluators), then add one more construct (plus) and one more function<br />

(conversion to a string). “ [ExpressionProblem]<br />

Eine Auflistung von bisherigen Lösungsansätzen und ein eigener Ansatz wird in<br />

[Scala] präsentiert. In den abschließenden Betrachtungen gehe ich auf diesen<br />

Aspekt ein und stelle den Bezug der Techniken auf diese Forderungen grafisch<br />

dar.<br />

5.4 Imperative Ansätze<br />

<strong>Die</strong> imperativen Ansätze, switch über Type Code bzw. if-then-else mit Typtests<br />

und -casts, orientieren sich an der Funktionsweise von Pattern matching, verwenden<br />

jedoch explizite Typecast und Typprüfungen. Auf diese Weise können Funktionen<br />

unabhängig von den Varianten definiert werden. Neue Varianten dürfen<br />

nicht hinzugefügt werden, weil bestehende Operationen neue Fälle nicht behandeln<br />

können. Bei algebraischen Datentypen ist dies per Definition ausgeschlossen,<br />

59


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

für objektorientierte Sprachen wurde dies im Teil über Varianten im Abschnitt<br />

über imperativ-objektorientierte Typen (S. 49) gezeigt.<br />

Im Beispiel nimmt die Evaluate-Methode des Evaluators diese Unterscheidung<br />

direkt vor. <strong>Die</strong> Fallunterscheidung (ob mit if-then-else oder mit switch) als Programmierer<br />

selbst vorzunehmen, führt zu Boilerplate-Code und zu einem schwer<br />

um neue Fälle erweiterbaren Entwurf.<br />

public Code Evaluate(Dictionary env, Code expr) {<br />

Var variable = expr as Var;<br />

if(variable != null) {<br />

return env[variable.Name];<br />

}<br />

...<br />

}<br />

Try-Methoden werden in dieser Darstellung nicht weiter betrachtet, weil ihr Einsatz<br />

in diesem Beispiel wenig Gewinn im Vergleich <strong>zum</strong> Typtest und Typecast<br />

bringt. Beispielsweise ist für die Variante Var Folgendes vorstellbar, benötigt aber<br />

die Methode bool TryVar(out string name) in der Basisklasse, die dort false zurückgibt<br />

und nur in der Klasse Var das Feld an den Out-Parameter bindet und true<br />

zurückgibt.<br />

public Code Evaluate(Dictionary env, Code expr) {<br />

string name;<br />

if(expr.TryVar(out name)) {<br />

return env[name];<br />

}<br />

...<br />

}<br />

5.5 Das Besucher-Entwurfsmuster (Visitor pattern)<br />

Beim Besucher-Entwurfsmuster geht es um eine einfache Typunterscheidung, sodass<br />

die eben genannte Typ-Unterscheidung nicht mit Typtests und Typecasts<br />

durchgeführt werden muss. <strong>Die</strong> oben genannte Voraussetzung bleibt bestehen,<br />

dass die Hinzunahme neuer Fälle alte Operationen ungültig macht.<br />

Ein Nachteil ist relativ viel Boilerplate-Code, der in jeder Unterklasse eingefügt<br />

werden muss. Das Visitor-Interface muss Kenntnis über alle Varianten besitzen.<br />

Ein viel gravierender Nachteil ist, dass das Besucher-Entwurfsmuster im Gegensatz<br />

<strong>zum</strong> Pattern matching nur eine einfache Typ-Unterscheidung vornimmt und<br />

mit geschachtelten Unterscheidungen nicht umgehen kann. <strong>Die</strong>s wird im Fall des<br />

Evaluator-Beispiels dadurch deutlich, dass bei einer Applikation geprüft wird,<br />

was appliziert werden soll. Im Code für den Visitor-Ansatz (S. 88) wird dies<br />

mit einer zweiten Visitor-Klasse gelöst, die dann diese Entscheidung vornimmt.<br />

Speicherung von Zusatzinformationen, unter anderem die Argumente der Applikation<br />

und die Variablenbindungen, werden zur Aufgabe des Programmierers;<br />

60


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

im Pattern-matching- und imperativen Evaluator hingegen befanden sich diese<br />

Werte im Variablen-Sichtbereich und standen somit zur Verfügung. Im Fall von<br />

Subtyp-Polymorphie wurden die Argumente der Applikation als Parameter übergeben.<br />

5.6 Abschließende Betrachtung<br />

<strong>Die</strong>se Untersuchung hat einen Teil der Möglichkeiten der Fallunterscheidungen<br />

dar- und gegenübergestellt. Ein wichtiger Aspekt bei dieser Diskussion ist das<br />

Expression Problem und die Frage, wieweit Fälle und Operationen hinzugefügt<br />

werden können. Damit im Zusammenhang <strong>steht</strong> das Open Closed Principle, das<br />

besagt, das objektorientierte Klassen offen für Erweiterungen (vor allem Fälle)<br />

und geschlossen gegenüber Änderungen sein sollen (Quelltext von Basisklassen<br />

soll nicht neukompiliert werden müssen). Gerade durch diese Anforderung an<br />

Klassen wird die Notwendigkeit der Kompositionierbarkeit und Modularität deutlich,<br />

die Mixins bzw. Traits bieten. <strong>Die</strong>se Techniken gibt es in beispielsweise in<br />

Scala und werden für den Lösungsansatz in [Scala] exzessiv verwendet.<br />

Auf der anderen Seite sind deklarative Fallunterscheidungen mit Pattern matching<br />

intuitiv zu formulieren, berufen sich aber auf eine abgeschlossene Variantenanzahl.<br />

Active Patterns können mit Klassifizierern (S. 30) eine Varianten-ähnliche<br />

Sicht z.B. auf eine Klassen<strong>hier</strong>archie bieten. <strong>Die</strong>s wurde in [ActivePattern, S. 42]<br />

im Fall vom Typ Type (der einen Typen darstellt) als Beispiel angeführt.<br />

Im Beispiel habe ich für den Evaluator definiert, dass die einzige Erweiterung<br />

durch eingebaute Funktionen (BuiltInFuncs) stattfinden darf. Im Falle, dass neue<br />

Operationen auf diesen Datentypen hinzugefügt werden soll, etwa eine Übersetzung<br />

des Ausdrucks in einen Scheme-Ausdruck (z.B. in Form eines Strings), lässt<br />

sich dies ebenfalls als Funktion umsetzen, die Pattern matching verwendet.<br />

Das Beispiel hat durchaus Praxisrelevanz, nicht nur auf konzeptioneller Ebene,<br />

weil Syntaxbäume für Metaprogrammierung sinnvoll sind, sondern weil die konkreten<br />

Basisbibliotheken von C# und F# diese Typen verwenden. In F# sind dies<br />

die schon genannten Quotations (S. 42), die ausführlicher in [FSharp, S. 498]<br />

diskutiert werden und in C# ist dies die Klassen<strong>hier</strong>archie der LINQ-Expressions<br />

(Details finden sich in [CSharp, S. 377ff.]).<br />

<strong>Die</strong> Verwendung der Typen ist bei F#-Quotations mit Pattern matching und Active<br />

Patterns konzipiert 2 und bei C#-LINQ-Expressions ist die Visitor-Basisklasse<br />

System.Linq.Expression.ExpressionVisitor zu verwenden.<br />

Eine andere Überlegung stammt aus der dynamisch typisierten objektorientierten<br />

Programmiersprache Smalltalk und geht davon aus, dass der Quelltext von Basisklassen<br />

(z.B. die Smalltalk-Klassen Object oder String) offenliegt und während<br />

der Anwendungsprogrammierung verändert werden darf. So lassen sich neue<br />

(abstrakte) Operationen in der Basisklasse einfügen und in Unterklassen über-<br />

2 Verschiedene Active Patterns sind unter Microsoft.FSharp.Quotations zu finden.<br />

61


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Abbildung 5.2: Techniken und ihre Einordnung nach den Kriterien Erweiterbarkeit<br />

um neue Fälle (Varianten) und Operationen (Funktionen).<br />

schreiben (bzw. implementieren). In der ebenfalls dynamisch typisierten objektorientierten<br />

Programmiersprache Ruby ist dies auch vorgesehen und beruft sich<br />

auf Metaprogrammierungstechniken wie eval (Ausführen eines Strings als Programmcode),<br />

class_eval zur Änderung von Klassen und define_method <strong>zum</strong> Hinzufügen<br />

von Methoden.<br />

62


6<br />

Fazit<br />

6.1 Dargestellte Aspekte<br />

Gesamtbild „deklarativ“: Nach einer Defintionsgegenüberstellung habe ich eine<br />

eigene Definition gegeben, um unter anderem funktionale Programmiersprachen<br />

mit Seiteneffekten als Vertreter einzuordnen und deklarative Techniken<br />

(Regeln: Pattern matching und Active Patterns, Zusammenhänge: Computation<br />

Expression) zu untersuchen, die auf hoher Abstraktionsebene arbeiten,<br />

ohne dass Details immer wieder ausprogrammiert werden müssen.<br />

Im Fall von Active Patterns und Computation Expressions müssen diese Details<br />

einmal programmiert werden und lassen sich wiederverwenden.<br />

Historische Heranführung: Prolog und Miranda haben eine überschaubare Syntax,<br />

basieren auf wenigen Konzepten und können damit bereits viele Probleme<br />

elegant lösen. Referenzielle Transparenz ist eine nützliche Eigenschaft<br />

für das Programm-Verstehen und die Parallelisierung. Multiparadigmatische<br />

Programmiersprachen sind komplexer und umfangreicher, dafür aber<br />

mächtiger und flexibler, da Zugriff auf mehrere Herangehensweisen (deklarative<br />

und imperative) gewährt wird. Das Abwägen, welche sich anbieten<br />

war Inhalt dieser Arbeit.<br />

Deklarative Typ-Definitionen: In F# lassen sich Datensätze und algebraische<br />

Datentypen einfach definieren. Mit wenig Schreibaufwand werden komplexe<br />

Typen und Klassen<strong>hier</strong>archien definiert.<br />

Wert-Verarbeitung (Pattern matching): Deklarativ definierte Typen können<br />

mit Pattern matching intuitiv verarbeitet werden.<br />

DSLs und Metaprogrammierung: Wenn die Sprache um neue Konstrukte erweitert<br />

werden soll, kann dies mit Metaprogrammierung getan werden. Interne<br />

DSLs stellen eine Fokussierung der Programmiersprache auf ein Problemfeld<br />

dar. <strong>Die</strong> Umsetzung der somit formulierten Sprache muss nicht<br />

an die Standardauswertungsumgebung gebunden sein, sondern kann eine<br />

andere sein.<br />

63


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Passende imperative Techniken und Herangehensweisen wurden dargestellt und<br />

Zusammenhänge zur deklarativen Programmierung gezeigt:<br />

Veränderlichkeit / Konsistenzprüfung: Konsistenz-Prüfungen dienen einem sicheren<br />

Umgang mit Seiteneffekten und null-Werten.<br />

Typ-Defintionen und -Belange: Viele Aufgaben sind bei Typ-Definition zu erledigen,<br />

um verwendbare Typen zu programmieren: Gleichheit, Initialisierung,<br />

Kopieren, Persistierung, Verwendung in Klassen<strong>hier</strong>archien.<br />

Fallunterscheidungen: Behandelt wurden if-then-else und switch auf der reinen<br />

imperativen Seite und Subtyp-Polymorphie und das Entwurfsmuster<br />

Besucher auf der objektorientierten Seite. Auf Fallunterscheidungstechniken<br />

bin ich besonders eingegangen, weil sie den Regel-Begriff aus meiner<br />

Definition darstellen. <strong>Die</strong> Darstellung gegenüber Pattern matching wurde in<br />

der Evaluation gezeigt. In diesem Sinne habe ich Bezug auf das Expression<br />

Problem genommen und die zwei Dimensionen der Erweiterbarkeit für die<br />

Lösungen dargestellt.<br />

6.2 Ausgelassene Aspekte<br />

Scala: Scala ist umfangreich, sowohl die Anzahl der Konzepte als auch die Basis-<br />

Bibliothek. Außerdem gibt es in Scala neue Konzepte, die es vorher in der<br />

objekt-funktionalen Form noch nicht gegeben hat. Ein Beispiel sind Module<br />

als first class values, die bei der Keynote der Scala-Konferenz flatMap in Oslo<br />

(13. - 14.05.2013) vorgestellt wurden 1 . Zudem habe ich nicht genügend<br />

Praxiserfahrung mit Scala.<br />

Weitere Entwurfsmuster und funktionale Konzepte: Entwurfsmuster kann man<br />

als objektorientiertes Handwerkszeug ansehen: Man muss Entwurfsmuster<br />

verstehen, um mit objektorientierten Programmbibliotheken arbeiten zu<br />

können und auf der anderen Seite Entwurfsmuster in eigenen Bibliotheken<br />

verwenden, damit andere objektorientierte Programmierer diese wiederverwenden<br />

können.<br />

In [AnalyseEntwurfsmusterFP] wird ein Vergleich von Entwurfsmustern mit<br />

funktionalen Konzepten umfangreich dargestellt, deshalb bin ich bin in dieser<br />

Arbeit nicht auf weitere Entwurfsmuster eingegangen.<br />

Eine gute Einführung in funktionale Programmierung, in der verzögerte<br />

Auswertung (Laziness) und Funktionen höherer Ordnung als Eckpfeiler für<br />

modulare Programmierung vorgestellt werden, ist [WhyFPMatters]. Funktionen<br />

höherer Ordnung gehören <strong>zum</strong> Handwerkszeug des funktionalen Programmierers<br />

und wurden in dieser Arbeit wie selbstverständlich verwendet.<br />

In moderneren objektorientierten Programmiersprachen halten sie zusammen<br />

mit Lambda-Ausdrücken als Features Einzug.<br />

1 http://2013.flatmap.no/spiewak.html<br />

64


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

Expression Problem: Bei der Evaluation der Fallunterscheidungstechniken (S.<br />

54) gibt es nicht nur den Aspekt der Eleganz sondern auch die Frage um Erweiterbarkeit<br />

mit Funktionen und Fällen. Bei dem oben genannten Vortrag<br />

wird das Problem aufgegriffen und es wird eine Lösung aus der dynamisch<br />

typisierten funktionalen Programmiersprache Clojure (einem LISP-Ableger<br />

von 2007) vorgestellt, die auf extern definierten Funktionen wie in CLOS<br />

basiert. Eine statisch typisierte Lösung ist in [Scala] zu finden.<br />

Objektorientierte Sicht: Ich habe mich für F# und nicht C# entschieden, weil<br />

ich nicht den Weg von der Objektorientierung zur funktionalen Programmierung<br />

einschlage, sondern im deklarativ-funktionalen Programmiersprachenraum<br />

anfange, Konzepte zu untersuchen. Ein sehr gutes Buch, das C#-<br />

Programmierer die funktionale Denkweise (mit C# und F#) anschaulich<br />

vermittelt, ist [RealWorldFP].<br />

Formale Programmiersprachen-Untersuchung: Formale Untersuchungen über<br />

funktionale Programmiersprachen sind aufgrund des mathematischen Anspruchs,<br />

den diese Programmiersprachen haben, sinnvoll. Dasselbe gilt für<br />

Untersuchungen im Bereich der Logik zur Logik-Programmierung. In diesem<br />

Zusammenhang werden deklarative Programmiersprachen gerne „Akademikersprachen“<br />

genannt, die sich eher nicht für typische Anwendungsprogrammierung<br />

eignen und nur im universitären Umfeld genutzt werden. <strong>Die</strong>s<br />

wird durch Konzepte wie Monaden und Typklassen aus Haskell verschärft,<br />

die Akademiker benutzen, für durchschnittliche Programmierer hingegen<br />

kompliziert zu verwenden sind. Haskell <strong>steht</strong> somit anderen „gängigeren“<br />

Programmiersprachen in der Akzeptanz nach.<br />

In diesem Sinne halte ich eine formale Betrachtungsweise von F# im Zuge<br />

dieser <strong>Bachelorarbeit</strong> nicht für sinnvoll.<br />

6.3 Schlusswort<br />

Deklarative Programmierung wurde in dieser Arbeit als Kombination von moderner<br />

funktionaler Programmierung und der Verwendung von DSLs vorgestellt.<br />

Insbesondere Programmiersprachen wie F# und Scala bringen diese beiden Aspekte<br />

in die Welt der Anwendungsprogrammierung, sodass Probleme mithilfe von<br />

funktionaler Programmierung und objektorientierten Programmbibliotheken gelöst<br />

werden können, was insbesondere in Bezug auf das Programmieren von grafischen<br />

Benutzeroberflächen und Datenbanken nützlich ist. Um den in einigen<br />

Situationen geforderten schnellen wahlfreien Zugriff auf große Datenmengen zu<br />

gewährleisten, kann auf das imperative Array zurückgegriffen werden.<br />

Nicht nur die deklarative Seite wurde beleuchtet, auch Teile der imperativen Programmierung<br />

wurden angeschnitten, wobei die imperative Welt viel umfangreicher<br />

ist, als ich in dieser <strong>Bachelorarbeit</strong> behandeln könnte. <strong>Die</strong> deklarative Seite<br />

von F# hingegen wurde zu großen Teilen vorgestellt, weil sie auf überschaubar<br />

wenigen Konzepten basiert (z.B. Pattern matching (S. 26) und einfach zu verwen-<br />

65


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

dende Typen (S. 21)). Das mag auch der Grund sein, warum einige deklarative<br />

Sprachen wie Prolog oder Scheme in kürzerer Zeit gelehrt werden können als<br />

imperative-objektorientierte Programmierung, die umfangreich und komplex ist.<br />

Damit soll durchaus nicht relativiert werden, dass auch funktionale und Logik-<br />

Programmierung ein weites Feld sind.<br />

<strong>Die</strong>se Arbeit lässt sich als Kompendium für deklarative Programmierung mit F#<br />

verwenden, wobei Pattern matching und Typen eine zentrale Rolle spielen.<br />

66


Literaturverzeichnis<br />

[ActivePattern] Don Syme, Gregory Neverov, James Margetson: Extensible Pattern<br />

Matching via a Lightweight Language Extension. ICFP ’07 Proceedings of<br />

the 12th ACM SIGPLAN international conference on Functional programming(S.<br />

29-40). ACM Press. 2007.<br />

[AnalyseEntwurfsmusterFP] Johannes Pietrzyk: Analyse der Entwurfsmuster der<br />

Viererbande unter dem Funktionalen Paradigma. <strong>Bachelorarbeit</strong> am Fachbereich<br />

Informatik an der Universität Hamburg. 2012.<br />

[CSharp] Joseph Albahari, Ben Albahari: C# 5.0 in a Nutshell. 5te Auflage.<br />

O’Reilly. 2012.<br />

[Cormen] Thomas H. Cormen, Charles Leiserson, Ronald L. Rivest, Clifford Stein:<br />

Algorithmen - Eine Einführung. 3te Auflage. Oldenbourg Verlag. 2010.<br />

[Curry] Michael Hanus: Teaching Functional and Logic Programming with a<br />

Single Computation Model. Programming Languages: Implementations, Logics,<br />

and Programs(S. 335-350). Springer Berlin Heidelberg. 1997.<br />

[DeklarativeProgrammierung] Marc H. Scholl: Deklarative Programmierung. Vorlesungsskript<br />

an der Universität Konstanz. 2006. URL: http://www.inf.unikonstanz.de/dbis/teaching/ss06/fp/fp.pdf<br />

(abgerufen am 24.07.2013)<br />

[DSLs] Martin Fowler, Rebecca Parsons: Domain-specific languages. Addison-<br />

Wesley. 2011.<br />

[EffectiveJava] Joshua Block: Effective Java. 2te Auflage. Addison-Wesley. 2008.<br />

[Entwurfsmuster] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides:<br />

Entwurfsmuster - Elemente für wiederverwendbare Software. 6te Auflage.<br />

Addison-Wesley. 2010.<br />

[ExpressionProblem] E-Mail-Diskussion, E-Mail von Philip Wadler<br />

(wadler@research.bell-labs.<strong>com</strong>): The Expression Problem. 1998. URL:<br />

http://www.daimi.au.dk/~madst/tool/papers/expression.txt (abgerufen<br />

am 27.07.2013).<br />

67


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

[FPWithMiranda] Ian Holyer: Functional Programming with Miranda. University<br />

College London (ULC) Press. 1993.<br />

[FSharp] Don Syme, Adam Granicz, Antonio Cisternino: Expert F# 3.0. 3te Auflage.<br />

Apress. 2012.<br />

[FSharpMeta] Don Syme: Leveraging .NET Meta-programming Components<br />

from F# - Integrated Queries and Interoperable Heterogeneous Execution.<br />

Proceedings of the 2006 workshop on ML. ACM New York. 2006. URL:<br />

http://research.microsoft.<strong>com</strong>/apps/pubs/default.aspx?id=147193 (abgerufen<br />

am 28.07.2013).<br />

[FSharpSpec] Don Syme: The F# 3.0 Language Specification. 2012. URL:<br />

http://fsharp.org/about/files/spec.pdf (abgerufen am 21.07.2013).<br />

[FunctionalArrays] Melissa E. O’Neil, F. Warren Burton: A New Method for Functional<br />

Arrays. Journal of Functional Programming archive Volume 7 Issue 5,<br />

September 1997(S. 487-513). Cambridge University Press. 1997.<br />

[FunktProg] Peter Pepper, Petra Hofstedt: Funktionale Programmierung: Sprachdesign<br />

und Programmiertechnik. eXamen.press, Springer-Verlag. 2006.<br />

[InformatikLexikon] Peter Fischer, Peter Hofer: Lexikon der Informatik. 14te Auflage.<br />

Springer. 2007.<br />

[LanguageOrientedProg] Martin P. Ward: Language Oriented Programming. Software—Concepts<br />

and Tools 1995. Band 15. S. 147-161. 1995.<br />

[MetaprogDotNet] Kevin Hazzard, Jason Bock: Metaprogramming in .NET. Manning.<br />

2013.<br />

[MultiparadigmenProg] Martin Grabmüller: Multiparadigmen-<br />

Programmiersprachen. 2003-15 in Forschungsberichte Fakultät IV - Elektrotechnik<br />

und Informatik. Technische Universität Berlin. 2003.<br />

[PractAdvantDecl] John W. Lloyd: Practical Advantages of Declarative Programming.<br />

Joint Conference on Declarative Programming, GULP-PRODE.<br />

1994.<br />

[RealWorldFP] Thomas Petricek, John Skeet: Real-world functional programming:<br />

with examples in F# and C#. Manning. 2010.<br />

[Scala] Martin Odersky and Matthias Zenger: Independently Extensible Solutions<br />

to the Expression Problem. EPFL Technical Report IC/2004/33.<br />

École Polytechnique Fédérale de Lausanne, Switzerland. 2004. URL:<br />

http://lampwww.epfl.ch/~odersky/papers/ExpressionProblem.html<br />

(abgerufen am 27.07.2013).<br />

[SICP] Hal Abelson, Jerry Sussman, Julie Sussman: Structure and<br />

Interpretation of Computer Programs. MIT Press. 1984. URL:<br />

http://sicpebook.wordpress.<strong>com</strong>/2011/05/28/new-electronic-sicp (abgerufen<br />

am 21.07.2013).<br />

68


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

[WhyFPMatters] David Turner: Why Functional Programming Matters. Research<br />

Topics in Functional Programming, ed. D. Turner(S. 17–42). Addison-Wesley.<br />

1990.<br />

69


7<br />

Anhang<br />

7.1 Listing 1: Miss Grant’s Controller in F#<br />

type condition = DoorClosed | DrawerOpened | LightOn<br />

| DoorOpened | PanelClosed<br />

and actions = UnlockPanel | LockPanel | LockDoor | UnlockDoor<br />

5 and codes = D1CL | D2OP | L1ON | D1OP | PNCL<br />

| PNUL | PNLK | D1LK | D1UL<br />

and stateName = Idle | Active | WaitingForLight<br />

| WaitingForDrawer | UnlockedPanel<br />

and state = {<br />

10 name: stateName;<br />

actions: actions list;<br />

transitions: (condition * stateName) list<br />

}<br />

and machine = {<br />

15 events : (condition * codes) list<br />

resetEvents: condition list<br />

<strong>com</strong>mands : (actions * codes) list<br />

states : state list<br />

}<br />

20<br />

let inline (=>) a b = (a, b)<br />

let inline (:=) name (actions, transitions) =<br />

{ name = name; actions = actions; transitions = transitions }<br />

25 let machine = {<br />

events = [<br />

DoorClosed => D1CL<br />

DrawerOpened => D2OP<br />

LightOn => L1ON<br />

30 DoorOpened => D1OP<br />

PanelClosed => PNCL<br />

];<br />

resetEvents = [ DoorOpened ];<br />

<strong>com</strong>mands = [<br />

35 UnlockPanel => PNUL<br />

70


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

LockPanel => PNLK<br />

LockDoor => D1LK<br />

UnlockDoor => D1UL<br />

];<br />

40 states = [<br />

Idle := [UnlockDoor; LockPanel]<br />

=> [DoorClosed => Active]<br />

Active := []<br />

=> [DrawerOpened => WaitingForLight;<br />

45 LightOn => WaitingForDrawer]<br />

WaitingForLight := []<br />

=> [LightOn => UnlockedPanel]<br />

WaitingForDrawer := []<br />

=> [DrawerOpened => UnlockedPanel]<br />

50 UnlockedPanel := [UnlockPanel; LockDoor]<br />

=> [PanelClosed => Idle]<br />

]<br />

}<br />

55 open System<br />

open System.Text.RegularExpressions<br />

open Microsoft.FSharp.Reflection<br />

let run { events = events; resetEvents = resetEvents;<br />

<strong>com</strong>mands = <strong>com</strong>mands; states = states } =<br />

60 let assoc key (k, value) = if k = key then Some value else None<br />

let listAssoc key = List.pick(assoc key)<br />

let codeForEvent cond =<br />

events |> listAssoc cond<br />

let codeForAction action =<br />

65 <strong>com</strong>mands |> listAssoc action<br />

let start = states.Head<br />

let splitOnCamelCase(word: string) =<br />

String.concat ”” [<br />

for i in 1..word.Length-1 do<br />

70 let a, b = word.[i-1], word.[i]<br />

yield string a<br />

if Char.IsLower a && Char.IsUpper b then yield ” ”<br />

yield string word.[word.Length-1]<br />

]<br />

75 let print x = splitOnCamelCase(sprintf ”%A” x)<br />

let printList select list =<br />

String.concat ”, ”<br />

(Seq.map (fun x -> print (select x)) list)<br />

let cases =<br />

80 let ctor = FSharpValue.PreComputeUnionConstructor<br />

FSharpType.GetUnionCases(typeof)<br />

|> Array.map(fun x -> (x.Name.ToUpper(),<br />

ctor x [||]:?> condition))<br />

let readCondition() =<br />

85 let input = Console.ReadLine().Replace(” ”, ””).ToUpper()<br />

cases |> Array.tryPick(assoc input)<br />

let rec repl(state: state)(input: condition) =<br />

printfn ”Received %s (%s)...”<br />

(print input) (print(codeForEvent input))<br />

90 match List.tryPick(assoc input) state.transitions with<br />

| Some next -><br />

71


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

state.actions |> List.iter(fun x -><br />

printfn ”Doing %s (%s)...”<br />

(print x) (print(codeForAction x)))<br />

95 states |> List.find(fun x -> x.name = next) |> startRepl<br />

| None -><br />

if resetEvents |> List.exists((=)input) then<br />

startRepl start<br />

else<br />

100 startRepl state<br />

and startRepl state =<br />

printfn ”You’re now in state %s.” (print state.name)<br />

printfn ”Transitions: %s” (printList fst state.transitions)<br />

let rec findSome() =<br />

105 printf ”> ”<br />

match readCondition() with<br />

| Some cond -> repl state cond<br />

| None -> findSome()<br />

findSome()<br />

110 printfn ”Reset transitions: %s” (printList id resetEvents)<br />

startRepl start<br />

115<br />

[]<br />

let main _ = run machine<br />

// Beispiel:<br />

// Reset transitions: Door Opened<br />

// You’re now in state Idle.<br />

// Transitions: Door Closed<br />

120 // > Door Closed<br />

// Received Door Closed (D1CL)...<br />

// Doing Unlock Door (D1UL)...<br />

// Doing Lock Panel (PNLK)...<br />

// You’re now in state Active.<br />

125 // Transitions: Drawer Opened, Light On<br />

// > Light On<br />

// Received Light On (L1ON)...<br />

// You’re now in state Waiting For Drawer.<br />

// Transitions: Drawer Opened<br />

130 // > Drawer Opened<br />

// Received Drawer Opened (D2OP)...<br />

// You’re now in state Unlocked Panel.<br />

// Transitions: Panel Closed<br />

// > Panel Closed<br />

135 // Received Panel Closed (PNCL)...<br />

// Doing Unlock Panel (PNUL)...<br />

// Doing Lock Door (D1LK)...<br />

// You’re now in state Idle.<br />

// Transitions: Door Closed<br />

140 // > _<br />

7.2 Listing 2: XML-Traversierung mit Active Patterns<br />

in F#<br />

72


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

4<br />

#r ”System.Xml.Linq”<br />

open System.Xml.Linq<br />

let (|Node|_|)(name: string)(xObj: XObject) =<br />

match xObj with<br />

| :? XElement as element<br />

when element.Name.LocalName = name -><br />

9 Some(element.Nodes())<br />

| _ -> None<br />

let (|Text|_|)(xObj: XObject) =<br />

match xObj with<br />

14 | :? XElement -> None<br />

| _ -> Some(xObj.ToString())<br />

let (|Attribute|_|)(name: string)(xObj: XObject) =<br />

match xObj with<br />

19 | :? XElement as element -><br />

match element.Attribute(XName.Get(name)) with<br />

| null -> None<br />

| x -> Some(x.Value)<br />

| _ -> None<br />

24<br />

let rec traverseAll = Seq.iter traverseNode<br />

and traverseNode = function<br />

| Text text -> printfn ” %s” (text.Trim())<br />

| Node ”Matches” children -> traverseAll children<br />

29 | Node ”Match” children & Attribute ”Winner” winner<br />

& Attribute ”Loser” loser & Attribute”Score” score -><br />

printfn ”%s won against %s with score %s” winner loser score<br />

traverseAll children<br />

34 let sampleXml = @”<br />

<br />

Description of the first match...<br />

<br />

<br />

39 Description of the second match...<br />

<br />

”<br />

traverseNode(XElement.Parse(sampleXml))<br />

7.3 Listing 3: Generische Variante von Counting Sort<br />

in F#<br />

1<br />

> let inline SortInPlaceGeneric numbers =<br />

let maximum = Array.max numbers<br />

let occurences = Array.zeroCreate(int maximum + 1)<br />

for num in numbers do<br />

6 occurences.[int num]


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

while num ^a)<br />

21 and ^b : (static member get_One : -> ^b)<br />

// Beispielnutzung mit Ganzzahltypen int, int64, sbyte (signed byte),<br />

byte, int16 (suffix s für short), BigInteger<br />

> SortInPlaceGeneric [| 1; 2; 3; 14; 120; 0; 2; 4 |];;<br />

val it : int [] = [|0; 1; 2; 2; 3; 4; 14; 120|]<br />

26 > SortInPlaceGeneric [| 1L; 2L; 3L; 14L; 120L; 0L; 2L; 4L |];;<br />

val it : int64 [] = [|0L; 1L; 2L; 2L; 3L; 4L; 14L; 120L|]<br />

> SortInPlaceGeneric [| 1y; 2y; 3y; 14y; 120y; 0y; 2y; 4y |];;<br />

val it : sbyte [] = [|0y; 1y; 2y; 2y; 3y; 4y; 14y; 120y|]<br />

> SortInPlaceGeneric [| 1uy; 2uy; 3uy; 14uy; 120uy; 0uy; 2uy; 4uy |];;<br />

31 val it : byte [] = [|0uy; 1uy; 2uy; 2uy; 3uy; 4uy; 14uy; 120uy|]<br />

> SortInPlaceGeneric [| 1s; 2s; 3s; 14s; 120s; 0s; 2s; 4s |];;<br />

val it : int16 [] = [|0s; 1s; 2s; 2s; 3s; 4s; 14s; 120s|]<br />

> SortInPlaceGeneric [| 1I; 2I; 3I; 14I; 120I; 0I; 2I; 4I |];;<br />

val it : System.Numerics.BigInteger [] =<br />

36 [|0I; 1I; 2I; 2I; 3I; 4I; 14I; 120I|]<br />

7.4 Listing 4: Prolog-ähnliche Quotation in F#<br />

> open Microsoft.FSharp.Core.Operators.Unchecked<br />

3 let predicate = ignore // ignoriert den Parameter (f x = ())<br />

let value = defaultof // liefert null/false/0/0.0, je nach<br />

Typ<br />

// ^- Für das Typsystem, die Funktionen sollen nicht ausgeführt<br />

werden.<br />

let prolog x = x // Hier käme die Metaprogrammierung<br />

// (Verarbeitung der Quotation x)<br />

8<br />

let memb = predicate<br />

let E = value<br />

let __ = value<br />

let R = value<br />

13 let ( prolog


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

NewUnionCase (Cons, Call (None, E, []),<br />

23 Call (None, __, [])))),<br />

Call (None, op_LessMinusMinus,<br />

[Application (Call (None, memb, []),<br />

NewTuple (Call (None, E, []),<br />

NewUnionCase (Cons,<br />

28 Call (None, __, []),<br />

Call (None, R, [])))),<br />

Application (Call (None, memb, []),<br />

NewTuple (Call (None, E, []), Call (None, R, [])))]))<br />

7.5 Listing 5: Evaluator - algebraische Datentypen<br />

/ Pattern matching<br />

2 /// Stellt einen Ausdruck oder ein Ergebnis einer simplen<br />

Programmiersprache dar.<br />

type Code =<br />

/// Number(Konstante Ganzzahl)<br />

| Number of int<br />

/// Bool(Konstanter Wahrheitswert)<br />

7 | Bool of bool<br />

/// Var(Name)<br />

| Var of string<br />

/// Binding(Name, Wert, Ausdruck)<br />

| Binding of string * Code * Code<br />

12 /// Conditional(Bedingung, Folge, Alternative)<br />

| Conditional of Code * Code * Code<br />

/// Application(Funktion, Argument-Liste)<br />

| Application of Code * Code list<br />

/// Lambda(Parameter-Namen, Ausdruck)<br />

17 | Lambda of string list * Code<br />

/// Closure = Lambda + Variablenbindungen<br />

| Closure of Map * string list * Code<br />

/// BuiltInFunc(F#-Funktion) (die Funktion erhält ausgewertete<br />

Argumente)<br />

| BuiltInFunc of (Code list -> Code)<br />

22<br />

module Evaluator =<br />

let rec eval env expr =<br />

match expr with<br />

| Number _ | Bool _ | BuiltInFunc _ | Closure _ -> expr<br />

27 | Lambda(param, body) -> Closure(env, param, body)<br />

| Var name -> Map.find name env<br />

| Binding(name, value, body) -><br />

eval (Map.add name (eval env value) env) body<br />

| Conditional(condition, thenExpr, elseExpr) -><br />

32 match eval env condition with<br />

| Bool false -> eval env elseExpr<br />

| _ -> eval env thenExpr<br />

| Application(func, args) -><br />

match eval env func with<br />

37 | Closure(extendedEnv, names, body) -><br />

let addToMap map (key, value) = Map.add key value map<br />

75


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

let newEnv = List.fold addToMap extendedEnv (List.zip<br />

names (List.map (eval env) args))<br />

eval newEnv body<br />

| BuiltInFunc(func) -><br />

42 func (List.map (eval env) args)<br />

| other -> failwithf ”Only closures and built-in functions<br />

can be applied, found: %A” other<br />

let Evaluate(expr) = eval Map.empty expr<br />

47 module BuiltInFuncs =<br />

let toNumber = function<br />

| Number value -> value<br />

| other -> failwithf ”Integer expected, found: %A” other<br />

52 let equality = function<br />

| Number value1, Number value2 -> value1 = value2<br />

| Bool value1, Bool value2 -> value1 = value2<br />

| _ -> false<br />

57 let plus() = BuiltInFunc(fun args -><br />

Number(List.sumBy toNumber args))<br />

// let (|>) x f = f(x) // Pipelining [FSharp, vgl. S. 40-41]<br />

let times() = BuiltInFunc(fun args -><br />

62 args |> List.map toNumber |> List.fold (*) 1 |> Number)<br />

67<br />

// geschachtelte Schreibweise ohne (|>)<br />

let times’() = BuiltInFunc(fun args -><br />

Number(List.fold (*) 1 (List.map toNumber args)))<br />

let eq() = BuiltInFunc(fun args -><br />

args |> Seq.pairwise |> Seq.forall equality |> Bool)<br />

let lt() = BuiltInFunc(fun args -><br />

72 args |> Seq.map toNumber |> Seq.pairwise<br />

|> Seq.forall(fun(a, b) -> a < b) |> Bool)<br />

77<br />

open Evaluator<br />

open BuiltInFuncs<br />

#if INTERACTIVE<br />

#I ”C:\Program Files (x86)\Microsoft Visual Studio<br />

11.0\Common7\IDE\PublicAssemblies”<br />

#r ”Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll”<br />

#endif<br />

82 open Microsoft.VisualStudio.TestTools.UnitTesting<br />

open System<br />

open System.Reflection<br />

[]<br />

type FunctionalTests() =<br />

87 let (-->) expr result =<br />

let raw = function<br />

| Number x -> box x<br />

| Bool b -> box b<br />

| _ -> obj()<br />

76


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

92 let evaluated = Evaluate(expr)<br />

Assert.AreEqual(raw evaluated, raw result,<br />

sprintf ”expected %A actual %A (expr: %A)”<br />

result evaluated expr)<br />

97 static member RunInConsole() =<br />

let tester = FunctionalTests()<br />

let tests =<br />

typeof.GetMethods()<br />

|> Array.filter(fun test -><br />

102 Attribute.IsDefined(test,<br />

typeof))<br />

let mutable passed = 0<br />

for test in tests do<br />

try<br />

test.Invoke(tester, null) |> ignore<br />

107 passed<br />

try raise(e.InnerException)<br />

with :? AssertFailedException as e -><br />

112 printfn ”%s\n%s\n”<br />

test.Name (e.Message.Replace(”. ”, ”\n”))<br />

printfn ”\n%d von %d Tests bestanden.” passed tests.Length<br />

[]<br />

117 member this.FunctionalLet() =<br />

let sample = // (let ([x 20]) (+ x x 4))<br />

Binding(”x”, Number 20,<br />

Application(plus(), [Var ”x”; Var ”x”; Number 4]))<br />

122 sample --> Number 44<br />

[]<br />

member this.FunctionalSquare() =<br />

let square = // (lambda (x) (* x x))<br />

127 Lambda([”x”], Application(times(), [Var ”x”; Var ”x”]))<br />

for i = -100 to 100 do<br />

Application(square, [Number i]) --> Number(i * i)<br />

132 []<br />

member this.FunctionalMinimum() =<br />

let minimum = // (lambda (a b) (if (< a b) a b))<br />

Lambda([”a”; ”b”],<br />

Conditional(Application(lt(), [Var ”a”; Var ”b”]),<br />

137 Var ”a”, Var ”b”))<br />

142<br />

for i = -10 to 10 do<br />

for k = -10 to 10 do<br />

Application(minimum, [Number i; Number k]) --><br />

Number(min i k)<br />

[]<br />

member this.FunctionalFactorial() =<br />

// ”Rekursiver” Lambda-Ausdruck:<br />

77


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

// (lambda (cont x) (if (= x 0) 1 (* x (cont cont (- x 1)))))<br />

147 let factorial =<br />

Lambda([”cont”; ”x”],<br />

Conditional(<br />

Application(eq(), [Var ”x”; Number 0]),<br />

Number 1,<br />

152 Application(times(),<br />

[Var ”x”; Application(Var ”cont”,<br />

[Var ”cont”;<br />

Application(plus(),<br />

[Var ”x”; Number<br />

-1])])])))<br />

for i = 0 to 10 do<br />

157 Application(factorial, [factorial; Number i])<br />

--> Number(Seq.fold (*) 1 { 1..i })<br />

[]<br />

member this.FunctionalClosure() =<br />

162 let cons = // (lambda (hd tl) (lambda (takeHead) (if takeHead<br />

hd tl)))<br />

Lambda([”hd”; ”tl”],<br />

Lambda([”takeHead”],<br />

Conditional(<br />

Var ”takeHead”,<br />

167 Var ”hd”,<br />

Var ”tl”)))<br />

let empty = Bool false // #f<br />

172 let head = // (lambda (consCell) (consCell true))<br />

Lambda([”xs”], Application(Var ”xs”, [Bool true]))<br />

177<br />

let tail = // (lambda (consCell) (consCell false))<br />

Lambda([”xs”], Application(Var ”xs”, [Bool false]))<br />

let isEmpty = // (lambda (consCell) (if consCell #f #t))<br />

Lambda([”xs”], Conditional(Var ”xs”, Bool false, Bool<br />

true))<br />

182 let oneTwo = // (cons 1 (cons 2 empty))<br />

Application(cons, [Number 1;<br />

Application(cons, [Number 2; empty])])<br />

Application(head, [oneTwo]) --> Number 1<br />

187 Application(head, [Application(tail, [oneTwo])]) --> Number 2<br />

Application(tail, [Application(tail, [oneTwo])]) --> empty<br />

Application(isEmpty, [empty]) --> Bool true<br />

Application(isEmpty, [oneTwo]) --> Bool false<br />

192 Application(isEmpty, [Application(tail, [oneTwo])])<br />

--> Bool false<br />

Application(isEmpty, [Application(tail, [Application(tail,<br />

[oneTwo])])])<br />

--> Bool true<br />

78


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

197 #if INTERACTIVE<br />

FunctionalTests.RunInConsole()<br />

#endif<br />

7.6 Listing 6: Evaluator - datenorientiert mit Typtests<br />

using System;<br />

using System.Collections.Generic;<br />

5 namespace Typtests<br />

{<br />

abstract public class Code { }<br />

public class Number : Code { public int Value; }<br />

public class Bool : Code { public bool Value; }<br />

10 public class Var : Code { public string Name; }<br />

public class Binding : Code<br />

{ public string Identifier; public Code Value, Body; }<br />

public class Conditional : Code<br />

{ public Code Condition, Then, Else; }<br />

15 public class Application : Code<br />

{ public Code Function; public Code[] Args; }<br />

public class Lambda : Code<br />

{ public string[] ParameterIdentifiers; public Code Body; }<br />

public class Closure : Lambda<br />

20 { public Dictionary Env; }<br />

abstract public class BuiltInFunc : Code<br />

{ public abstract Code Apply(Code[] args); }<br />

public class Plus : BuiltInFunc<br />

25 {<br />

public override Code Apply(Code[] args)<br />

{<br />

int sum = 0;<br />

foreach (var item in args)<br />

30 { sum += ((Number)item).Value; }<br />

return new Number { Value = sum };<br />

}<br />

}<br />

public class Equals : BuiltInFunc<br />

35 {<br />

public override Code Apply(Code[] args)<br />

{<br />

for (int i = 0; i < args.Length - 1; i++)<br />

{<br />

40 var x = args[i];<br />

var y = args[i + 1];<br />

if (x is Number && ((Number)x).Value !=<br />

((Number)y).Value<br />

|| x is Bool && ((Bool)x).Value != ((Bool)y).Value)<br />

{ return new Bool { Value = false }; }<br />

45 }<br />

return new Bool { Value = true };<br />

79


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

}<br />

}<br />

public class LessThan : BuiltInFunc<br />

50 {<br />

public override Code Apply(Code[] args)<br />

{<br />

for (int i = 0; i < args.Length - 1; i++)<br />

{<br />

55 var x = args[i];<br />

var y = args[i + 1];<br />

if (((Number)x).Value >= ((Number)y).Value)<br />

{ return new Bool { Value = false }; }<br />

}<br />

60 return new Bool { Value = true };<br />

}<br />

}<br />

public class Evaluator<br />

{<br />

65 public Code Evaluate(Code expr)<br />

{ return Evaluate(new Dictionary(), expr); }<br />

public Code Evaluate(Dictionary env, Code expr)<br />

{<br />

70 if (expr is Number || expr is Bool || expr is BuiltInFunc<br />

|| expr is Closure)<br />

{ return expr; }<br />

var l = expr as Lambda;<br />

if (l != null)<br />

75 {<br />

return new Closure<br />

{<br />

Env = new Dictionary(env),<br />

Body = l.Body,<br />

80 ParameterIdentifiers = l.ParameterIdentifiers<br />

};<br />

}<br />

Var v = expr as Var;<br />

85 if (v != null)<br />

{ return env[v.Name]; }<br />

var binding = expr as Binding;<br />

if (binding != null)<br />

90 {<br />

Code old = null;<br />

if (env.ContainsKey(binding.Identifier))<br />

{ old = env[binding.Identifier]; }<br />

env[binding.Identifier] = Evaluate(env, binding.Value);<br />

95 var result = Evaluate(env, binding.Body);<br />

if (old != null) { env[binding.Identifier] = old; }<br />

return result;<br />

}<br />

100 var conditional = expr as Conditional;<br />

if (conditional != null)<br />

80


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

{<br />

var test = Evaluate(env, conditional.Condition) as<br />

Bool;<br />

if (test != null && !test.Value)<br />

105 { return Evaluate(env, conditional.Else); }<br />

else<br />

{ return Evaluate(env, conditional.Then); }<br />

}<br />

110 var app = expr as Application;<br />

if (app != null)<br />

{<br />

var func = Evaluate(env, app.Function);<br />

var f = func as Closure;<br />

115 if (f != null)<br />

{<br />

var envToExtend = f.Env;<br />

var len = f.ParameterIdentifiers.Length;<br />

var oldValues = new Code[len];<br />

120 for (int i = 0; i < len; i++)<br />

{<br />

var name = f.ParameterIdentifiers[i];<br />

if (envToExtend.ContainsKey(name))<br />

{ oldValues[i] = envToExtend[name]; }<br />

125 envToExtend[name] = Evaluate(env, app.Args[i]);<br />

// Werte die Argumente mit env aus, den<br />

} // Körper der Closure mit erweitertem env.<br />

var result = Evaluate(envToExtend, f.Body);<br />

for (int i = 0; i < len; i++)<br />

130 {<br />

var name = f.ParameterIdentifiers[i];<br />

if (oldValues[i] != null)<br />

{ envToExtend[name] = oldValues[i]; }<br />

}<br />

135 return result;<br />

}<br />

var prim = func as BuiltInFunc;<br />

if (prim != null)<br />

{<br />

140 var evaluatedArgs = new Code[app.Args.Length];<br />

for (int i = 0; i < evaluatedArgs.Length; i++)<br />

{ evaluatedArgs[i] = Evaluate(env, app.Args[i]); }<br />

return prim.Apply(evaluatedArgs);<br />

}<br />

145 throw new Exception(<br />

”Only closures and built-in functions can be<br />

applied. Given: ”<br />

+ expr);<br />

}<br />

throw new Exception(”Unknown subclass of Code: ” + expr);<br />

150 }<br />

}<br />

}<br />

81


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

7.7 Listing 7: Evaluator - datenorientiert mit switch<br />

über Type code<br />

2 using System;<br />

using System.Collections.Generic;<br />

namespace TypeCodes<br />

{<br />

7 public enum CodeType<br />

{ Number, Bool, Var, Binding,<br />

Conditional, Application, Lambda, Closure, BuiltInFunc }<br />

public abstract class Code<br />

12 { public abstract CodeType Type { get; } }<br />

public class Number : Code<br />

{ public int Value;<br />

public override CodeType Type<br />

{ get { return CodeType.Number; } } }<br />

17 public class Bool : Code<br />

{ public bool Value;<br />

public override CodeType Type<br />

{ get { return CodeType.Bool; } } }<br />

public class Var : Code<br />

22 { public string Name;<br />

public override CodeType Type<br />

{ get { return CodeType.Var; } } }<br />

public class Binding : Code<br />

27 {<br />

public string Identifier; public Code Value, Body;<br />

public override CodeType Type<br />

{ get { return CodeType.Binding; } }<br />

}<br />

32 public class Conditional : Code<br />

{<br />

public Code Condition, Then, Else;<br />

public override CodeType Type<br />

{ get { return CodeType.Conditional; } }<br />

37 }<br />

public class Application : Code<br />

{<br />

public Code Function; public Code[] Args;<br />

public override CodeType Type<br />

42 { get { return CodeType.Application; } }<br />

}<br />

public class Lambda : Code<br />

{<br />

public string[] ParameterIdentifiers; public Code Body;<br />

47 public override CodeType Type<br />

{ get { return CodeType.Lambda; } }<br />

}<br />

public class Closure : Lambda<br />

{<br />

82


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

52 public Dictionary Env;<br />

public override CodeType Type<br />

{ get { return CodeType.Closure; } }<br />

}<br />

public class BuiltInFunc : Code<br />

57 {<br />

public virtual Code Apply(params Code[] args)<br />

{ throw new Exception(); }<br />

public override CodeType Type<br />

{ get { return CodeType.BuiltInFunc; } }<br />

62 }<br />

public class Plus : BuiltInFunc<br />

{<br />

public override Code Apply(params Code[] args)<br />

67 {<br />

int sum = 0;<br />

foreach (var item in args)<br />

{ sum += ((Number)item).Value; }<br />

return new Number { Value = sum };<br />

72 }<br />

}<br />

public class Equals : BuiltInFunc<br />

{<br />

77 public override Code Apply(params Code[] args)<br />

{<br />

for (int i = 0; i < args.Length - 1; i++)<br />

{<br />

var x = args[i];<br />

82 var y = args[i + 1];<br />

if (x is Number && ((Number)x).Value !=<br />

((Number)y).Value<br />

|| x is Bool && ((Bool)x).Value != ((Bool)y).Value)<br />

{ return new Bool { Value = false }; }<br />

}<br />

87 return new Bool { Value = true };<br />

}<br />

}<br />

public class LessThan : BuiltInFunc<br />

92 {<br />

public override Code Apply(params Code[] args)<br />

{<br />

for (int i = 0; i < args.Length - 1; i++)<br />

{<br />

97 var x = args[i];<br />

var y = args[i + 1];<br />

if (x is Number<br />

&& ((Number)x).Value >= ((Number)y).Value)<br />

{ return new Bool { Value = false }; }<br />

102 }<br />

return new Bool { Value = true };<br />

}<br />

}<br />

83


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

107 public class Evaluator<br />

{<br />

public Code Evaluate(Code expr)<br />

{ return Evaluate(new Dictionary(), expr); }<br />

112 public Code Evaluate(Dictionary env, Code expr)<br />

{<br />

switch (expr.Type)<br />

{<br />

case CodeType.Number:<br />

117 case CodeType.Bool:<br />

case CodeType.BuiltInFunc:<br />

case CodeType.Closure:<br />

return expr;<br />

case CodeType.Lambda:<br />

122 var l = (Lambda)expr;<br />

return new Closure<br />

{<br />

Env = new Dictionary(env),<br />

Body = l.Body,<br />

127 ParameterIdentifiers = l.ParameterIdentifiers<br />

};<br />

case CodeType.Var:<br />

var v = (Var)expr;<br />

return env[v.Name];<br />

132 case CodeType.Binding:<br />

var bind = (Binding)expr;<br />

Code old = null;<br />

if (env.ContainsKey(bind.Identifier))<br />

{ old = env[bind.Identifier]; }<br />

137 env[bind.Identifier] = Evaluate(env, bind.Value);<br />

var result = Evaluate(env, bind.Body);<br />

if (old != null) { env[bind.Identifier] = old; }<br />

return result;<br />

case CodeType.Conditional:<br />

142 var conditional = (Conditional)expr;<br />

var test = Evaluate(env, conditional.Condition) as<br />

Bool;<br />

if (test != null && !test.Value)<br />

{ return Evaluate(env, conditional.Else); }<br />

else<br />

147 { return Evaluate(env, conditional.Then); }<br />

case CodeType.Application:<br />

var app = (Application)expr;<br />

var func = Evaluate(env, app.Function);<br />

switch (func.Type)<br />

152 {<br />

case CodeType.Closure:<br />

var c = (Closure)func;<br />

var envToExtend = c.Env;<br />

var len = c.ParameterIdentifiers.Length;<br />

157 var oldValues = new Code[len];<br />

for (int i = 0; i < len; i++)<br />

{<br />

var name = c.ParameterIdentifiers[i];<br />

if (envToExtend.ContainsKey(name))<br />

84


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

162 { oldValues[i] = envToExtend[name]; }<br />

envToExtend[name] =<br />

Evaluate(env, app.Args[i]);<br />

// Werte die Argumente mit env aus, den<br />

} // Körper der Closure mit erweitertem<br />

env.<br />

167 var functionResult =<br />

Evaluate(envToExtend, c.Body);<br />

for (int i = 0; i < len; i++)<br />

{<br />

var name = c.ParameterIdentifiers[i];<br />

172 if (oldValues[i] != null)<br />

{ envToExtend[name] = oldValues[i]; }<br />

}<br />

return functionResult;<br />

case CodeType.BuiltInFunc:<br />

177 var b = (BuiltInFunc)func;<br />

var length = app.Args.Length;<br />

var evaluatedArgs = new Code[length];<br />

for (int i = 0; i < length; i++)<br />

{ evaluatedArgs[i] =<br />

182 Evaluate(env, app.Args[i]); }<br />

return b.Apply(evaluatedArgs);<br />

default:<br />

throw new Exception(”Cannot be applied ” +<br />

func);<br />

}<br />

187 }<br />

throw new Exception(”Wrong CodeType: ” + expr.Type + ” (of<br />

object ” + expr + ”)”);<br />

}<br />

}<br />

}<br />

7.8 Listing 8: Evaluator - verhaltensorientiert mit<br />

Polymorphie<br />

using System;<br />

3 using System.Collections.Generic;<br />

namespace Polymorphie<br />

{<br />

abstract public class Code<br />

8 {<br />

public virtual Code Evaluate(Dictionary env)<br />

{ return this; }<br />

public virtual Code Apply(Dictionary env,<br />

Code[] args)<br />

13 { throw new Exception(”Cannot apply ” + this); }<br />

}<br />

public class Number : Code { public int Value; }<br />

85


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

18 public class Bool : Code { public bool Value; }<br />

public class Var : Code<br />

{<br />

public string Name;<br />

23 public override Code Evaluate(Dictionary env)<br />

{ return env[Name]; }<br />

}<br />

public class Binding : Code<br />

28 {<br />

public string Identifier;<br />

public Code Value, Body;<br />

public override Code Evaluate(Dictionary env)<br />

{<br />

33 Code old = null;<br />

if (env.ContainsKey(Identifier)) { old = env[Identifier]; }<br />

env[Identifier] = Value.Evaluate(env);<br />

var result = Body.Evaluate(env);<br />

38 if (old != null) { env[Identifier] = old; }<br />

return result;<br />

}<br />

}<br />

43 public class Conditional : Code<br />

{<br />

public Code Condition, Then, Else;<br />

public override Code Evaluate(Dictionary env)<br />

{<br />

48 var result = Condition.Evaluate(env) as Bool;<br />

if (result != null && !result.Value)<br />

{ return Else.Evaluate(env); }<br />

else<br />

{ return Then.Evaluate(env); }<br />

53 }<br />

}<br />

public class Application : Code<br />

{<br />

58 public Code Function;<br />

public Code[] Args;<br />

public override Code Evaluate(Dictionary env)<br />

{<br />

var func = Function.Evaluate(env);<br />

63 return func.Apply(env, Args);<br />

}<br />

}<br />

public class Lambda : Code<br />

68 {<br />

public string[] ParameterIdentifiers;<br />

public Code Body;<br />

public override Code Evaluate(Dictionary env)<br />

{<br />

73 return new Closure<br />

86


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

{<br />

78 };<br />

}<br />

}<br />

Env = new Dictionary(env),<br />

ParameterIdentifiers = ParameterIdentifiers,<br />

Body = Body<br />

public class Closure : Lambda<br />

83 {<br />

public Dictionary Env;<br />

public override Code Evaluate(Dictionary env)<br />

{ return this; }<br />

88 public override Code Apply(Dictionary env,<br />

Code[] args)<br />

{<br />

var len = ParameterIdentifiers.Length;<br />

var oldValues = new Code[len];<br />

for (int i = 0; i < len; i++)<br />

93 {<br />

var name = ParameterIdentifiers[i];<br />

if (Env.ContainsKey(name))<br />

{ oldValues[i] = Env[name]; }<br />

Env[name] = args[i].Evaluate(env);<br />

98 // Werte die Argumente mit env aus, den<br />

} // Körper der Closure mit erweitertem env.<br />

var result = Body.Evaluate(Env);<br />

for (int i = 0; i < len; i++)<br />

{<br />

103 var name = ParameterIdentifiers[i];<br />

if (oldValues[i] != null) { Env[name] = oldValues[i]; }<br />

}<br />

return result;<br />

}<br />

108 }<br />

abstract public class BuiltInFunc : Code<br />

{<br />

public override Code Apply(Dictionary env,<br />

Code[] args)<br />

113 {<br />

var len = args.Length;<br />

var evaluatedArgs = new Code[len];<br />

for (int i = 0; i < len; i++)<br />

{ evaluatedArgs[i] = args[i].Evaluate(env); }<br />

118 return Apply(evaluatedArgs);<br />

}<br />

protected abstract Code Apply(Code[] evaluatedArgs);<br />

}<br />

123 public class Plus : BuiltInFunc<br />

{<br />

protected override Code Apply(Code[] args)<br />

{<br />

int sum = 0;<br />

87


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

128 foreach (var item in args)<br />

{ sum += ((Number)item).Value; }<br />

return new Number { Value = sum };<br />

}<br />

}<br />

133<br />

public class Equals : BuiltInFunc<br />

{<br />

protected override Code Apply(Code[] args)<br />

{<br />

138 for (int i = 0; i < args.Length - 1; i++)<br />

{<br />

var x = args[i];<br />

var y = args[i + 1];<br />

if (x is Number && ((Number)x).Value !=<br />

((Number)y).Value<br />

143 || x is Bool && ((Bool)x).Value != ((Bool)y).Value)<br />

{ return new Bool { Value = false }; }<br />

}<br />

return new Bool { Value = true };<br />

}<br />

148 }<br />

public class LessThan : BuiltInFunc<br />

{<br />

protected override Code Apply(Code[] args)<br />

153 {<br />

for (int i = 0; i < args.Length - 1; i++)<br />

{<br />

var x = args[i];<br />

var y = args[i + 1];<br />

158 if (((Number)x).Value >= ((Number)y).Value)<br />

{ return new Bool { Value = false }; }<br />

}<br />

return new Bool { Value = true };<br />

}<br />

163 }<br />

public class Evaluator<br />

{<br />

public Code Evaluate(Code expr)<br />

168 { return expr.Evaluate(new Dictionary()); }<br />

}<br />

}<br />

7.9 Listing 9: Evaluator - verhaltensorientiert mit<br />

Visitor pattern<br />

4<br />

using System;<br />

using System.Collections.Generic;<br />

namespace Visitors<br />

{<br />

88


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

abstract public class Code<br />

{ public abstract Code Accept(Visitor v);}<br />

9 public class Number : Code<br />

{<br />

public int Value;<br />

public override Code Accept(Visitor v)<br />

{ return v.Visit(this); } // Visit(Number)<br />

14 }<br />

public class Bool : Code<br />

{<br />

public bool Value;<br />

public override Code Accept(Visitor v)<br />

19 { return v.Visit(this); } // Visit(Bool)<br />

}<br />

public class Var : Code<br />

{<br />

public string Name;<br />

24 public override Code Accept(Visitor v)<br />

{ return v.Visit(this); } // usw.<br />

}<br />

public class Binding : Code<br />

{<br />

29 public string Identifier; public Code Value, Body;<br />

public override Code Accept(Visitor v)<br />

{ return v.Visit(this); }<br />

}<br />

public class Conditional : Code<br />

34 {<br />

public Code Condition, Then, Else;<br />

public override Code Accept(Visitor v)<br />

{ return v.Visit(this); }<br />

}<br />

39 public class Application : Code<br />

{<br />

public Code Function; public Code[] Args;<br />

public override Code Accept(Visitor v)<br />

{ return v.Visit(this); }<br />

44 }<br />

public class Lambda : Code<br />

{<br />

public string[] ParameterIdentifiers; public Code Body;<br />

public override Code Accept(Visitor v)<br />

49 { return v.Visit(this); }<br />

}<br />

public class Closure : Lambda<br />

{<br />

public Dictionary Env;<br />

54 public override Code Accept(Visitor v)<br />

{ return v.Visit(this); }<br />

}<br />

abstract public class BuiltInFunc : Code<br />

{<br />

59 public override Code Accept(Visitor v)<br />

{ return v.Visit(this); }<br />

public abstract Code Apply(Code[] args);<br />

}<br />

89


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

64 public class LessThan : BuiltInFunc<br />

{<br />

public override Code Apply(Code[] args)<br />

{<br />

for (int i = 0; i < args.Length - 1; i++)<br />

69 {<br />

var x = args[i];<br />

var y = args[i + 1];<br />

if (((Number)x).Value >= ((Number)y).Value)<br />

{ return new Bool { Value = false }; }<br />

74 }<br />

return new Bool { Value = true };<br />

}<br />

}<br />

79 public class Plus : BuiltInFunc<br />

{<br />

public override Code Apply(params Code[] args)<br />

{<br />

int sum = 0;<br />

84 foreach (var item in args)<br />

{ sum += ((Number)item).Value; }<br />

return new Number { Value = sum };<br />

}<br />

}<br />

89<br />

public class Equals : BuiltInFunc<br />

{<br />

public override Code Apply(params Code[] args)<br />

{<br />

94 for (int i = 0; i < args.Length - 1; i++)<br />

{<br />

var x = args[i];<br />

var y = args[i + 1];<br />

if (x is Number && ((Number)x).Value !=<br />

((Number)y).Value<br />

99 || x is Bool && ((Bool)x).Value != ((Bool)y).Value)<br />

{ return new Bool { Value = false }; }<br />

}<br />

return new Bool { Value = true };<br />

}<br />

104 }<br />

abstract public class Visitor<br />

{<br />

public virtual Code Visit(Number expr)<br />

109 { return VisitDefault(expr); }<br />

public virtual Code Visit(Bool expr)<br />

{ return VisitDefault(expr); }<br />

public virtual Code Visit(Var expr)<br />

{ return VisitDefault(expr); }<br />

114 public virtual Code Visit(Binding expr)<br />

{ return VisitDefault(expr); }<br />

public virtual Code Visit(Conditional expr)<br />

{ return VisitDefault(expr); }<br />

90


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

public virtual Code Visit(Application expr)<br />

119 { return VisitDefault(expr); }<br />

public virtual Code Visit(Lambda expr)<br />

{ return VisitDefault(expr); }<br />

public virtual Code Visit(Closure expr)<br />

{ return VisitDefault(expr); }<br />

124 public virtual Code Visit(BuiltInFunc expr)<br />

{ return VisitDefault(expr); }<br />

protected abstract Code VisitDefault(Code expr);<br />

}<br />

129 public class EvaluationVisitor : Visitor<br />

{<br />

Dictionary env;<br />

public EvaluationVisitor()<br />

: this(new Dictionary()) { }<br />

134 public EvaluationVisitor(Dictionary env)<br />

{ this.env = env; }<br />

139<br />

protected override Code VisitDefault(Code expr)<br />

{ return expr; }<br />

public override Code Visit(Var expr)<br />

{ return env[expr.Name]; }<br />

public override Code Visit(Lambda expr)<br />

144 {<br />

return new Closure<br />

{<br />

Env = new Dictionary(env),<br />

Body = expr.Body,<br />

149 ParameterIdentifiers = expr.ParameterIdentifiers<br />

};<br />

}<br />

public override Code Visit(Binding expr)<br />

154 {<br />

Code old = null;<br />

if (env.ContainsKey(expr.Identifier))<br />

{ old = env[expr.Identifier]; }<br />

env[expr.Identifier] = expr.Value.Accept(this);<br />

159 var result = expr.Body.Accept(this);<br />

if (old != null) { env[expr.Identifier] = old; }<br />

return result;<br />

}<br />

164 public override Code Visit(Conditional expr)<br />

{<br />

var result = expr.Condition.Accept(this) as Bool;<br />

if (result != null && !result.Value)<br />

{ return expr.Else.Accept(this); }<br />

169 else<br />

{ return expr.Then.Accept(this); }<br />

}<br />

public override Code Visit(Application expr)<br />

91


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

174 {<br />

179 }<br />

}<br />

var evaluatedFunction = expr.Function.Accept(this);<br />

var appVisitor = new ApplicationVisitor(this, env, expr);<br />

return evaluatedFunction.Accept(appVisitor);<br />

public class ApplicationVisitor : Visitor<br />

{<br />

Visitor parentVisitor;<br />

184 Application app;<br />

Dictionary env;<br />

public ApplicationVisitor(Visitor parentVisitor,<br />

Dictionary env, Application app)<br />

189 {<br />

this.parentVisitor = parentVisitor;<br />

this.env = env;<br />

this.app = app;<br />

}<br />

194<br />

public override Code Visit(Closure expr)<br />

{<br />

var envToExtend = expr.Env;<br />

var len = expr.ParameterIdentifiers.Length;<br />

199 var oldValues = new Code[len];<br />

for (int i = 0; i < len; i++)<br />

{<br />

var name = expr.ParameterIdentifiers[i];<br />

if (envToExtend.ContainsKey(name))<br />

204 { oldValues[i] = envToExtend[name]; }<br />

envToExtend[name] = app.Args[i].Accept(parentVisitor);<br />

} // Argumente mit env auswerten, Körper mit neuem env.<br />

var result = expr.Body.Accept( // Dafür neuer Visitor:<br />

new EvaluationVisitor(envToExtend));<br />

209 for (int i = 0; i < len; i++)<br />

{<br />

var name = expr.ParameterIdentifiers[i];<br />

if (oldValues[i] != null)<br />

{ envToExtend[name] = oldValues[i]; }<br />

214 }<br />

return result;<br />

}<br />

public override Code Visit(BuiltInFunc expr)<br />

219 {<br />

var len = app.Args.Length;<br />

var args = new Code[len];<br />

for (int i = 0; i < len; i++)<br />

{ args[i] = app.Args[i].Accept(parentVisitor); }<br />

224 return expr.Apply(args);<br />

}<br />

229 }<br />

protected override Code VisitDefault(Code expr)<br />

{ throw new Exception(”Cannot apply ” + expr); }<br />

92


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

public class Evaluator<br />

{<br />

public Code Evaluate(Code expr)<br />

234 { return expr.Accept(new EvaluationVisitor()); }<br />

}<br />

}<br />

7.10 Listing 10: Test der Evaluator-Implementierungen<br />

in C#<br />

using System;<br />

3 using Microsoft.VisualStudio.TestTools.UnitTesting;<br />

// Stellt eine Sammlung von Tests dar, die eine<br />

Evaluator-Implementierung überprüfen.<br />

// <strong>Die</strong> Typparameter müssen den zu testenden Klassen entsprechen.<br />

// Der Evaluator kapselt die Strategie, indem die Evaluate-Methode auf<br />

den Ausdruck angewendet und das Ergebnis geprüft wird.<br />

8 // <strong>Die</strong> Klassen werden instanziiert und zu Test-Ausdrücken<br />

zusammengesetzt.<br />

// <strong>Die</strong> statischen Methoden stellen die Tests dar. In Unterklassen<br />

müssen sie aufgerufen werden.<br />

// Als Kommentar der einzelnen Tests <strong>steht</strong> ein exemplarischer<br />

Scheme-Ausdruck, der den Test darstellt.<br />

// Der Test wird für gewöhnlich mit mehreren Prüfungen ausgeführt.<br />

// (assert= actual expected) ist eine fiktive Funktion, die der<br />

Assert.AreEqual-Methode des Test-Frameworks enspricht.<br />

13 public class DynamicTests<br />

where Evaluator : new()<br />

where LessThan : Code, new()<br />

where Equals : Code, new()<br />

where Plus : Code, new()<br />

18 where Number : Code, new()<br />

where Bool : Code, new()<br />

where Var : Code, new()<br />

where Binding : Code, new()<br />

where Conditional : Code, new()<br />

23 where Application : Code, new()<br />

where Lambda : Code, new()<br />

{<br />

// (assert= (let ([x 20]) (+ x x 4))<br />

// 44)<br />

28 public static void LetPlus()<br />

{<br />

dynamic bind = new Binding();<br />

dynamic variable = new Var();<br />

dynamic plus = new Plus();<br />

33 dynamic apply = new Application();<br />

dynamic constant = new Number();<br />

dynamic init = new Number();<br />

93


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

variable.Name = ”x”;<br />

38 bind.Identifier = ”x”;<br />

bind.Value = init;<br />

apply.Function = plus;<br />

apply.Args = new Code[] { variable, variable, constant };<br />

bind.Body = apply;<br />

43<br />

dynamic ev = new Evaluator();<br />

for (int i = -100; i < 100; i++)<br />

{<br />

for (int k = -100; k < 100; k++)<br />

48 {<br />

constant.Value = i;<br />

init.Value = k;<br />

dynamic res = ev.Evaluate(bind);<br />

Assert.AreEqual(2 * k + i, res.Value);<br />

53 }<br />

}<br />

}<br />

// (assert= ((lambda (a b) (if (< a b ) a b)) 1 2)<br />

58 // 1)<br />

public static void LambdaIfLess()<br />

{<br />

dynamic var1 = new Var();<br />

dynamic var2 = new Var();<br />

63 dynamic lt = new LessThan();<br />

dynamic min = new Lambda();<br />

dynamic cond = new Conditional();<br />

dynamic apply1 = new Application();<br />

dynamic apply2 = new Application();<br />

68 dynamic num1 = new Number();<br />

dynamic num2 = new Number();<br />

var1.Name = ”a”;<br />

var2.Name = ”b”;<br />

73 min.ParameterIdentifiers = new[] { ”a”, ”b”, };<br />

min.Body = cond;<br />

cond.Condition = apply1;<br />

apply1.Function = lt;<br />

apply1.Args = new Code[] { var1, var2 };<br />

78 cond.Then = var1;<br />

cond.Else = var2;<br />

apply2.Function = min;<br />

num1.Value = 1;<br />

83 num2.Value = 2;<br />

apply2.Args = new Code[] { num1, num2 };<br />

dynamic eval = new Evaluator();<br />

88 for (int i = -100; i < 100; i++)<br />

{<br />

for (int k = -100; k < 100; k++)<br />

{<br />

num1.Value = i;<br />

94


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

93 num2.Value = k;<br />

dynamic result = eval.Evaluate(apply2);<br />

Assert.AreEqual(Math.Min(i, k), result.Value);<br />

}<br />

}<br />

98 }<br />

// (assert=<br />

// ((lambda (cont x) (if (= x 0) 1 (+ x (cont cont (+ x -1)))))<br />

// (lambda (cont x) (if (= x 0) 1 (+ x (cont cont (+ x -1)))))<br />

103 // 4)<br />

// 11) // Wie Fakultät nur mit + statt *: 1, 2, 4, 7, 11, 16,<br />

22, 29, ...<br />

public void ContinuationRecursion()<br />

{<br />

dynamic fun = new Lambda();<br />

108 dynamic cont = new Var();<br />

dynamic x = new Var();<br />

dynamic eq = new Equals();<br />

dynamic input = new Number();<br />

dynamic cond = new Conditional();<br />

113 dynamic recurse = new Application();<br />

dynamic plus = new Plus();<br />

dynamic applyPlus = new Application();<br />

dynamic applyEq = new Application();<br />

dynamic subtractOne = new Application();<br />

118 dynamic negOne = new Number();<br />

dynamic zero = new Number();<br />

dynamic one = new Number();<br />

dynamic test = new Application();<br />

123 zero.Value = 0;<br />

one.Value = 1;<br />

negOne.Value = -1;<br />

x.Name = ”x”;<br />

cont.Name = ”cont”;<br />

128<br />

fun.ParameterIdentifiers = new[] { ”cont”, ”x” };<br />

fun.Body = cond;<br />

applyEq.Function = eq;<br />

133 applyEq.Args = new Code[] { x, zero };<br />

cond.Condition = applyEq;<br />

cond.Then = one;<br />

cond.Else = applyPlus;<br />

138 applyPlus.Function = plus;<br />

applyPlus.Args = new Code[] { x, recurse };<br />

recurse.Function = cont;<br />

recurse.Args = new Code[] { cont, subtractOne };<br />

subtractOne.Function = plus;<br />

143 subtractOne.Args = new Code[] { x, negOne };<br />

test.Function = fun;<br />

test.Args = new Code[] { fun, input };<br />

95


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

148 dynamic eval = new Evaluator();<br />

for (int i = 0; i < 100; i++)<br />

{<br />

input.Value = i;<br />

153 dynamic result = eval.Evaluate(test);<br />

var sum = 1;<br />

for (int counter = 1; counter


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

dynamic tailApp = new Application();<br />

dynamic falseBool = new Bool();<br />

falseBool.Value = false;<br />

208 tail.ParameterIdentifiers = new[] { ”consCell” };<br />

tail.Body = tailApp;<br />

tailApp.Function = consCellVar;<br />

tailApp.Args = new Code[] { falseBool };<br />

213 dynamic isEmpty = new Lambda();<br />

dynamic isEmptyIf = new Conditional();<br />

dynamic xs = new Var();<br />

xs.Name = ”xs”;<br />

isEmpty.ParameterIdentifiers = new[] { ”xs” };<br />

218 isEmpty.Body = isEmptyIf;<br />

isEmptyIf.Condition = xs;<br />

isEmptyIf.Then = falseBool;<br />

isEmptyIf.Else = trueBool;<br />

223 dynamic one = new Number();<br />

one.Value = 1;<br />

dynamic two = new Number();<br />

two.Value = 2;<br />

228 dynamic twoList = new Application();<br />

twoList.Function = cons;<br />

twoList.Args = new Code[] { two, falseBool };<br />

dynamic oneTwo = new Application();<br />

oneTwo.Function = cons;<br />

233 oneTwo.Args = new Code[] { one, twoList };<br />

dynamic ev = new Evaluator();<br />

dynamic headTest = new Application();<br />

headTest.Function = head;<br />

238 headTest.Args = new Code[] { oneTwo };<br />

dynamic num = ev.Evaluate(headTest);<br />

Assert.AreEqual(1, num.Value);<br />

243 dynamic tailOfOneTwo = new Application();<br />

tailOfOneTwo.Function = tail;<br />

tailOfOneTwo.Args = new Code[] { oneTwo };<br />

dynamic headOfTailTest = new Application();<br />

248 headOfTailTest.Function = head;<br />

headOfTailTest.Args = new Code[] { tailOfOneTwo };<br />

253<br />

num = ev.Evaluate(headOfTailTest);<br />

Assert.AreEqual(2, num.Value);<br />

dynamic tailOfTailTest = new Application();<br />

tailOfTailTest.Function = tail;<br />

tailOfTailTest.Args = new Code[] { tailOfOneTwo };<br />

258 dynamic empty = ev.Evaluate(tailOfTailTest);<br />

Assert.AreEqual(false, empty.Value);<br />

97


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

263<br />

dynamic isEmptyTest = new Application();<br />

isEmptyTest.Function = isEmpty;<br />

isEmptyTest.Args = new Code[] { oneTwo };<br />

empty = ev.Evaluate(isEmptyTest);<br />

Assert.AreEqual(false, empty.Value);<br />

268 isEmptyTest.Args = new Code[] { tailOfOneTwo };<br />

empty = ev.Evaluate(isEmptyTest);<br />

Assert.AreEqual(false, empty.Value);<br />

isEmptyTest.Args = new Code[] { tailOfTailTest };<br />

273 empty = ev.Evaluate(isEmptyTest);<br />

Assert.AreEqual(true, empty.Value);<br />

}<br />

}<br />

278 // Es folgen die Tests der einzelnen Implementierungen. Sie befinden<br />

sich in einem Namespace,<br />

// damit dort die Klassen mit using importiert werden können.<br />

// <strong>Die</strong>s sind die Typparameter der Test-Sammlung.<br />

// <strong>Die</strong> Namen der Test-Methoden enthalten den Namen des<br />

Implementierungsansatzes,<br />

// um schnell zu sehen, welcher Implementierungsaspekt welches<br />

Ansatzes fehlerhaft war.<br />

283 namespace TypetestsTest<br />

{<br />

using Typtests;<br />

[TestClass]<br />

public class Test : DynamicTests<br />

288 {<br />

[TestMethod]<br />

public void TyptestsLambdaIfLess() { LambdaIfLess(); }<br />

[TestMethod]<br />

public void TyptestsLetPlus() { LetPlus(); }<br />

293 [TestMethod]<br />

public void TyptestsContinuationRecursion() {<br />

ContinuationRecursion(); }<br />

[TestMethod]<br />

public void TyptestsClosure() { ListClosure(); }<br />

}<br />

298 }<br />

namespace TypeCodesTest<br />

{<br />

using TypeCodes;<br />

303 [TestClass]<br />

public class Test : DynamicTests<br />

{<br />

[TestMethod]<br />

public void TypeCodesLambdaIfLess() { LambdaIfLess(); }<br />

98


Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Karsten Pietrzyk<br />

308 [TestMethod]<br />

public void TypeCodesLetPlus() { LetPlus(); }<br />

[TestMethod]<br />

public void TypeCodesContinuationRecursion() {<br />

ContinuationRecursion(); }<br />

[TestMethod]<br />

313 public void TypeCodesClosure() { ListClosure(); }<br />

}<br />

}<br />

namespace PolymorphieTest<br />

318 {<br />

using Polymorphie;<br />

[TestClass]<br />

public class Test : DynamicTests<br />

{<br />

323 [TestMethod]<br />

public void PolymorphieLambdaIfLess() { LambdaIfLess(); }<br />

[TestMethod]<br />

public void PolymorphieLetPlus() { LetPlus(); }<br />

[TestMethod]<br />

328 public void PolymorphieContinuationRecursion() {<br />

ContinuationRecursion(); }<br />

[TestMethod]<br />

public void PolymorphieClosure() { ListClosure(); }<br />

}<br />

}<br />

333<br />

namespace VisitorsTest<br />

{<br />

using Visitors;<br />

[TestClass]<br />

338 public class Test : DynamicTests<br />

{<br />

[TestMethod]<br />

public void VisitorsLambdaIfLess() { LambdaIfLess(); }<br />

[TestMethod]<br />

343 public void VisitorsLetPlus() { LetPlus(); }<br />

[TestMethod]<br />

public void VisitorsContinuationRecursion() {<br />

ContinuationRecursion(); }<br />

[TestMethod]<br />

public void VisitorsClosure() { ListClosure(); }<br />

348 }<br />

}<br />

99


Karsten Pietrzyk<br />

Deklarativ, wenn möglich; imperativ, wenn nötig<br />

Eigenständigkeitserklärung<br />

Ich versichere <strong>hier</strong>mit, die <strong>Bachelorarbeit</strong> im Studiengang Informatik selbstständig<br />

verfasst und keine anderen als die angegebenen Hilfsmittel benutzt zu haben,<br />

insbesondere keine im Quellenverzeichnis nicht benannten Internet-Quellen. Ich<br />

versichere weiterhin, dass ich die Arbeit vorher nicht in einem anderen Prüfungsverfahren<br />

eingereicht habe und die eingereichte schriftliche Fassung der auf dem<br />

elektronischen Speichermedium entspricht.<br />

Ich bin außerdem mit der Einstellung meiner Arbeit in die Bibliothek einverstanden.<br />

....................................................................................<br />

Datum, Ort, Unterschrift Karsten Pietrzyk<br />

100

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!