Ausarbeitung / Bachelorarbeit / Diplomhausarbeit

Werbung
Westfälische Wilhelms-Universität Münster
Ausarbeitung
MetaLanguage (ML)
im Rahmen des Seminars Programmiersprachen
Philipp Westrich
Themensteller: Prof. Dr. Herbert Kuchen
Betreuer: Dipl.-Medienwiss. Susanne Gruttmann
Institut für Wirtschaftsinformatik
Praktische Informatik in der Wirtschaft
Inhaltsverzeichnis
1
Motivation.................................................................................................................. 1
2
Grundlagen der Sprache ............................................................................................ 2
2.1
Der Lambda-Kalkül ........................................................................................... 2
2.2
Syntax und Semantik.......................................................................................... 4
2.2.1
2.2.2
2.2.3
3
Wichtige Konzepte der Sprache ................................................................................ 8
3.1
Programmieren mit Funktionen ......................................................................... 8
3.1.1
3.1.2
3.2
3.3
Rekursion .................................................................................................... 8
Higher-order Functions ............................................................................. 11
Modulare und zustandsbehaftete Programmierung .......................................... 12
3.2.1
3.2.2
Modularisierung ........................................................................................ 12
Imperative Elemente ................................................................................. 14
Typdeklarationen.............................................................................................. 14
3.3.1
3.3.2
4
Standardtypen und benutzerdefinierte Typen ............................................. 4
Funktionen .................................................................................................. 6
Auswertung ................................................................................................. 6
Polymorphe Deklaration ........................................................................... 15
Ambige Deklarationen .............................................................................. 16
Verwendung der Sprache ......................................................................................... 16
4.1
Erweiterung des ‟97 Standards......................................................................... 16
4.1.1
4.1.2
4.2
Lazy Evaluation ........................................................................................ 17
Constraint Programming ........................................................................... 18
ML in der Praxis............................................................................................... 20
5
Zusammenfassung ................................................................................................... 22
A
Iterative Lösung der „Türme von Hanoi“ ................................................................ 23
B
Approximative Berechnung eines Integrals ............................................................. 24
Literaturverzeichnis ........................................................................................................ 25
II
Kapitel 1: Motivation
1 Motivation
Objektorientierte Sprachen wie C# oder Java dominieren heute die Wahrnehmung
junger Programmieranfänger. Dutzende Bücher versprechen einen schnellen Lernerfolg
in nur wenigen Tagen und auch im Internet lassen sich viele Tutorials und CodeBeispiele finden. Im Gegensatz dazu wird wenig über funktionale Sprachen wie Lisp,
Haskell oder ML geschrieben. Lässt sich daraus etwas über die Güte und Mächtigkeit
der funktionalen Sprachen herleiten? Sind sie etwa nur ein überflüssiges Paradigma aus
den Anfängen der Programmierung? Ziel dieser Arbeit ist es dieser Frage nachzugehen,
die Vorzüge der funktionalen Programmierung mit Hilfe der Sprache ML vorzustellen
und anhand von Beispielen zu untermauern. Von den zahlreichen Implementierungen
von ML wurde zur Demonstration in dieser Arbeit der New Jersey Compiler und
Interpreter (SML/NJ) gewählt, da er zu den populärsten zählt und ständig
weiterentwickelt wird [01].
ML steht für meta language (engl. „Meta-Sprache“) und wurde von Robin Milner in
Zusammenarbeit mit Malcolm Newey, Lockwood Morris und Weiteren um 1974 in
Edinburgh entwickelt, um Aussagen über Programmiersprachen mit Hilfe des
interaktiven Beweissystems „Logic for Computable Functions“ (LCF) machen zu
können. Ihre Stärke liegt in der mathematischen Ausdruckskraft, inspiriert durch die
erste „echte“ funktionale Sprache Lisp (die Milner in seiner Zeit in Stanfort kennen
lernte) sowie in der hohen Typsicherheit, die sich durch die Verwendung von
Polymorphie jedoch recht flexibel verhält [Fr93, S. 93]. Durch die Integration von
imperativen Elementen und einer starken Fehlerbehandlung erlangte ML auch
außerhalb Edinburghs viel Zuspruch und wird heute noch zur Vermittlung der
Grundlagen der Informatik an Hochschulen verwendet.
Funktionale Sprachen bilden zusammen mit den logischen Sprachen (z. B: Prolog) die
Menge der deklarativen Sprachen, die sich von den imperativen Sprachen (z. B: C,
Pascal, Java) dadurch unterscheidet, dass der Programmierer nicht dem von-NeumannModell folgend, der Maschine über Zuweisungen und Sprünge vorgibt wie ein Problem
gelöst werden soll, sondern vielmehr das Problem an sich beschreibt. Der Fokus liegt
auf der Beschreibung des Konzeptes in einer nahe an die mathematische Schreibweise
angelegten Syntax und Semantik. PEPPER beschreibt das zentrale Anliegen der
1
Kapitel 2: Grundlagen der Sprache
funktionalen Sprachen als den Versuch, etwas von der Eleganz, Klarheit und Präzision
der Mathematik in die Welt der Programmierung einfließen zu lassen [Pe03, S. 2].
Durch den Einblick in die Konzepte, die die funktionalen von den imperativen Sprachen
unterscheiden, soll zusätzlich das Verständnis des Programmierens im Allgemeinen
gefördert werden.1 Beispielsweise fördert das Konzept der Funktionen höherer Ordnung
eine abstraktere Sicht auf „generalisierte Methoden“ oder das zustandslose
Programmieren die Verwendung von Rekursion anstelle von Schleifen. Die gewonnene
Erfahrung lässt sich auf andere Sprachen übertragen und erweiterter die Fähigkeit des
Programmierers Probleme zu lösen.
Im folgenden Kapitel wird auf die mathematische Grundlage der funktionalen
Programmierung eingegangen und die Syntax und Semantik von ML dargestellt.
Anschließend werden im dritten Kapitel die Besonderheiten von ML betrachtet. Im
vierten Kapitel wird exemplarisch der Einsatz von ML in der Forschung und der
Wirtschaft vorgestellt. Darauf aufbauend soll im letzten Kapitel die Frage nach der
Relevanz funktionaler Sprachen in der heutigen Zeit beantwortet werden.
2 Grundlagen der Sprache
2.1 Der Lambda-Kalkül
Der mathematische Grundstein der funktionalen Sprachen wurde in den 1930iger Jahren
von Alonzo Church mit der Entwicklung des  -Kalküls gelegt, welches die formale
Beschreibung des Verhaltens von Computerprogrammen erlaubt [Kl07, S.237ff].
Die Sprache des  -Kalküls, die Menge der  -Terme, L , mit V als abzählbare Menge
von Variablen, ist die kleinste Menge mit folgenden Eigenschaften:
1. V  L
2. Für e0 , e1  L ist auch (e, e1 )  L .
3. Für x V , e  L ist auch ( x.e)  L .
Ein  -Term der Form (e0 , e1 ) heißt Applikation mit Operator e0 und Operand e1 . Ein
Term der Form ( x.e) heißt Abstraktion, wobei x Parameter der Abstraktion heißt und e
1
Denn schon der Philosoph Ludwig J. J. Wittenstein erkannte: „… die Grenzen meiner Welt sind die
Grenzen meiner Sprache“ (Wittenstein: Tractatus 5.6, S. 67, 1984)
2
Kapitel 2: Grundlagen der Sprache
als Rumpf bezeichnet wird. Der  -Kalkül ist ein Reduktionskalkül, der das Verhalten
von  -Termen festlegt und beschreibt, wie ein  -Term in einen anderen,
gleichbedeutenden, überführt werden kann.
Eine Besonderheit des  -Kalküls ist der Umstand, dass es ausschließlich Funktionen
gibt, doch lässt sich alles weitere (z. B. Zahlen oder boolesche Werte) mittels
Funktionen nachbilden. Allgemein ist eine Funktion definiert als:
„… ein Tripel  D f ,W f , R f  , bestehend aus einer Definitionsmenge D f , einer
Wertemenge W f und einer Relation R f  D f W f , die Funktionsgraph genannt
wird. Dieser Funktionsgraph muss linkseindeutig sein, d. h., es gibt keine zwei
Paare  a, b1   R f und  a, b2   R f mit b1  b2 .“ [Pe03, S.15]
Eine einfache Geradengleichung hat beispielsweise die Form  m, x, b. mx  b und wird
 -Term oder  -Ausdruck genannt. Der Vorspann  m, x, b. bindet hierbei die
Variablen in der typischen  -Notation an den Ausdruck. Im  -Kalküls gilt das Prinzip
der lexikalischen Bindung: Das Vorkommen einer Variable v als  -Term gehört immer
zur innersten umschließenden Abstraktion .e.v , deren Parameter ebenfalls v ist.
Es gibt zwei Reduktionsregeln im  -Kalkül, die  -Reduktion und die  -Reduktion.
Die Erste benennt eine gebundene Variable in eine andere um, die Zweite steht für
Funktionsapplikationen: Eine Abstraktion wird angewendet, indem die Vorkommen
ihres Parameters durch den Operanden einer Applikation ersetzt werden. Es ist erlaubt,
jederzeit beliebige Teilausdrücke zu reduzieren, solange sie nur  - oder  -Redexe
sind. (Dabei ist die Reduktionsreihenfolge laut dem Satz von Chruch/Rosser egal.) Es
gibt mehrere populäre Auswertungsstrategien, um denjenigen  -Redex innerhalb eines
 -Terms zu finden, der tatsächlich reduziert werden soll. Während es bei rein
funktionalen Sprachen egal ist, ob beispielsweise eine leftmost-outermost reduction
oder eine call-by-name reduction angewendet wird, muss bei ML aufgrund der
Verwendung imperativer Elemente zwingend eine feste Auswertungsreihenfolge
benutzt werden. Im konkreten Fall wird von innen nach außen ausgewertet (call-byvalue reduction). ML wird deshalb auch als strikte Programmiersprache bezeichnet.
3
Kapitel 2: Grundlagen der Sprache
2.2 Syntax und Semantik
Obwohl die Prinzipien von ML auf dem  -Kalkül aufbauen, wurde die Syntax zu
Gunsten der besseren Handhabbarkeit weniger an die mathematische Schreibweise als
an die – für große Softwareprojekte besser geeignete – Syntax von Algol und Pascal
angelehnt [MQ93, S. 37]. Sie wurde auch von der in Edinburgh verwendeten POP-2
Tradition beeinflusst, der eine leichter lesbare Version der
 -Schreibweise
zugrundeliegt. Mit 52 Schlüsselworten und reservierten Symbolen ist sie dennoch nicht
zu umfangreich, wie ein Vergleich mit Pascal (59 Schlüsselworte) und C (76 Schlüsselworte) zeigt.
ML verfügt über eine wohldefinierte Semantik. Durch die Veröffentlichung der
Definition of Standard ML auf Basis reiner Mathematik anstelle von formaler Sprache
konnte die Sicherheit der Sprache garantiert und bewiesen werden. Mehrdeutige oder
undefinierte Ausdrücke kommen somit nicht vor [Mi90, vii].
ML wurde für die direkte Kommunikation mit der Maschine entwickelt [Fr93, S. 93],
folglich wird vom System eine Eingabe (abgeschlossen mit einem „;„) erwartet, diese
ausgewertet und das Ergebnis im Fenster angezeigt. Es folgt ein kleines Beispiel. Der
Standard Prompt ist „-„, die Ausgaben des Compilers werden kursiv wiedergegeben.
Der Variablen it weist der Compiler automatisch das letzte Resultat zu. Sie wird
deshalb auch „Ergebnisbezeichner“ genannt [Sm08, S. 4]:
- "Hello World!"; (* This is a commentary *)
val it = "Hello World!" : string
Um die LCF bestmöglich unterstützen zu können, wurde ML mit einem strikten,
statischen Typsystem entworfen [Fr93, S. 93]. Anhand der Syntax der Eingabe erkennt
der Compiler automatisch den entsprechenden Typ der Variablen (Typinferenz), d. h.
der Typ muss beispielsweise bei einer Variablendeklaration nicht explizit genannt
werden. Nachdem die unterstützten Typen im folgenden Abschnitt vorgestellt wurden,
soll auf die Besonderheiten der Auswertung eingegangen werden.
2.2.1 Standardtypen und benutzerdefinierte Typen
Die Programmiersprache ML verfügt über folgende Standardtypen: Integer, Gleitkommazahlen (Typ: real), boolesche Werte und Strings. Die vordefinierten zusammengesetzten Typen umfassen record, list und tuple.
4
Kapitel 2: Grundlagen der Sprache
Ein record ist eine Sammlung von Typen, die zur Referenzierung jeweils mit einem
Lable versehen werden. Die Ordnung der Elemente ist daher irrelevant. Das Gegenteil
ist die list, in der jedes Element vom gleichen Typ sein muss und die Reihenfolge der
Elemente eine Rolle spielt. Die Funktionsweise ist mit der von Lisp oder Haskell
identisch. Der Infix Operator „::„ fügt ein Element als Kopf an die Liste an und „@„
verbindet zwei Listen mit einander. Ein tuple ist ein kartesisches Produkt seiner
Elemente. Sein Typ ist das Produkt der Typen seiner Elemente. Die Syntax der zusammengesetzten Typen lässt sich in EBNF wie folgt ausdrücken:
NAME
TYP


RECORD
LIST
TUPLE



String, der kein Schlüsselwort darstellt
bool | int | real | string | unit | TYP ref | RECORD
| TYP LIST | TUPLE
{ NAME:
:TYP { , NAME:
:TYP }}
}
nil | TYP { ::TYP:
::TYP } ::nil | LIST @ LIST
(TYP {,
, TYP })
)
Mit Hilfe von tuples ist es beispielsweise möglich, musterbasierte Definitionen (engl:
pattern matching) anzuwenden, um mehrere Zuweisungen in einer Deklaration
abzuwickeln:
- val
val a
val b
val c
(a,b,c) = (true, ["Hallo", "World!"], (7, 7.0));
= true : bool
= ["Hallo","World!"] : string list
= (7,7.0) : int * real
Eigene Datentypen können in ML, ähnlich wie der Typ enum in Haskell, mit dem
datatype Schlüsselwort erzeugt werden. Die Syntax in EBNF lautet:
TYPCREATION
DTDEF


datatype NAME = DTDEF { | DTDEF }
NAME { of TYP { * TYP } }
Bei der Erzeugung eigener Datentypen wird zwischen null- und mehrstelligen
Konstruktoren unterschieden [Sm08, S. 114-115]. Ein Beispiel für den ersten Fall liefert
der Datentyp workdays, dessen Werte wie Konstanten verwendet werden können. Im
zweiten Fall hingegen erfordert der Wert Triangel drei Fließkommazahlen um einen
Datentyp shape bilden zu können.
datatype workday = Mo | Tu | We | Th | Fr;
datatype shape = Circle of real
| Square of real
| Trianagle of real * real * real;
Zusätzlich zu eigenen Datentypen können mit dem Schlüsselwort type so genannte
Typsynonyme erzeugt werden, die lediglich einen neuen Bezeichner für einen
5
Kapitel 2: Grundlagen der Sprache
bestehenden Typen einführen. Durch ein Typsynonym Form für shape ließe sich
beispielsweise eine einfache Lokalisierung von Englisch nach Deutsch realisieren.
2.2.2 Funktionen
In ML werden auch Funktionen als normale Datentypen behandelt (vgl. Abschnitt
Fehler! Verweisquelle konnte nicht gefunden werden.) und können mit Hilfe des
Schlüsselwortes fun erzeugt werden. Es ist die Kurzschreibweise für die an das  Kalkül angelehnte anonyme Funktionsdefinition fn(x) => x… . Auch bei der
Funktionsdefinition kann pattern machting zur Vereinfachung eingesetzt werden [Pe03,
S. 165]. Um lange if-then-else-Blöcke zu vermeiden, lässt sich beispielsweise
eine Funktion zur Berechnung der Fakultät folgendermaßen definieren:
fun fac 0 = 1
(* Doesn’t terminate on negative values *)
| fac(n)= n*fac(n-1);
Wenn eine Funktion als Argument mehrere Variablen übernehmen soll, müssen diese
als tuple übergeben werden. Zusätzlich können, wie in Haskell auch, Funktionen auf
weniger Argumente angewendet werden als nötig. Diese sogenannte partielle
Applikation produziert eine neue Funktion, die bei Anwendung auf die restlichen
Argumente das gleiche Ergebnis wie die ursprüngliche vollständig applizierte Funktion
liefert. Der Übergang von der Tupelbildung (_ * _) zum Funktionspfeil -> wird dabei
als currying bezeichnet [Pe06, S. 16]. Zur Illustration folgt ein Beispiel:
- fun abstractLine(a,b) x = a*x+b; (* Currying *)
val abstractLine = fn : int * int -> int -> int
- val line = abstractLine(3,1);
(* Normal function y=3x+1 *)
val line = fn : int -> int
- line(5);
val it = 16 : int
(* Usage *)
Bei der Deklaration von line wird abstractLine mit nur zwei anstatt drei
Argumenten aufgerufen, folglich ist das Resultat kein Integer sondern eine Funktion,
die ein Argument erwartet und als Ergebnis einen Integer zurück liefert.
2.2.3 Auswertung
Der Benutzer wird in ML gezwungen, seine Operationen genau zu definieren, da der
Compiler auf Typkorrektheit prüft und gegebenenfalls eine Fehlermeldung ausgibt,
wodurch die dynamische Sicherheit der Sprache garantiert wird. Alle Werte einer
6
Kapitel 2: Grundlagen der Sprache
Operation müssen deshalb vom gleichen Typ sein. So gibt folgende Addition
beispielsweise eine Fehlermeldung aus, weil im Gegensatz zu Java keine automatische
Typumwandlung erfolgt und der Compiler anstelle des „falschen“ real-Wertes 7.0 einen
Integer erwartet:
- 7 + 7.0;
stdIn:2.1-2.8 Error: operator and operand don't agree [literal]
operator domain: int * int
operand:
int * real
in expression:
7 + 7.0
Ein Beispiel für die statische Auswertung ist der folgende Aufruf der Funktion cond:
- fun cond(b,x,y) = if b then x else y;
- cond(true,1,1 div 0);
uncaught exception Div [divide by zero]
(* Definition *)
(* Usage *)
ML wertet von innen nach außen aus (vgl. Abschnitt 0), folglich wird y ausgewertet,
obwohl es für die korrekte Ausgabe nicht zwingend erforderlich war. Eine verzögerte
Auswertungsstrategie (engl: lazy evaluation) hätte nur bis x ausgewertet und 1
zurückgegeben. Auf die Vorzüge von lazy evaluation soll im Abschnitt 4.1.1 noch
ausführlicher eingegangen werden.
In ML beginnt der Gültigkeitsbereich eines Wertes oder einer Funktion immer erst an
der Stelle ihrer Definition [Pe03, S. 124]. Folglich spielt, im Gegensatz zu Haskell, die
Reihenfolge, in der sie im Quellcode genannt werden, eine Rolle. Die folgende
Deklaration wertet beispielsweise zu 10 aus:
let val x=4
in let val x=x+1
in 2*x
end
end;
Der let-Ausdruck dient zur Deklaration eines lokalen Kontextes, der nur für die
Ausdrücke zwischen in und end gilt. Er ist folgendermaßen Definiert:
EXPR
LETEXPR


ein oder mehere gültige ML Audrücke (zu denen auch LETEXPR zählt)
let EXPR in EXPR end
7
Kapitel 3: Wichtige Konzepte der Sprache
3 Wichtige Konzepte der Sprache
3.1 Programmieren mit Funktionen
Hauptmerkmal der funktionalen Sprache, die auch für ihre Namensgebung
verantwortlich ist, ist das nicht-exklusive Arbeiten mit Funktionen. Eine Funktion ist
ein Objekt wie jedes andere auch und kann deshalb – wie ein primitiver Datentyp – als
Argument an Funktionen übergeben, als Resultat zurückgegeben oder in einer
Datenstruktur gespeichert werden. Ein komplettes Programm einer funktionalen
Programmiersprache ist somit nur eine Funktion, die gewisse Eingabeparameter
entgegennimmt und nach einer komplexen Rechnung mit einer beliebigen Anzahl von
internen Funktionsaufrufen eine gewünschte Ausgabe produziert [Hu84, S. 2].
In einer rein funktionalen Programmiersprache hat der Aufruf einer Funktion keinen
Seiteneffekt, somit kann auch jede Funktion eines Programmes einzeln evaluiert
werden. Diese referenzielle Transparenz kann das Auffinden von Fehlern vereinfachen,
weil nicht mehr die Ausführungsreihenfolge oder der Zustand des Systems
berücksichtigt werden muss. In den folgenden beiden Abschnitten soll beispielhaft die
Eleganz und Zweckmäßigkeit, die durch diese Form der Programmierung möglich ist,
aufgezeigt werden.
3.1.1 Rekursion
Rekursive Funktionen sind ein mächtiges Werkzeug, das nicht allein funktionalen
Sprachen vorbehalten, sondern vielmehr in allen gängigen Programmiersprachen wie C,
Pascal oder Java möglich ist. Jedoch lassen der einfache Aufbau und die kompakte
Syntax Rekursion in funktionalen Sprachen viel natürlicher wirken [Pe03, S. 59].
Als rekursive Funktion gilt jede Funktion, die sich in ihrem Rumpf erneut aufruft,
[Pe03, S. 60, 68-69]. Die konkrete Ausgestaltung der Selbstreferenzierung führt zu
verschiedenen Arten von Rekursion, auf die im Folgenden näher eingegangen werden
sollen.
Bei der Lineare Rekursion besteht der Rumpf einer Funktion nur aus einem bedingten
Ausdruck, für den in jedem Zweig höchstens ein rekursiver Aufruf vorkommt (ist der
Aufruf die äußerste Operation spricht man von repetitiver Rekursion). Damit führt jeder
Aufruf der Funktion unmittelbar höchstens zu einem weiteren Aufruf, d. h. es entsteht
8
Kapitel 3: Wichtige Konzepte der Sprache
insgesamt eine lineare Kette von Aufrufen. Ein Beispiel ist die im Abschnitt 2.2.2
vorgestellte Funktion fac.
Wenn als Argumente eines rekursiven Aufrufs weitere rekursive Aufrufe auftreten
können (siehe Ackermannfunktion im Abschnitt 3.1.2), spricht man von geschachtelter
Rekursion. Bei der Verschränkte Rekursion rufen sich zwei oder mehr Funktionen in
ihrem Rumpf gegenseitig auf. Ein Beispiel sind die nachfolgenden Realisierungen der
Funktionen even und odd:
fun even(n)
= if n=0 then true else odd(n-1)
and odd(n) = if n=0 then true else even(n-1);
Zu beachten ist, dass bei verschränkt rekursiven Funktionen das Schlüsselwort and
verwendet werden muss, da der Compiler Funktionen immer erst nach ihrer Deklaration
kennt [Pe03, S.76].
Bei der Baumartige Rekursion können in einem Ausdruck mehrere rekursive Aufrufe
nebeneinander vorkommen, d. h. es kommt im Allgemeinen zu einer baumartigen
Kaskade von weiteren Aufrufen. Ein Beispiel ist die im Folgenden vorgestellte Lösung
des Problems der „Türme von Hanoi“, an der die Eleganz und einfache Lesbarkeit
rekursiver Funktionsdefinitionen demonstriert werden soll. Die Aufgabenstellung des
vom Französischen Mathematiker Édouard Lucas erfundenen Puzzles lautet [Pe03, 59]:
In der Legende der „Türme von Hanoi“ muss ein Stapel von unterschiedlich großen
Scheiben von einem Pfahl auf einen zweiten Pfahl übertragen werden unter
Zuhilfenahme eines Hilfspfahls. Dabei darf jeweils nur eine Scheibe pro Zug bewegt
werden und nie eine größere auf einer kleineren Scheibe liegen.
Mit Hilfe von Rekursion lässt sich die Lösung im Pseudocode leicht verständlich
niederschreiben. Die iterative Lösung ist hingegen weniger einsichtig und um einiges
länger (siehe Anhang A):
fun bewegeStein (n Steine, Start, Ziel, Lager) =
if n = 0 then
Breche ab.
else
Bewege Stein n-1 vom Start zum Lager.
Bewege Stein n vom Start zum Ziel.
Bewege Stein n-1 vom Lager zum Ziel.
end if
end
9
Kapitel 3: Wichtige Konzepte der Sprache
Einer der großen Vorteile, die die funktionale Programmierung bietet, ist der Umstand,
dass sobald ein Problem mathematisch verstanden wurde, es nur noch „niedergeschrieben“ werden muss [Pe03, 68]. Die mathematische Syntax und Semantik
begünstigt zudem die Möglichkeit, die Richtigkeit einer Funktion zu beweisen. So liest
sich die Implementierung des Pseudocodes in ML fast genauso:
fun hanoi(n) =
let fun bewegeStein(x, ziel, start, lager) =
if x = 0 then nil
else bewegeStein(x-1, lager, start, ziel) @
[(start, ziel)] @
bewegeStein(x-1, ziel, lager, start)
in bewegeStein(n, 3, 1, 2)
end;
Das größte Problem der rekursiven Funktionen ist die Performance, die im Gegensatz
zur iterativen Variante deutlich ineffizienter sein kann [Pe06, S. 35-36]. Eine Ausnahme
ist die repetitive Rekursion, bei der keine Rechnung nachträglich zum rekursiven Aufruf
erfolgt. In der oben aufgezeigten linear rekursiven Funktion fac wird beispielsweise
für jedes n > 0 eine Addition auf den Stack gelegt, bis n = 0 erreicht wird und die
Multiplikation rückwärts erfolgen kann:
fac(4) =
4 * fac(3) =
4 * 3 * fac(2) =
4 * 3 * 2 * fac(1) =
4 * 3 * 2 * 1 * fac(0) =
4 * 3 * 2 * 1 * 1 =
4 * 3 * 2 * 1 =
4 * 3 * 2 =
4 * 6 =
24
Wenn jedoch das Ergebnis der Berechnung als Argument der Funktion mit übergeben
wird, lässt sich die Performance verbessern, da die Multiplikationen nicht mehr auf dem
Stack gehalten werden müssen. Die nachfolgende Version wird nun als endrekursiv
bezeichnet:
fun fac2(n) =
(* Also doesn’t terminate on negative values *)
let fun facER(0, x) = x
| facER(a, x) = facER(a-1, a*x)
in facER(n,1)
end;
Interessanterweise ist die rekursive Lösung des Problems der „Türme von Hanoi“ nicht
nur die kürzeste und eleganteste, sondern erzielte bei einem Vergleich mit fünf
iterativen Varianten im Bezug auf die Laufzeit sogar den zweiten Platz [Er86, S. 100].
10
Kapitel 3: Wichtige Konzepte der Sprache
3.1.2 Higher-order Functions
Das mächtigste Konzept funktionaler Sprachen kommt erst bei der Verwendung von
Funktionen höherer Ordnung (engl. higher-order functions) zum Tragen. Diese
Funktionen nehmen Funktionen als Argumente an oder liefern als Resultat wieder
Funktionen zurück. Im Gegensatz zur imperativen Sprache wie z. B: Java ist das
Konzept der Generalisierung somit nicht nur auf Klassen beschränkt, sondern lässt sich
auch auf Funktionen übertragen. Die umständliche Definition von Interfaces und die
Kapselung von einzelnen Methoden in konkreten Klassen bleiben erspart.
Anhand eines Beispiels soll gezeigt werden, wie sich mittels higher-order functions
viele Funktionen ohne großen Programmieraufwand aus einer „Grundfunktion“
generieren lassen:
- fun hyper (1) (x, y) = x + y
| hyper (2) (x, y) = x * y
| hyper (n) (x, y) = if y=0 then 1 else
hyper (n-1) (x , (hyper (n) (x, (y-1))));
val hyper = fn : int -> int * int -> int
Die Funktion hyper liefert eine Funktion zurück, die je nach Wahl des Grades n zwei
Argumente aggregiert. Für n = 1 wird eine Additionsfunktion, für n = 2 eine
Multiplikationsfunktion und für n > 2 eine n-fache Potenzfunktion erzeugt. Ohne die
Funktion höherer Ordnung müsste für jeden benötigten Grad n eine neue Funktion
manuell erzeugt werden:
fun plus(x,
fun times(_,
| times(x,
fun power(_,
| power(x,
fun super(_,
| super(x,
...
y)
0)
y)
0)
y)
0)
y)
=
=
=
=
=
=
=
x + y;
0
plus(x, (times(x, (y-1))));
1
times(x, (power(x, (y-1))));
1
power(x, (super(x, (y-1))));
x
Für die Funktion super gilt dann:  ( x, y). x
x...

y mal
. Offensichtlich wächst die Funktion
hyper sehr schnell. Wenn die Argumente x und y mit n übereinstimmen, erhält man
die Ackermannfunktion2:
- fun ackermann(n) = hyper(n)(n,n);
- ackermann(3);
(* Is equivalent to 3^3 *)
2
Für ausführliche Informationen siehe: Ackermann: Zum Hilbertschen Aufbau der reellen Zahlen,
Math. Ann. (99) S. 118-133, 1928.
11
Kapitel 3: Wichtige Konzepte der Sprache
val it = 27 : int
- power(3,3);
val it = 27 : int
- ackermann(4);
(* Equals 1.3407807929942597E154 *)
uncaught exception Overflow [overflow]
raised at: <file stdIn>
3.2 Modulare und zustandsbehaftete Programmierung
Inspiriert durch die Sprachspezifikationen CLEAR und HOPE, wurde 1983 die
ursprüngliche Version von ML unter der Aufsicht von Milner um neue Konzepte
erweitert. Beispielsweise wurden modulare Spezifikation mit Signaturen und Interfaces
sowie eine stream-basierte Ein- und Ausgabe der Sprache hinzugefügt. Darauf
aufbauend erschien 1990 der erste Standard für ML (SML) [Mi90, S. 82]. In der
aktuellen Version von 1997 wurde dieser Standard noch einmal überarbeitet und um
eine Basis-Bibliothek ergänzt, die Programmierern bei der Implementierung von großen
Softwareprogrammen unterstützen soll. Durch die Entwicklung des Standards wurde
ML auch für andere Forschungseinrichtungen und Softwarefirmen interessant, da nun
große Softwareprojekte komfortabel umgesetzt werden konnten. In dem nachfolgenden
Abschnitt soll das Konzept der Modularisierung, welches eines der wichtigsten der neu
eingeführten Konzepte darstellt und das „Programmieren im Großen“ erleichtert, näher
erläutert werden. Ein weiterer Grund für die Verbreitung von ML liegt in der Tatsache
begründet, dass ML zustandsbehaftete Programmierung zulässt [Le01, S. 189]. Auf ihre
Handhabung soll im Abschnitt 3.2.2 kurz eingegangen werden.
3.2.1 Modularisierung
Zur Realisierung von großen Softwareprojekten werden Konzepte benötigt, die es
erlauben, Funktionen und Datentypen zu strukturieren und voneinander abzugrenzen.
Ein solches Konzept ist die Modularisierung [Pe03, S. 33]. Durch die Definition einer
Schnittstelle (in ML signature genannt) ist es Möglich den Zugriff auf Daten und
Funktionen eines Paketes steuern: Der Benutzer kann nur auf die Daten und Funktionen
zugreifen, die in der Signatur angekündigt werden; die Implementierung (in ML durch
eine structure realisiert) bleibt ihm verborgen. Auf diese Weise können z. B:
Hilfsfunktionen gekapselt oder die konkrete Implementierung einer abstrakten
Datenstruktur
im
Verborgenen
ausgetauscht
werden.
Das
Verbergen
von
Implementierungsinformationen lässt sich durch die Deklarierung eines signature
12
Kapitel 3: Wichtige Konzepte der Sprache
constrains realisieren [Sm08, S. 281]. Allgemein haben die signature-Deklaration und
die Strukturdeklaration (mit optionalem signature constraint) die folgende Form:
SIGDEKL
STRUCTDEKL


signature NAME = sig EXPR end
structure NAME :[ > ] NAME = struct EXPR end
Dabei darf der Name der Struktur nicht mit dem Namen der Signatur, die sie
implementiert, übereinstimmen. Es folgt ein Beispiel für die Trennung von
Funktionsdeklaration und konkreter Implementierung für geordnete Sequenzen
(welches noch im Abschnitt 3.3.1 um Polymorphie erweitert werden wird):
signature Order = sig
val eq : 'a * 'a -> bool
val le : 'a * 'a -> bool
end
signature OrderedSequence = sig
type 'a Seq
val smaller : 'a Seq * 'a Seq -> bool
end
(* Implements Order without signature constraint *)
structure OrderedCharacters : Order = struct
fun eq(a,b) = … (* Implementation *)
val lt(a,b) = … (* Implementation *)
end;
Um auf die Funktionen und Werte einer Signatur zugreifen zu können, muss die Quelle
mit angegeben werden. Beispielhaft kann auf die Math-Schnittstelle – SML besitzt wie
Java viele vorimplementierten Bibliotheken – folgendermaßen zugegriffen werden:
- Math.pi;
[autoloading]
[library $SMLNJ-BASIS/basis.cm is stable]
[autoloading done]
val it = 3.14159265359 : real
Alternativ kann mit dem Schlüsselwort open eine Signatur in den globalen Kontext
geladen werden. Hierbei ist aber auf Namensgleichheit zu achten, da ggf. Werte
ungewollt überschrieben werden, wie folgendes Beispiel demonstriert:
- val pi = 3.14;
- open Math;
[autoloading]
…
- pi;
val it = 3.14159265359 : real
13
Kapitel 3: Wichtige Konzepte der Sprache
3.2.2 Imperative Elemente
ML wird nicht als rein funktionale Sprache angesehen, weil die Sprachdefinition auch
zustandsbehaftete Operationen zulässt. Diese wurden benötigt, um bestimmte
Algorithmen (z. B: Datenstrukturen die auf Graphen basieren) leichter implementieren
zu können, da sie imperativ formuliert leichter verständlich sind. Gerade bei der
Implementierung
von
Beweissystemen
ist
dies
ein
entscheidender
Faktor
[Le01, S. 189]. Obwohl die Integration imperativer Elemente in eine funktionale
Programmiersprache oft als Makel angesehen wird, verteidigt HUGHES diesen Ansatz
mit der Feststellung, dass keine Programmiersprache durch das Weglassen von Features
mächtiger werden kann [Hu84, S. 2]. Im Gegenteil, imperative Elemente wären
essentiell, um das volle Spektrum der funktionalen Sprachen ausbeuten zu können.
Zusätzlich lässt sich der imperative Anteil auch komplett vermeiden, da er nicht von der
Sprache aufgezwungen wird. (So wurden bis auf die Beispiele, die imperative Elemente
demonstrieren sollen, alle Beispiele dieser Arbeit rein funktional programmiert.)
Ein Beispiel aus der iterativen Implementierung der Lösung des Problems der „Türme
von Hanoi“ veranschaulicht die Verwendung von Zuständen in ML (vgl. Anhang A):
fun hanoiIter(n) =
let …
val i = ref 0
in while !i < limit do (
…
i := !i +1
);
!result
end;
(* Equivalent to the
*)
(* nonexisting for-loop *)
Mit dem Schlüsselwort ref erfolgt eine Allokation einer Variablen zu einer
Speicherzelle. Mit dem Operator „:=„ wird die Speicherzelle mit einem neuen Wert
versehen, auf dessen aktuellen Wert mit dem Operator „!„ zugegriffen werden kann.
3.3 Typdeklarationen
ML war eine der ersten Programmiersprachen, die Polymorphie in heute üblicher Form
bereitstellte. Dabei wird Polymorphie nicht strukturglobal für eine ganze Gruppe von
Typen und Funktionen gemeinsam festgelegt, sondern individuell für jeden Typ und
jede Funktion einzeln. Als polymorpher Typ gilt jede Typdeklaration einer
Datenstruktur, bei der der Basistyp ihrer Elemente als Parameter angegeben wird.
14
Kapitel 3: Wichtige Konzepte der Sprache
Mathematisch kann ein polymorpher Typ als Funktion aufgefasst werden, die Typen in
Typen abbildet. Eine polymorphe Funktion ist für Argumente unterschiedlicher Typen
definiert und lässt sich mathematisch als Familie von Funktionen auffassen. Mit Hilfe
der Polymorphie ist es folglich möglich, ein Problem abstrakter und allgemeingültiger
zu definieren [Pe03, S. 218-219, S.223-226]. Auf die verschiedenen Deklarationsarten
soll im Folgenden näher eingegangen werden.
3.3.1 Polymorphe Deklaration
Polymorphe Datentypen erlauben es, Datenstrukturen zu definieren, die unabhängig von
einem konkreten Typ sind. Somit kann das starre Typkonzept etwas aufgeweicht und
das Programmieren erleichtert werden. Mit den Typvariablen ‘a, ‘b‚‘c… können in
ML jegliche Datentypen repräsentiert werden, wie im folgenden Beispiel geschehen:
- datatype 'a Pair = pair of ('a * 'a);
datatype 'a Pair = pair of 'a * 'a
- val intPair = pair(1,1);
val intPair = pair (1,1) : int Pair
- val boolPair = pair(true,false);
val boolPair = pair (true,false) : bool Pair
Ebenso können auch Funktionen polymorph programmiert werden. Beispielsweise
liefert folgende Funktion wahlweise das erste oder das letzte Element eines Tupels:
- fun get(x,
val get = fn
- get(false,
val it = 7 :
(a:'a, b:'a)) = if x then a else b;
: bool * ('a * 'a) -> 'a
(1,7));
(* Usage *)
int
Auch in Strukturen kann Polymorphie eingesetzt werden. Gegeben sei das in
Abschnitt 3.2.1 vorgestellte Beispiel der geordneten Sequenz, dann kann der Funktor
OrdSeq eingeführt werden, der als Parameter eine Struktur(variable) namens
OrderedElements hat:
- functor OrdSeq (OrderedElements : Order) : OrderedSequence =
struct
datatype ‘a Seq
= …
(* Implementation *)
fun smaller (S1, S2) = …
(* Implementation *)
end;
- structure Words = OrdSeq (OrderedCharacters); (* Usage *)
15
Kapitel 4: Verwendung der Sprache
3.3.2 Ambige Deklarationen
Funktionsdeklarationen werden immer monomorph oder polymorph getypt. Bei valDeklarationen hingegen gibt es noch eine weitere Möglichkeit: die ambige Deklaration
[Sm08, S. 61]. Hierbei handelt es sich um eine Deklaration, die freie Typvariablen
monomorph behandelt, obwohl eigentlich eine polymorphe Typisierung möglich wäre.
Dies ist dann der Fall, wenn ihre Ausführung, die Ausführung einer Funktions- oder
Operatoranwendung
beinhaltet.
Ambige
Deklarationen
werden
aufgrund
von
Speicheroperationen benötigt, wie folgendes Beispiel demonstriert [Sm08, S. 300]:
- let val r = ref (fn x => x)
in r := (fn() => ());
1 + (!r 4)
end;
stdIn:22.9-22.13 Error: operator and operand don't agree
[literal]
operator domain: unit
operand:
int
in expression:
(! r) 4
Die Deklaration von r ist ambig, weil ihre rechte Seite eine Applikation ist. Somit muss
r mit ref typisiert werden. Während jedoch das erste benutzende Auftreten von r
einen Typ (unit  unit) ref verlangt, wird bei der zweiten Benutzung ein Typ
(int  int) ref benötigt. Folglich ist der Ausdruck unzulässig und es wird eine
Fehlermeldung ausgegeben.
4 Verwendung der Sprache
4.1 Erweiterung des ’97 Standards
ML wird in vielen Forschungseinrichtungen und einigen Firmen für verschiedene
Zwecke eingesetzt und gegebenenfalls erweitert, falls der Funktionsumfang des ‟97
Standards nicht ausreichte. So fügte SML/NJ dem Standard beispielsweise Vektoren,
OR Muster oder Module höherer Ordnung hinzu. Eine andere umfassende Erweiterung
mit dem Namen Alice ML wurde an der Universität des Saarlandes im Rahmen des
Forschungsprojektes Ressourcenadaptive kognitive Prozesse entwickelt und ermöglicht
parallele, verteilte und bedingte Programmierung [02]. Zu diesem Zweck musste SML
auch um verzögerte Auswertung erweitert werden, die im Folgenden näher vorgestellt
16
Kapitel 4: Verwendung der Sprache
wird. Im Anschluss soll anhand des SEND-MORE-MONEY-Beispiels die Vorzüge der
bedingten Programmierung (engl: constraint programming) aufgezeigt werden.
4.1.1 Lazy Evaluation
Bei der verzögerten Auswertung werden die Argumente einer Funktion erst dann
ausgewertet, wenn sie tatsächlich benötigt werden. In Haskell werden alle Funktionen
hierfür automatisch nach dem call-by-need-Prinzip ausgewertet, in Alice ML hingegen
wurde aufgrund der strikten Auswertung von ML ein anderer Weg gewählt: Ein
spezieller Typ (genannt lazy futures) verhindert die selektive Auswertung so
lange wie möglich [Ne06, S. 2,11].
Neben der Möglichkeit Funktionen aus Funktionen zu generieren, bietet das Konzept
der higher-order functions in Kombination mit der verzögerten Auswertung ein weiteres
mächtiges Instrument: Die Möglichkeit ganze Programme miteinander zu verknüpfen
[Hu84, S. 8-9]. Da ganze Programme in einer funktionalen Sprache auch nur
Funktionen sind, können somit Programme als Argument übergeben oder als Resultat
zurückgegeben werden. Seien foo und bar zwei Programme, dann würde in dem
Ausdruck foo( bar( input)) das Programm bar durch die verzögerte
Auswertung erst dann gestartet werden, wenn foo versucht sein Argument zu lesen.
Somit kann bar sogar ein nicht terminierendes Programm sein, denn sobald foo seine
Berechnungen abgeschlossen hat, wird bar automatisch (von außen) terminiert.
Anstelle von komplexen Abbruchbedingungen kann die Frage der Terminierung von
Programmen durch diese Methode elegant umgangen werden. Wenn die verzögerte
Auswertung uniform für jeden Funktionsaufruf durchgeführt wird, ist es sogar möglich
jeden beliebigen Teil des Programmes auf diese Art und Weise zu modularisieren.
Ein vereinfachtes Beispiel ist die näherungsweise Berechnung des Integrals einer
Funktion in einem bestimmten Bereich [Hu84, S. 13-14]. Eine grobe Annäherung erhält
man bereits mit folgender einfachen Integralfunktion, die jede Funktion als linear
ansieht:
- fun easyintegrate(f, a, b) = (f(a) + f(b))*(b-a)/2.0;(* Def *)
- easyintegrate(Math.sin, 0.0, Math.pi);
(* Usage *)
val it : real = 0.420735492404
Die Berechnung lässt sich verbessern, indem das Intervall [a, b] halbiert und die Fläche
unterhalb der beiden Hälften addiert wird. Bei jedem weiteren Halbierungsschritt erhöht
17
Kapitel 4: Verwendung der Sprache
sich die Genauigkeit, sodass sich eine endlose Liste von Annäherungen erstellen lässt,
die sich immer mehr dem tatsächlichen Ergebnis annähert. Die Funktion integrate
benutzt hierfür eine Reihe von lazy Hilfsfunktionen (die verzögerte Auswertung wird
mit dem vorangestelltem Schlüsselwort lazy einleitet). lmap und lzip entsprechen
den gängigen Listenfunktionen map und zip, ladd addiert ein durch lzip generiertes
Paar zu einem Wert und integ berechnet die Integralfläche der Hälften (der
vollständige Quellcode für die folgenden Funktionen ist im Anhang B abgedruckt):
fun lazy integrate(f, a, b) =
let …
fun lazy integ (f, a, b, x, y) =
let val m = (a+b)/2.0
val z = f(m)
in (x+y)*(b-a)/2.0 :: lmap ladd
(lzip( integ(f, a, m, x, z), integ(f, m, b, z, y)))
end
in integ(f, a, b, f(a), f(b))
end;
Da für die Hilfsfunktion integ keine Abbruchsbedingung definiert wurde, produziert
sie eine endlose Liste von Fließkommazahlen. Jedoch werden diese nur soweit
ausgewertet, wie für die Berechnung eines konkreten Wertes tatsächlich erforderlich ist.
Mit der Funktion within wird beispielsweise so lange integriert, bis die Differenz
zweier aufeinander folgenden Annäherungen kleiner als der angegebene Wert eps ist:
- List.take(integrate(Math.sin, 0.0, 1.0),3);
val it : real list = [0.420735492404, _lazy, _lazy]
- fun within(eps, (a::b::rest)) = if abs(1.0*a-b) <= eps then b
else within(eps, b::rest);
- within(0.0001, integrate(Math.sin, 0.0, 1.0));
val it : real = 0.45968834152
Die verzögert ausgewerteten Funktionen lassen sich, genau wie die beispielsweise
streams in Java, beliebig verschachteln. So kann die Funktion super verwendet
werden, um die Annäherungen noch schneller konvergieren zu lassen:
- within(0.0001, (super ( integrate(Math.sin, 0.0, 1.0))));
val it : real = 0.45969769039
4.1.2 Constraint Programming
Constraints sind spezielle prädikatenlogische Formeln, mit deren Hilfe der Benutzer
Eigenschaften von kombinatorischen Problemen und deren Lösungen durch
Bedingungen oder Einschränkungen beschreibt [Ho07, S. 46-53]. Mit der Belegung der
18
Kapitel 4: Verwendung der Sprache
Variablen mit konkreten Werten wird ein Constraint entweder erfüllt oder verletzt. In
einem baumartigen Suchverfahren übernimmt nun der Computer die Suche nach einer
oder mehreren Lösungen; dabei werden alle Pfade abgeschnitten, die eine Bedingung
verletzen. Eine Lösung wird als gültig angesehen, wenn für alle Variablen konkrete
Werte eingesetzt werden konnten, ohne dass eine Bedingung verletzt wurde. Anstatt
also jede mögliche Kombination stumpf auszuprobieren, wird der Suchraum schon vor
der Zuweisung konkreter Werte für die Variablen durch die Überprüfung der
Erfüllbarkeit
aller
Bedingungen
stark
eingeschränkt.
Demzufolge
können
Inkonsistenzen vermieden werden.
Ein Beispiel ist das krypto-arithmetische Puzzel SEND-MORE-MONEY. Jeder
Buchstabe der Gleichung SEND + MORE = MONEY soll durch eine der Ziffern 0 bis 9
belegt werden, so dass die Geleichung erfüllt wird. Die Definition des Problems mit
Hilfe von Alice lautet:
import structure
import structure
import structure
import structure
open Modeling;
FD
Modeling
Search
Explorer
from
from
from
from
"x-alice:/lib/gecode/FD";
"x-alice:/lib/gecode/Modeling";
"x-alice:/lib/gecode/Search";
"x-alice:/lib/tools/Explorer";
fun money sp =
let val letters as #[S,E,N,D,M,O,R,Y]
fdtermVec (sp, 8, [0`#9])
in distinct (sp, v, FD.BND);
post (sp, S `<> `0, FD.BND);
post (sp, M `<> `0, FD.BND);
post (sp,
`1000`*S `+
`+
`1000`*M `+
`= `10000`*M `+ `1000`*O `+
FD.BND);
branch (sp, letters, FD.B_SIZE_MIN,
{S,E,N,D,M,O,R,Y}
end;
=
`100`*E `+ `10`*N `+ D
`100`*O `+ `10`*R `+ E
`100`*N `+ `10`*E `+ Y,
FD.B_MIN);
Explorer.exploreAll money;
Zunächst werden den Buchstaben die möglichen Ziffern von 0 bis 9 zugeordnet. Die
Funktion distinct sorgt dafür, dass alle Variablen verschieden belegt werden.
Anschließend werden die weiteren Constrains mit post definiert. Dabei lassen sich
neben der Gleichung selbst noch zwei weitere Nebenbedingungen ableiten: weder M
noch S dürfen 0 sein.
19
Kapitel 4: Verwendung der Sprache
( D=2..8, E=4..7, M=1, N=5..8, O=0, R=2..8, S=9, Y=2..8 )
E=4
()
E <> 4
( D=2..8, E=5..7, M=1, N=6..8, O=0, R=2..8, S=9, Y=2..8 )
E=5
( D=2, E=5, M=1, N=6, O=0, R=8, S=9, Y=2 )
E <> 5
( D=2..8, E=6..7, M=1, N=7..8, O=0, R=2..8, S=9, Y=2..8 )
E=6
()
Entscheidung
Lösung
E <> 6
()
Verletzt Bedingung
Abbildung 4-1: First-Fail Suchbaum
Die Funktion branch bestimmt das Verhalten des Suchalgorithmus. In diesem Fall
wurde eine first-fail Strategie verwendet, d. h. es wird die Variable überprüft, die die
wenigsten möglichen Werte annehmen kann und davon der kleinste Wert zur
Verzweigung genutzt. Wie aus der Abbildung 4-1 ersichtlich, wurden auf diese Weise
deutlich weniger als die 108 Möglichkeiten untersucht, welches für die Effizienz des
Verfahrens spricht. Die Lösung lässt sich aus dem grün unterlegtem Kasten ablesen.
4.2 ML in der Praxis
Obwohl sich die ML nach der Veröffentlichung des ‟97 Standards auch für große
Softwareprojekte eignete, wurde es außerhalb der Forschung und Lehre nur selten
verwendet. PEPPER sieht die Ursache weniger in der Sprache an sich, als in der
Tatsache, dass Menschen lieber mit den Sprachen und Konzepten arbeiten, die ihnen
vertraut sind, als sich für eine weniger vertraute Sprache zu entscheiden, die für das
spezifische Problem geeigneter wäre [Ay99].
Der naheliegende Einsatz von ML ist der Einsatz als Beweissystem, welches auf die
Wurzeln von LCF zurückzuführen ist. Ein bekanntes Beispiel ist das an der TU
München und der University of Cambridge entwickelte Programm Isabelle [03]. Es
erlaubt den Ausdruck mathematischer Beweise in formaler Sprache und unterstützt die
formale Verifikation, bei der u. A. die Korrektheit von Hardware und Software sowie
Eigenschaften von Computersprachen und Protokollen bewiesen werden können.
Zusätzlich können ausführbare Spezifikationen in SML, OCaml oder Haskell generiert
20
Kapitel 4: Verwendung der Sprache
werden. Ein weiteres Programm ist das System wHOLe, welches auf der
Prädikatenlogik höherer Stufe (eng. higher-order logic) basiert [Wo99]. Es ist in
SML/NJ geschrieben und erlaubt Verifizierungen von ganzen Programmen in derselben
Sprache. Ein Beispiel aus der Industrie bietet Motorola UK. Dort wird ML verwendet,
um die Syntax und Semantik von Message Sequence Charts (MSC) zu validieren [04].
MSC werden hautsächlich in der Telekommunikationsbranche für die einheitliche
Darstellung von Nachrichtenfolgen eingesetzt. Das Programm kann zusätzlich
Testscipts für die Systeme genieren, die MSC implementieren. Auch bei der Behebung
des „Millennium-Bugs“ in Cobol-Programmen, bei dem durch die zweistellige
Darstellung der Jahreszahlen sowohl das Jahr 1900 als auch 2000 gemeint sein konnte,
setzte der Entwickler Hafnium auf die Sprache ML [05].
Eine andere Domäne von ML ist das Gebiet des Compilerbaus. So existiert eine
Implementierung der renommierten Compilertools Lex und YACC (Abk.: Yet Another
Compiler Compiler) in ML [06, 07]. Diese Programme ermöglichen die lexigraphische
Analyse und Parsing-Funktionen, die das Frontend eines Compilers bilden. Der Entwurf
eines effizienten Compilers für eine spezifische Programmiersprache ist sehr
zeitaufwendig, besonders wenn mehrere Hardwarearchitekturen unterstützt werden
sollen. Um die Programmierer bei der Portierung des Compilers auf andere
Architekturen,
der
Wiederverwendung
von
Compiler-Konzepten
und
der
abschließenden Optimierung softwaremäßig zu unterstützen, wurde an der New York
University in Zusammenarbeit mit Bell Labs ein auf ML basierendes Framework mit
dem Namen MLRisc entwickelt [08]. Neben den Compiler für die Sprachen C-- oder
Moby gehört auch der für diese Arbeit verwendete Compiler SML/NJ zu den Nutzern
von MLRisc. Abschließend soll noch MLton erwähnt werden, welches ganze
Programme, die dem ‟97 Standard gehorchen, optimiert kompilieren kann [09]. Selbst
große Programme stellen kein Problem dar, beispielsweise hat MLton (140 000 lines of
code) sich selbst kompiliert.
21
Kapitel 5: Zusammenfassung
5 Zusammenfassung
In den vorherigen Kapiteln wurden die Vorzüge der funktionalen Programmierung,
insbesondere die elegante Ausdrucksmöglichkeit und das Arbeiten mit Funktionen
höherer Ordnung vorgestellt und anhand von Beispiel-Programmen die erfolgreiche
Verankerung dieser Konzepte in ML demonstriert.
Der von vielen Unterstützern der funktionalen Sprache als „Makel“ angesehene,
imperativer Anteil von ML hat dabei keineswegs zu Komplikationen geführt. Im
Gegenteil, durch das Weglassen dieses Features hätte ML an Ausdruckskraft verloren
und würde einige Implementierungen nur unnötig erschweren. Auch das von den
objektorientierten Sprachen dominierte Konzept der Abstraktion und Verbergung kann
mit Hilfe des Modulsystems entsprechend anwendet werden. Durch die Erweiterung um
lazy evaluation lassen sich schließlich ganze Programme elegant mit einander
verknüpfen.
Die Entwicklungen an Alice ML oder Isabelle zeigen, das trotz des großen Zulaufs zu
gängigen Programmiersprachen wie Pascal, Java oder C# funktionale Sprachen ihre
Daseinsberechtigung nicht verloren haben und in einigen Gebieten, wie zum Beispiel
die
Validierung
von
Systemen,
sogar
bevorzugt
verwendet
werden.
Programmiersprachen wie ML oder Python haben gezeigt, dass die Verschmelzung von
funktionalen und imperativen Konzepten durchaus erfolgsversprechend sein kann und
man mag gespannt sein, ob in Zukunft andere Programmiersprachen in die selben
Fußstapfen treten werden.
22
Anhang A: Iterative Lösung der „Türme von Hanoi“
A
Iterative Lösung der „Türme von Hanoi“
Im Folgenden eine iterative Implementierung der Lösung des Problems der “Türme von
Hanoi” in Anlehnung an die Implementierung von Mark Allen Weiss [10].
fun hanoiIter(n) =
let fun even(x) = x mod 2 = 0
fun shift(f, x, y) =
let fun pow(a, 0) = 1
| pow(a, b) = if even(b) then pow(a*a, b div 2)
else pow(a*a, b div 2) * a
in f(x, pow(2,y))
end
(* disk to be moved in step i *)
fun getDisk(x) =
let val d = ref 0 and i = ref (x+1)
in while even(!i) do (
i := !i div 2;
d := !d+1
);
!d
end
(* how many times disk d is moved before stage i *)
fun movements(i, d) =
let fun opDiv(x,y) = x div y
infix 8 >>
fun A >> B = shift(opDiv, A, B)
in ((i >> d) +1) >> 1
end
(* clockwise = 1; 2 the other way *)
fun direction(d) = 2 - (n mod 3 +d) mod 2
infix 8 <<
fun A << B = shift(op*, A, B)
val i = ref 0
val limit = 1 << n -1
val disk = ref 0
val start = ref 0
val dest = ref 0
val result = ref []
in while !i < limit do (
disk := getDisk(!i);
start := (movements(!i, !disk) * direction(!disk)) mod 3;
dest := (!start + direction(!disk)) mod 3;
result := !result @ [(!start + 1, !dest + 1)];
i := !i +1
);
!result
end;
23
Anhang B: Approximative Berechnung eines Integrals
B
Approximative Berechnung eines Integrals
fun lazy integrate(f, a, b) =
let fun lazy lzip(x::xs, y::ys) = [x,y] :: lzip (xs,ys)
|
lzip
_
= nil
fun lazy lmap f
nil = nil
|
lmap f (x::xs) = f(x) :: lmap f xs
fun lazy ladd(nil)
= 0.0
|
ladd(hd::l) = hd + ladd(l)
fun lazy integ (f, a, b, x, y) =
let val m = (a+b)/2.0
val z = f(m)
in (x+y)*(b-a)/2.0 :: lmap ladd
(lzip( integ(f, a, m, x, z), integ(f, m, b, z, y)))
end
in integ(f, a, b, f(a), f(b))
end;
fun lazy super(s) =
let fun lazy lmap f
nil = nil
|
lmap f (x::xs) = f(x) :: lmap f xs
fun lazy second (a::b::rest) = b
fun lazy repeat(f, a) = a :: repeat(f, f(a))
fun lazy improve(s) =
(* reduce error *)
let fun lazy elimerror(n, a::b::rest) =
let val h = Math.pow(2.0, n)
in (b*h-a)/(h-1.0) :: elimerror(n, b::rest)
end
(* estimate best n for elimerror * )
fun lazy order (a::b::c::rest) =
let fun roundR(n) = real (round n)
fun log2(x) = Math.ln(x)/Math.ln(2.0)
in roundR( log2( (a-c)/(b-c) -1.0))
end
in elimerror( order(s), s)
end
in lmap second (repeat( improve, s))
end;
(* within(0.0001, (super ( integrate(Math.sin, 0.0, 1.0)))); *)
24
Literaturverzeichnis
Literaturverzeichnis
[Ay99]
Sibel Aydinc, Amarilis M. Aranya: Interview: OPAL, iCoup (2), 1999.
URL: http://user.cs.tu-berlin.de/~icoup/archiv/2.ausgabe/artikel/opal.html
Abrufdatum: 07.04.2009.
[Er86]
M. C. Er: Performance evaluations of recursive and iterative algorithms for
the Towers of Hanoi problem, Computing 37(2), S. 93-102, 1986.
[Fr93]
Kaxen Frenkel: An interview with Robin Milner, Comm. of the ACM 36(1),
S. 90-97, 1993.
[Ho07]
Petra Hofstedt, Armin Wolf: Einführung in die ConstraintProgrammierung, Springer, 2007.
[Hu84]
John Hughes: Why Functional Programming Matters, 1984
URL: http://www.cs.chalmers.se/~rjmh/Papers/whyfp.html
Abrufdatum: 20.03.2009.
[Le01]
Martin Leucker, Thomas Noll, Perdita Stevens, MichaelWeber: Functional
programming languages for verification tools: a comparison of Standard
ML and Haskell, Proceedings of the Scottish Functional Programming
Workshop, S. 184-194, 2001.
[Kl07]
Herbert Klaeren, Michael Sperber: Die Macht der Abstraktion, Teubner,
2007.
[Mq93]
David B. Macqueen: Reflections on Standard ML, Functional Programming,
Concurrency, Simulation and Automated Reasoning, S. 32-46, 1993.
[Mi90]
Robin Milner, Mads Tofte, Robert Harper: The Definition of Standard ML,
MIT Press, 1990.
[Ne06]
Georg Neis: A Semantics for Lazy Types, Bachelorarbeit, Universität des
Saarlandes, 2006. URL: http://www.ps.uni-sb.de/Papers/abstracts/lazytypes.html. Abrufdatum: 07.04.2009.
[Pe03]
Peter Pepper: Funktionale Programmierung in Opal, ML, Haskell und
Gofer, 2. Aufl., Springer, 2003.
[Pe06]
Peter Pepper, Petra Hofstedt: Funktionale Programmierung – Sprachdesign
und Programmiertechnik, Springer, 2006.
[Sm08]
Gert Smolka: Programmierung – eine Einführung in die Informatik mit
Standard ML, Oldenbourg, 2008.
[Wo99]
Mark E. Woodclock: The wHOLe System, Applied Formal Methods – FMTrends 98 1641/1999, S.359-366, 1999.
25
Literaturverzeichnis
[01]
URL: http:// www.smlnj.org. Abrufdatum: 20.03.2009.
[02]
URL: http://www.ps.uni-sb.de/alice/. Abrufdatum: 07.04.2009.
[03]
URL: http://isabelle.in.tum.de/index.html. Abrufdatum: 07.04.2009.
[04]
URL: http://homepages.inf.ed.ac.uk/wadler/realworld/ptk.html.
Abrufdatum: 07.04.2009.
[05]
URL: http://homepages.inf.ed.ac.uk/wadler/realworld/annodomini.html.
Abrufdatum: 07.04.2009.
[06]
URL: http://www.cs.princeton.edu/~appel/modern/ml/ml-lex/.
Abrufdatum: 07.04.2009.
[07]
URL: http://www.smlnj.org/doc/ML-Yacc/index.html.
Abrufdatum: 07.04.2009.
[08]
URL:http://www.cs.nyu.edu/leunga/www/MLRISC/Doc/html/INTRO.html.
Abrufdatum: 07.04.2009.
[09]
URL: http://mlton.org/. Abrufdatum: 07.04.2009.
[10]
URL: http://www.cs.cornell.edu/Courses/cs211/2006fa/Lectures/L03Recursion/Hanoi-Iterative.java, Abrufdatum: 03.04.2009.
26
Herunterladen