07.11.2013 Aufrufe

2 Grundlagen der Sprache - Department of Information Systems ...

2 Grundlagen der Sprache - Department of Information Systems ...

2 Grundlagen der Sprache - Department of Information Systems ...

MEHR ANZEIGEN
WENIGER ANZEIGEN

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

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!