Westfälische Wilhelms-Universität Münster Ausarbeitung Curry im Rahmen des Seminars "Programmiersprachen" Björn Weinbrenner Themensteller / Betreuer: Prof. Dr. Herbert Kuchen Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis 1 2 3 4 Allgemeines zu Curry ................................................................................................ 3 1.1 Funktional-logische Programmierung .............................................................. 3 1.2 Entstehungsgeschichte ...................................................................................... 3 1.3 Entwicklungsumgebung.................................................................................... 4 1.4 Syntax ............................................................................................................... 4 1.5 Curry-Programme ............................................................................................. 5 Typsystem.................................................................................................................. 6 2.1 Typsystem......................................................................................................... 6 2.2 Typdeklaration .................................................................................................. 6 2.3 Standardtypen ................................................................................................... 7 2.4 Listen ................................................................................................................ 7 2.5 Funktionen ........................................................................................................ 7 2.6 Constraints (Randbedingungen) ....................................................................... 8 Funktionen ................................................................................................................. 8 3.1 Allgemein.......................................................................................................... 8 3.2 Vordefinierte Operatoren und Funktionen........................................................ 9 3.3 Konditionale Ausdrücke ................................................................................... 9 3.4 Funktionen höherer Ordnung.......................................................................... 10 3.5 Lokale Variable und lokale Funktionen.......................................................... 10 3.6 Constraints ...................................................................................................... 11 3.7 Verzögerte Auswertung (lazy evaluation) ...................................................... 11 Suche........................................................................................................................ 12 4.1 Logische Variable ........................................................................................... 12 4.2 Gekapselte Suche............................................................................................ 12 4.3 Suchprinzipien ................................................................................................ 13 5 Input/Output............................................................................................................. 14 6 Vergleich Curry und Java am Beispiel Quicksort ................................................... 14 Literaturverzeichnis ........................................................................................................ 15 II 1 Allgemeines zu Curry 1.1 Funktional-logische Programmierung Curry ist eine funktional-logische Programmiersprache. Funktionale und logische Programmierung sind die wichtigsten Paradigmen der deklarativen Programmierung. Deklarative Programme beschreiben das Problem selbst, nicht den Lösungsweg. Die Lösungsfindung ist Aufgabe des Computers. Funktionale Programmiersprachen basieren auf dem Funktionsbegriff der Mathematik. Ein Ausdruck besteht aus verschachtelten Funktionsaufrufen. Schleifen gibt es nicht, können aber problemlos über rekursive Funktionen dargestellt werden. Funktionen können Funktionen höherer Ordnung sein. Ausdrücke einer funktionalen Sprache werden deterministisch reduziert, so dass am Ende der Berechnung ein Ausdruck steht, der nur Konstruktoren enthält. Logische Programmierung basiert auf der Prädikatenlogik. Ausdrücke sind Suchziele, die Unbekannte enthalten. Das Ergebnis der Berechnung sind Bindungen für diese Unbekannten. Die Berechnung selbst ist die Suche nach diesen Bindungen. Dabei führen Ausdrücke ggf. zu nichtdetermistischen Berechnungen. Funktional-logische Sprachen vereinen die Eigenschaften der beiden Programmierparadigmen. Ausdrücke, die Unbekannte enthalten sind sog. Constraints (Randbedingungen). Es werden für die Auswertung dieser Constraints zwei wichtige operationale Prinzipien unterstützt: Residuation und Narrowing. Je nach Art der Funktionen, die innerhalb des Constraints aufgerufen werden, kommt eines der Prinzipien zur Anwendung. 1.2 Entstehungsgeschichte Die Sprache ist nach dem Logiker Haskell B. Curry benannt, nach dem auch die funktionale Programmiersprache Haskell benannt wurde. Curry ist praktisch eine Erweiterung der Sprache Haskell. Die Sprache wird von einer internationalen Initiative zwischen Hochschulen entwickelt. Im Bereich der Hochschullehre wird sie erfolgreich benutzt, logische und funktionale Programmierung mit einer Sprache zu lehren. Auf der Webseite www.informatik.uni-kiel.de/~curry/ sind praktisch alle Informationen zum Thema Curry zusammengetragen. Dort sind auch zwei Dokumente zu finden, die als Grundlage für Programmierer und Softwareentwickler dienen (s. Literaturhinweise). 1.3 Entwicklungsumgebung Im "Report on Curry" [Hanus], der z.Z. den Standard der Sprache definiert, sind Vorgaben für eine Curry-Entwicklungsumgebung zu finden. Alle Implementierungen sollen eine interaktive Programmumgebung in Form eines Kommandozeileninterpreters bereitstellen. Über diesen können Programmdateien geladen werden. Programme werden nicht ausgeführt, wie es in anderen Sprachen üblich ist. Stattdessen gibt der Benutzer einen Ausdruck über die Kommandozeile ein, der mit Hilfe der Definitionen in den geladenen Dateien ausgewertet wird. Es gibt einige wenige Implementierungen der Sprache Curry. Dabei handelt es sich um meistens um Cross-Compiler, die in PROLOG, C oder Java compilieren. Deshalb ist u.U. ein Compiler einer weiteren Sprache Vorraussetzung für die Ausführung von Curry-Programmen. Die bekannteste und am weitesten entwickelte Implementierung ist das PAKCS (Portland Aachen Kiel Curry System), dass ebenso wie die Sprache Curry von mehreren Hochschulen entwickelt wird. Es gibt keine Implementierungen für Windows-Systeme. 1.4 Syntax Die Syntax der Sprache entspricht weitesgehend der Syntax von Haskell. Das beinhaltet u.a., dass die Layout-Regel zu beachten ist. Es ist syntaktisch von Bedeutung, wie weit eine Zeile eingerückt ist. Es wird außerdem unterstützt, dass Programme literativ geschrieben sein können. In literativen Programmdateien werden nicht die Kommentare kenntlich gemacht, sondern der Code. Dieser wird durch ein Symbol ("> ") eingeleitet. Alle anderen Zeilnen werden vom Compiler ignoriert. Es ist dadurch z.B möglich, dass eine Datei gleichzeitig HTML-Datei und Curry-Programmdatei ist. 1.5 Curry-Programme Ein Curry-Programm ist eine Menge von Typdeklarationen und Funktionsdefinitionen. Diese werden in die Programmumgebung geladen und können in Ausdrücken, die der Benutzer eingibt, verwendet werden. Ein Beispiel soll den Einstieg etwas erleichtern. Die folgenden Definitionen stehen im Programmtext. data Bool = True | False and :: Bool -> Bool -> Bool and True y = y and False _ = False or :: Bool -> Bool -> Bool or True _ = True or False y = y not :: Bool -> Bool not True = False not False = True Der Benutzer stellt nun Fragen in Form von Ausdrücken an das System. and (or True False) (and False True) wird zu False ausgewertet. Das ist ein Beispiel für einen Ausdruck, der nur den funktionalen Teil der Sprache ausnutzt. Der folgende Aufruf nutzt hingegen auch den logischen Teil. and (or True False) (and X True) =:= True Die Ausgabe im PAKCS lautet darauf: Free variables in goal: X Result: success Bindings: X=True ? Der eingegebene Ausdruck ist ein Contraint. Constraints werden zu success ausgewertet, wenn Bindungen für die freien Variable gefunden werden, so dass die Constraint-Gleichung erfüllt ist. 2 Typsystem 2.1 Typsystem Curry verwendet ein Hindley-Milner-ähnliches polymorphes Typsystem. Typnotationen, wie im obigen Beispiel (z.B. not :: Int -> Int) sind nicht erforderlich, solange der Compiler alle fehlenden Typen aus dem Kontext des Programms rekonstruieren kann. Viele Fehler können daher beim Kompilieren festgestellt werden. Jedes Objekt hat einen eindeutigen Typ. Dieser Typ kann aber auch ein allgemeiner Typ sein, der durch eine Typvariable ausgedrückt wird. Die Identitätsfunktion kann z.B. folgendermaßen definiert werden: id :: a -> a id x = x Die Operation kann auf alle Typen angewandt werden. Der Typ des Ausdrucks entspricht dem dem Typ des übergebenen Parameters. Dieses Konstrukt wird parametrischer Polymorphismus genannt. Curry unterstützt keinen Ad-hoc-Polymorphismus, der das Überladen von Funktionen erlauben würde. Die Funktion (+) kann z.B. nur auf Integerwerte angewandt werden. Für Float-Werte muss eine andere Funktion definiert sein. 2.2 Typdeklaration Curry bietet viele Möglichkeiten eigene Typen zu deklarieren. Im obigen Beispiel wurde der Bool Datentyp deklariert: data Bool = True | False Bool ist dabei der Typkonstruktor, True und False sind Datenkonstruktoren. Es ist erlaubt Typen rekursiv zu definieren. Eine Liste vom Typ a ist dann folgendermaßen definiert: data list a = nil | cons a (list a) Eine Liste ist demnach eine leere Liste oder ein Kopfelement gefolgt von einer Liste. cons fügt ein einzelnes Element und eine Liste zusammen. Es lassen sich in Curry mit wenig Code komplexe Datenstrukturen (z.B. Bäume) definieren. 2.3 Standardtypen Curry stellt die Standardtypen bereit, die aus anderen Programmiersprachen bekannt sind: Bool, Int, Float, Char, String. 2.4 Listen Listen sind geordnete Zusammenstellungen von Daten. Sie sind ähnlich zu Arrays. Die Elemente einer Liste haben alle den selben Typ. Listen sind rekursiv definiert, ähnlich wie im obigen Beispiel. Jedoch ist die folgende Kurzschreibweise für eine Liste möglich: data [a] = [] | a:[a] Der Doppelpunktoperator fügt ein Kopfelement an eine Liste an. Die leere Liste wird als [], eine Liste vom Typ a als [a] notiert. Listen können auch in Aufzählungsform (z.B. [1,2,3,4,5]) oder als arithmetische Sequenz (z.B. [1..15]) geschrieben werden. Der Datentyp String ist eine Liste von Zeichen. Der folgende Ausdruck definiert ein Synonym String für den Typ [Char]: type String = [Char] 2.5 Funktionen Funktionen haben einen Typ, der aus den Typen der Parameter und dem Typ des Funktionswertes komponiert wird. Hat eine Funktion keinen Parameter, entspricht ihr Typ ihrem Funktionswert. Eine Funktion add, die zwei Integerwerte addiert, hat z.B. den Typ add :: Int -> Int -> Int Diese Notation ist eine andere Abkürzung für add :: Int -> (Int -> Int) Daran läst sich ablesen, wass passiert, wenn eine Funktion mit weniger Parametern aufgerufen wird als Ihr Typ erwartet. Die Funktion wird dann wiederum zu einer Funktion ausgewertet. Dieses Konstrukt wird partielle Anwendung genannt. Im folgenden Beispiel wird eine Funktion definiert, die den Nachfolger (Successor) einer Int-Zahl berechnet und sich dabei auf der add-Funktion abstützt. succ :: Int -> Int succ = add 1 add wird nur mit einem Parameter aufgerufen. Der Ausdruck add 1 ist daher eine Funktion vom Typ Int -> Int 2.6 Constraints (Randbedingungen) Cointraint sind vom einelementigen Typ Success. Den einzigen Wert, den dieser Typ bereitstellt, ist der Konstruktor success. Dieser wird zurückgegeben, wenn eine Constraint-Gleichung durch Bindung der auftretenden freien Variablen erfüllt ist. 3 Funktionen 3.1 Allgemein Funktionen sind Ausdrücke mit einem Namen, die Parameter haben können. Sie ermöglichen einen Ausdruck mehrfach zu verwenden oder ein Problem in kleinere Probleme zu gliedern. Eine Funktion wird durch eine oder mehrere Funktionsgleichungen definiert. Ein einfaches Beispiel ist die folgende Funktion: quadrat :: Int -> Int quadrat x = x * x Der Funktionsaufruf geschieht dann durch Aneinanderreihung des Funktionsnamens und seiner Parameter (z.B. quadrat 2). Eine Funktionsgleichung kann auf der linken Seite statt Variablen auch Konstruktoren haben. Das gilt z.B. für die Definition der Funktion not: not True = False not False = True Beim Aufruf der Funktion wird für jede Funktionsgleichung geprüft, ob das "ParameterMuster" der Gleichung auf den Funktionsaufruf zutrifft (sog. Pattern-Matching). Jede passende Gleichung wird dann angewandt. Sind die Parameter Listen, können sie auch in Doppelpunktnotation geschrieben werden, so dass direkt auf head und tail zugegriffen werden kann. head :: [a] -> a head (x:xs) = x Werden mehr als eine Funktionsgleichung angewandt, ist die Funktion nicht deterministisch. Sie folgt dann nicht mehr dem Funktionsbegriff der Mathematik. Wird eine nichtdetermistische Funktion aufgerufen, die mehr als einen Rückgabewert hat, wird nacheinander jede Lösung ausgegeben. 3.2 Vordefinierte Operatoren und Funktionen Die Standardbibliothek von Curry enthält ähnlich zu anderen Sprachen einen Zuweisungsoperator, arithmetische Operatoren, Vergleichsoperatoren und Boolsche Operatoren. Eine Besonderheit ist der Constraint-Gleichheitsoperator (=:=). Alle Operatoren sind auch Funktionen, d.h. alle erwähnten Eigenschaften von Funktionen treffen auch auf die Operatoren zu. Sind die Operatoren geklammert, gilt die gewohnte Präfixschreibweise von Funktionen, z.B. (+) 2 3 oder (==) x True Es werden einige praktische Operationen auf Listen bereitgestellt. Bespiele sind Zugriff auf das Kopfelement, Länge der Liste, die Umkehrung der Liste, das Zusammenfügen zweier Listen. 3.3 Konditionale Ausdrücke if_then_else bedingung ausdruck1 ausdruck2 wertet ausdruck1 aus, wenn bedingung zutrifft, andernfalls wird ausdruck2 ausgewertet. Um die Syntax etwas leslicher zu machen, ist auch die konventionelle Schreibweise if bedingung then ausdruck1 else ausdruck2 zulässig. Ähnlich, aber eleganter ist die Verwendung von sogenannten Guards. Beispiel: max x y | x > y = x |otherwise = y Es werden sukzessive alle Bedingungen überprüft bis die erste Bedingung zutrifft. otherwise wird zu True ausgewertet und leitet den "default case" ein. 3.4 Funktionen höherer Ordnung Funktionen höherer Ordnung sind Funktionen die als Parameter oder Funktionswert wiederum Funktionen haben. Ein Beispiel ist die vordefinierte Funktion map. map erwartet als Parameter eine Funktion und eine Liste und wendet dann die Funktion auf jedes Listenelement an: map :: (a -> b) -> [a] -> [b] map f (x:xs) = (f x) : (map f xs) Die Funktion f wird als Parameter übergeben und kann innerhalb von map verwendet werden. map succ [0,1,2] wird z.B. zu [1,2,3] ausgewertet. 3.5 Lokale Variable und lokale Funktionen Jeder Bezeichner für eine Funktion oder eine Variable hat implizit einen Sichtbarkeitsbereich, in dem über den Bezeichner auf die Funktion oder Variable zugegriffen werden kann. Funktionsdefinitionen auf oberster Ebene sind z.B. für alle anderen Funktionen sichtbar. Um diese Sichtbarkeit einzugrenzen, stellt Curry zwei Konstrukte bereit, um lokale Variable oder Funktionen zu definieren. let ... in ... deklariert im let-Teil lokale Variable und Funktionen, die im in- Teil verwendet werden können. Beispiel: let x = 3 in x * x let kann in allen Ausdrücken verwendet werden. Eine where-Klausel kann in Funktionsdefinitionen erscheinen. Hinter dem Schlüsselwort where folgen die lokalen Definitionen. Beispiel: f = x * x where x = 3 Hat eine lokale Funktion keinen Parameter (wie in diesen Beispielen), handelt es eine lokale Variable. Lokale Variable werden nur einmal berechnet und der Wert wird fest an den Bezeichner gebunden, während lokale Funktionen bei jedem Aufruf neu berechnet werden. Nichtdeterministische lokale Funktionen müssen daher mindestens einen Parameter haben, da die Funktion sonst als Variable gesehen wird und es zu einem Fehler beim Kompilieren kommt. 3.6 Constraints Constraints können in Funktionen in ähnlicher Notation auftreten wie die bereits beschriebenen Guards. Die folgende Funktion gibt das letzte Element einer Liste aus: last list | l ++ [x] =:= list = x where x, l free Zu erst wird nach Werten für die freien Variablen x und l gesucht, die die ConstraintGleichung erfüllen. x und l werden an Werte gebunden und können auf der rechten Seite der Funktionsgleichung verwendet werden. 3.7 Verzögerte Auswertung (lazy evaluation) Ausdrücke werden in Curry erst dann berechnet, wenn ihr Wert benötigt wird. Diese Auswertungsstrategie wird verzögerte oder faule Auswertung genannt. Das Gegenteil ist in vielen Sprachen der Fall, in denen die Argumente einer Funktion berechnet werden, bevor die Funktion ausgeführt wird. In Curry kann z.B. eine unendlich lange Liste an eine Funktion übergeben werden, ohne dass versucht wird, diese Liste vollständig zu berechnen. Es werden nur die Elemente der Liste extrahiert, die benötigt werden. liste = [1..] (eine unendlich lange Liste aller Ganzzahlen beginnend mit 1) head(x:xs) = x Der Aufruf head liste kann in Curry berechnet werden. 4 Suche 4.1 Logische Variable Funktionen können unbekannte Variable beinhalten, wenn diese als free deklariert sind und in einem Constraint enthalten sind. Ein Beispiel ist die Funktion last last list | l ++ [x] =:= list = x where x, l free Bevor ein Aufruf der Funktion berechnet werden kann, wird versucht, die Unbekannten an Werte zu binden, die die Constraint-Gleichheit (=:=) erfüllen. Gelingt dies, kann der Aufruf weiter berechnet werden. Andernfalls wird die Berechnung abgebrochen mit der Meldung, dass das Suchziel suspendiert wurde. 4.2 Gekapselte Suche Eine weitere Möglichkeit, nach Unbekannten zu suchen, ist die gekapselte Suche. Sie wird durch einige im Standard enthaltene Funktionen ermöglicht. Da noch nicht alle diese Funktionen in PAKCS implementiert sind, soll hier nur eine dieser Funktionen vorgestellt werden: findall :: (a -> Success) -> [a] findall erwartet einen Parameter vom Typ (a -> Success), also eine Funktion von einem Typ a zum Typ Success. Eine solche Funktion definiert das Suchziel. Die Funktion goal definiert ein solches Suchziel: goal :: (Bool -> Success) goal x = (x || True) =:= True findall gibt eine Liste zurück, die alle Werte enthält, die an a gebunden den Constraint erfüllen. Der Aufruf findall goal wird zu [True,False] ausgewertet. Prinzipiell kann durch die gekapselte Suche erreicht werden, dass der Suchvorgang in einem gekapselten Ausdruck und nicht global stattfindet. Globale Suche ist in manchen Fällen nicht effizient, da sie in der Regel nach dem Backtracking-Prinzip funktioniert. Mit der gekapselten Suche kann mehr Einfluss auf die Art der Suche genommen werden. Diese Möglichkeiten werden allerdings in den aktuellen Implementierungen noch unzureichend angeboten. 4.3 Suchprinzipien Curry unterstützt die zwei wichtigsten Prinzipien, um Lösungen für logische Variable zu finden: Residuation und Narrowing Residuation bedeutet, dass wenn freie Variable in einem Ausdruck vorkommen und eine Bindung dieser Variablen nicht direkt aus dem Ausdruck abgeleitet werden kann, erst alle anderen Ausdrücke, aus denen eine Bindung entstehen könnte, ausgewertet werden. Erst wenn die Variable gebunden ist, wird mit der Auswertung fortgefahren. Ist die freie Variable nicht gebunden und alle anderen Ausdrücke sind berechnet oder warten auch auf eine Bindung, wird die Berechnung abgebrochen. Das Suchziel wird "suspendiert". Unter Verwendung des Narrowing-Prinzips wird versucht, die Bindung der freien Variablen aus dem Ausdruck herzuleiten. Diese Vorgehensweise entspricht dem Auflösen einer mathematischen Gleichung, auch wenn das in Curry nicht ohne weiteres möglich ist. Welche Strategie angewendet wird, hängt von der Art der Funktionen ab, in denen die freien Variablen auftreten. Arithmetische Operationen sind sog. starre Funktionen (rigid), die immer dem Residuation-Prinzip folgen. Eine Berechnung der folgenden Art führt zu einer Suspendierung: nullstellen a b c | a*x*x + b*x + c =:= 0 = x where x free Andere Funktionen sind flexibel (flexible). Der Operator ++, der zwei Listen zusammenfügt, gehört dazu, so dass die oben definierte Funktion last nach dem Narrowing-Prinzip berechnet wird. 5 Input/Output Unter Verwendung der bisher erläuterten Sprachmittel treten keine Seiteneffekte auf. Das bedeutet, dass bei Aufruf einer Funktion Objekte außerhalb der Funktion nicht verändert werden. Diese Tatsache ist charakteristisch für funktionale Sprachen. Eingabe- und Ausgabefunktionen müssen daher besonders behandelt werden, weil diese Seiteneffekte z.B. beim Schreiben einer Datei auftreten würden. Die Lösung des Problems wird in Curry durch die Verwendung sog. Monaden geleistet. Monaden sind nicht die I/O-Operationen selbst sondern Objekte, die I/O-Operationen (Aktionen) bereitstellen. Zur Laufzeit werden diese Aktionen dann auf die Welt außerhalb des Programms angewandt. Das Problem der Seiteneffekte tritt dann außerhalb des funktionalen Gerüsts der Sprache aus, so dass die Grundsätze der funktionalen Programmierung weiterhin gelten können. Trotzdem gibt es einige Einschränkungen bezüglich der I/O-Operationen. Da Nichtterminismus in Zusammenhang mit Ein- und Ausgabe zu Schwierigkeiten führen kann, da z.B. der Ausführungszeitpunkt der Operationen nicht vorhersagbar ist, ist die Verwendung von I/O-Operationen nicht innerhalb von Funktionen erlaubt, die nicht selbst monadisch sind. Innerhalb von monadischen Funktionen müssen nichtterministische Ausdrücke gekapselt werden, z.B. durch eine gekapselte Suchfunktion. 6 Vergleich Curry und Java am Beispiel Quicksort Ein Vergleich zwischen Curry und einer anderen Sprache soll am Beispiel des Sortieralgorithmus Quicksort vorgenommen werden, der in Curry und in Java implementiert und schließlich ausgeführt wird. Der Vergleich soll bzgl. Dauer der Implementierung, Anzahl der Zeilen des Programms und Ausführungszeit vollzogen werden. Die Ausführungszeit ist dabei abhängig von den gewählten Entwicklungsumgebungen, in diesem Fall Java 2 Platform Standard Edition, Version 1.4.2 und PAKCS Version 1.6.0. Um die Ausführungszeit vergleichbar zu machen soll eine Datei mit 10.000 ZufallsIntegerzahlen gelesen werden. Diese sollen sortiert werden und in eine zweite Datei geschrieben werden. Im Vergleich der Implementierungszeit und Code-Länge sollen diese Lese- und Schreib-Operation allerdings unberücksichtigt bleiben und der Focus soll nur auf dem Sortieralgorithmus liegen. Alle drei betrachteten Größen unterscheiden sich signifikant zwischen den beiden Sprachen. Die Implementierungszeit betrug in Curry weniger als 5 Minuten und in Java 30 Minuten. Das Curry-Programm lässt sich in zwei Codezeilen (im Folgenden auf 4 Zeilen verteilt) ausdrücken, während das Java Programm 20 Zeilen einnimmt. Das Curry-Programm sieht folgendermaßen aus: quickSort [] = [] quickSort (x:xs) = (quickSort (filter (<= x) xs) ++ [x] ++ (quickSort (filter (> x) xs) Die filter-Funktion ist standardmäßig definiert und filtert eine Liste unter Verwendung eines Prädikates. Die resultierende Liste enthält also nur Werte, die dieses Prädikat erfüllen. In der Ausführungszeit sind ist der Unterschied allerdings ebenso prägnant. Während das Curry-Programm 16 Sekunden für das Lesen, Sortieren und Schreiben benötigte, brauchte das Java-Programm weniger als eine Sekunde. Der Vergleich dieser beider Sprachen zeigt einen Grundsatz deklarativer Sprachen auf. In deklarativen Sprachen wird das Problem und nicht dessen Lösung betrachtet. Die Lösungsfindung wird dem Computer überlassen. Dementsprechend besteht die Tendenz, dass die Implementierungszeit geringer ist als in anderen Sprachen. Dies geschieht jedoch auf Kosten der Ausführungszeit. Literaturverzeichnis [Hanus] Michael Hanus: Curry – An Integrated Functional Logic Language, Version 0.8.1, November 2003 [Antoy] Sergio Antoy: Curry – A Tutorial Introduction, November 2002.