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