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