Westfälische Wilhelms-Universität Münster Ausarbeitung Curry im Rahmen des Vertiefungsmoduls Programmiersprachen Sebastian Luhn Themensteller: Prof. Dr. Herbert Kuchen Betreuer: Susanne Gruttmann Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis 1 Curry – eine funktional-logische Programmiersprache .............................................. 3 2 Grundlagen von Curry ................................................................................................ 4 2.1 Aufbau eines Curry-Programms ......................................................................... 4 2.2 Aufbau der Typ- und Funktionsdeklarationen .................................................... 4 2.2.1 Typdeklarationen ........................................................................................... 4 2.2.2 Funktionsdeklarationen ................................................................................. 5 2.3 Listen................................................................................................................... 5 2.4 Pattern Matching ................................................................................................. 6 2.5 Lokale Definitionen ............................................................................................ 7 2.5.1 Lokale Definitionen mittels „let“ .................................................................. 7 2.5.2 Lokale Definitionen mittels „where“............................................................. 8 2.6 Nicht-Determinismus .......................................................................................... 8 2.7 Constraints und freie Variablen .......................................................................... 9 3 Besonderheiten von Curry ........................................................................................ 10 3.1 Lazy Evaluation ................................................................................................ 10 3.2 Zwei Wege zur Berechnung logischer Variablen ............................................. 12 3.2.1 Residuation .................................................................................................. 12 3.2.2 Narrowing, Lazy Narrowing und Needed Narrowing ................................. 13 3.3 Die operationelle Semantik Currys ................................................................... 14 3.4 Anwendungsgebiete Currys .............................................................................. 16 3.5 Beispielprogramm: „Send more money“ .......................................................... 18 4 Fazit .......................................................................................................................... 19 Literaturverzeichnis ........................................................................................................ 21 II Kapitel 1: Curry – eine funktional-logische Programmiersprache 1 Curry – eine funktional-logische Programmiersprache Die vorliegende Ausarbeitung beschäftigt sich mit Curry, einer funktional-logischen Programmiersprache. Sowohl funktionale als auch logische Programmiersprachen gehören zur Klasse der deklarativen Programmiersprachen – beides sind also höhere Programmierparadigmen. Deklarative und insbesondere funktionale Programmiersprachen sind sehr formale Sprachen, sie lassen sich also mathematisch exakt beschreiben; zudem hat dies Auswirkungen darauf, wie schnell und einfach etwa die Korrektheit eines Programms bestimmt werden kann. Vor diesem Hintergrund sind auch die Ziele, die Curry verfolgt, zu sehen: Die deklarativen Programmiersprachen teilen sich hauptsächlich in zwei Gebiete, die funktionalen und die logischen Programmiersprachen. Durch die Kombination versucht Curry zum einen, beide deklarativen Welten zu verbinden, aber auch, weiterhin eine stark formal geprägte Sprache zu bleiben. Die wichtigsten Eigenschaften beider Welten, wie etwa „Lazy Evaluation“ auf der funktionalen und die Ermittlung logischer Variablen auf der logischen Seiten werden kombiniert, um in der Summe mehr zu sein als nur die Teile: Curry versucht, neue Programmiertechniken und -möglichkeiten zu schaffen, die durch diese Kombination entstehen. Ihren Ursprung hat Curry durch ihre Eigenschaften natürlich in den funktionalen und logischen Programmiersprachen. Eine starke Verwandtschaft bei den funktionalen Sprachen besteht zu Haskell, hier sind große Teile der (funktionalen) Syntax und Semantik entlehnt. Aufgrund der noch relativ jungen Geschichte der deklarativen Programmiersprachen ist auch Curry noch eine relativ junge Sprache. Entwickelt wurde sie Anfang und Mitte der 1990er-Jahre insbesondere von Michael Hanus, damals an der RWTH Aachen. Wie später noch ausgeführt wird, stand und steht dabei durchaus die eher akademische Nutzung der Sprache im Vordergrund – also der Einsatz für Forschung und Lehre. Hier sind die erwähnten Eigenschaften wie die hohe Formalität insbesondere gefragt. 3 Kapitel 2: Grundlagen von Curry 2 Grundlagen von Curry 2.1 Aufbau eines Curry-Programms Curry ist, wie schon in der Einleitung erwähnt, eine funktional-logische Programmiersprache, kombiniert also zwei Programmierparadigmen: die funktionale Programmierung sowie die logische Programmierung [Ha06]. Der Aufbau eines Curry-Programms ergibt sich zum Teil aus dieser Verwandtschaft: Grundsätzlich besteht ein CurryProgramm – wie etwa Haskell-Programme – aus zwei Teilen: Den Typdeklarationen und den Funktionsdeklarationen [Ha06]. Die Typdeklarationen stehen dabei vor den Funktionsdeklarationen. Sie beschreiben die Struktur der Daten, die in den Funktionen benutzt werden. In den Funktionsdeklarationen wird beschrieben, auf welche Weise die Eingangsvariablen behandelt werden, um ein Ergebnis vom Ausgangstyp zu erhalten. 2.2 Aufbau der Typ- und Funktionsdeklarationen Sowohl Typ- als auch Funktionsdeklarationen werden in Gleichungsform formuliert, sind also von der grundsätzlichen Form x = y. Auf der linken Seite der Gleichung steht dabei der Name des zu definierenden Typs bzw. der zu definierenden Funktion und die Wertigkeit des Typs bzw. der Funktion. Links des Gleichheitszeichens steht die Definition des Typs bzw. der Funktion. 2.2.1 Typdeklarationen Typdeklarationen sind von der Form data T a1 … an = C1 t11 … t1n1 | … | Ck tk1 … tknk. Dabei ist T der Name des Typs und des Typkonstruktors. Da die Typen auch höherwertig sein können, sind auch mehr als eine Typvariable a möglich. Bei der Wertigkeit der Typkonstruktoren spricht man dabei von einem n-ären Typkonstruktor, wobei n die Anzahl der Typvariablen ist. Rechts des Gleichheitszeichens stehen k Datenkonstruktoren C, die jeweils wieder eine bestimmte Anzahl von Ausdrücken t erhalten [Ha06]. Die Bedeutung dieser Gleichung ist folgende: Der Datentyp T, der von der Wertigkeit n ist, ist entweder C1 mit den Ausdrücken t11 … t1n1 oder C2 mit den Ausdrücken t21 … t2n2 bis hin zu Ck. Ein binärer data Tree a = Leaf a Baum | ist z.B. durch die Node (Tree a) a (Tree a) Deklaration beschrie- ben [Ha06]. Die Bedeutung dieser Zeile ist: „Ein einwertiger Baum ist entweder ein 4 Kapitel 2: Grundlagen von Curry Blatt mit einem Wert oder ein Knoten mit einem linken Teilbaum, dem Wert und einem rechten Teilbaum“. Bereits in Curry integriert und damit nicht vom Programmierer zu spezifizieren sind unter anderem die Typen Int, Bool, Char, String, Success und Listen. Der Typ Success ist dabei eine Besonderheit von Curry, die die Sprache etwa von Haskell unterscheidet. Vom Typ Success sind mittels sogenannter Constraints ermittelte Ergebnisse, diese werden später noch genauer beschrieben. Das Lösen eines Gleichungssystems kann etwa ein solcher Constraint sein. 2.2.2 Funktionsdeklarationen Funktionsdeklarationen bestehen aus einer Typdeklaration, die aber auch entfallen kann, gefolgt von einer Reihe von Gleichungen, die die Funktion definieren [Ha95]. Die Typdeklaration ist von der Form f :: t1 -> t2 ->… -> tn -> t. Ebenso wie bei den Typdeklarationen wird die Anzahl der tk als Wertigkeit bzw. n-Ärigkeit bezeichnet. t1 … tn geben die Typen der eingehenden Variablen an, t den Typ der Ergebnisvariable. Eine Funktion kann immer nur einen Ergebniswert liefern, weswegen t keine Funktion sein darf [Ha95]. Nach der Typdeklaration folgen die die Funktion definierenden Gleichungen. Diese haben die Form f t1 … tn | c = e. Dabei ist f der Funktionsname, t1 … tn sind die eingehenden Variablen, c ist eine zu erfüllende Bedingung und e der Ausdruck, der ausgeführt wird. Hier wird der Unterschied zu reinen funktionalen Sprachen wie Haskell deutlich: Es gibt die Möglichkeit, Bedingungen, die oben schon erwähnten Constraints, anzugeben, die vor der Ausführung der Funktion erst auf Erfüllbarkeit überprüft werden müssen und die eine Lösung – so sie denn gefunden wird – liefert. Constraints sind dabei ausdrücklich von booleschen Ausdrücken zu unterscheiden, die aus Kompatibilitätsgründen mit Haskell ebenfalls nach einem | stehen dürfen [Ha06]: Boolesche Ausdrücke lassen sich zu „True“ oder „False“ reduzieren, Constraints liefern ein Ergebnis einer Gleichung, indem beide Seiten der Gleichung zu dem gleichen Ergebnis reduziert werden; sie werden später noch genauer behandelt. 2.3 Listen Listen nehmen in Curry eine besondere Stellung ein, da sie eine der häufigstbenutzten Typen in funktionalen, logischen und funktional-logischen Programmiersprachen 5 Kapitel 2: Grundlagen von Curry sind [AnH07]. Daher gibt es auch eine kürzere Syntax, um mit Listen leichter umzugehen. Eine Liste ist allgemein definiert als [AnH07]: data List a = Nil | Cons a (List a) Dies bedeutet: Eine Liste ist entweder leer oder sie besteht aus einem Element a sowie einer weiteren Teilliste „List a“. Da eine solche Schreibweise zur einfachen Benutzung von Listen allerdings eher ungeeignet ist, können Listen auch in vereinfachter Syntax als [a, b, c] geschrieben werden. Cons kann in Curry auch in Infix-Schreibweise mittels „:“ geschrieben werden, was bedeutet, dass obiges Listen-Beispiel auch „a:b:c:[]“ geschrieben werden kann. Wichtig hierbei ist, dass die letzte leere Teilliste in der KommaSchreibweise weggelassen wird [AnH07]. Mittels Listen lassen sich eine Reihe von Problemen einfacher handhaben, so auch viele Probleme mit höherwertigen Funktionen: So arbeitet z.B. die bekannte map-Funktion mit Listen, um eine bestimmte Funktion f auf eine Reihe von Werten anzuwenden – repräsentiert als Liste. Auch einige erweiterte Funktionen, wie das später besprochene „Narrowing“, kann beim Umgang mit Listen sehr hilfreich sein [AnH07]. 2.4 Pattern Matching Pattern Matching ist ein Programmierstil, der es dem Programmierer erlaubt, Funktionen in mehrere Teilregeln aufzuteilen. Dieses Verfahren erleichtert sowohl das Programmieren als auch das Verstehen des Quelltextes [AnH07]. Das Prinzip hinter Pattern Matching ist, die Funktion derart aufzuteilen, dass anhand des „Musters“, das der Funktionsaufruf enthält, entschieden werden kann, welche der Anweisungen auszuführen ist. Dadurch wird deutlicher, wann ein bestimmter Teil der Funktion aufgerufen wird, weil er links des Gleichheitszeichens steht und somit auch optisch vom Ausdruck, der bei Funktionsaufruf ausgeführt wird, getrennt ist. Ein einfaches Beispiel ist die folgende Funktion not, die boolesche Werte umkehrt [AnH07]: not :: Bool -> Bool not True = False not False = True Hier wird abhängig davon, welche Eingabe erfolgt, entweder False oder True zurückgegeben. Hier erklärt sich auch der Begriff „Pattern Matching“: Das Muster des Funkti6 Kapitel 2: Grundlagen von Curry onsaufrufs muss zum Muster des jeweiligen Funktionsteils passen, damit dieser ausgeführt wird. Diese Form der Funktion ist deutlich leichter lesbar als eine vergleichbare Funktion, die ohne Pattern Matching geschrieben wurde und mit if-Bedingungen arbeitet: not :: Bool -> Bool not x = if x==True then False else True Wie zu sehen ist, ist bei dieser Form der Funktion deutlich schwerer auf einen Blick zu erkennen, wie sich die Funktion für verschiedene Eingabeparameter verhalten wird. Dies ist bei diesem einfachen Beispiel noch nicht so sehr ausgeprägt, wird bei umfangreicheren Funktionen aber klarer. Mit Pattern Matching sieht man sofort, welche möglichen Eingabeparameter es gibt, ohne muss man dazu den rechten Teil der Funktion durchsuchen. 2.5 Lokale Definitionen Mittels lokaler Definitionen ist es möglich, den Rahmen, in dem Funktionen oder Variablen zugänglich sind, zu beschränken [AnH07]. Dies ist nützlich, wenn bestimmte Funktionen oder Variablen nur innerhalb einer anderen Funktion zugänglich sein sollen, etwa als Hilfsfunktionen bzw. -variablen. Auch können sie dazu dienen, Namenskollisionen zu vermeiden. Es gibt zwei Arten der lokalen Definitionen, mittels der Statements „let“ und „where“. 2.5.1 Lokale Definitionen mittels „let“ Mittels „let“ wird innerhalb eines Ausdrucks eine Unterebene geschaffen, innerhalb derer die darin beschriebenen Funktionen und Variablen benutzt werden können; außerhalb dieses Abschnitts sind sie nicht zugänglich. Bei der Verwendung von let wird zunächst der Ausdruck beschrieben, der von einer Funktion auf der nächst höheren Ebene benutzt wird. Dieser wird durch „in“ beendet. Danach folgt die Funktion, die den in der Unterebene beschriebenen Ausdruck benutzt. Die folgende Funktion berechnet beispielsweise das Quadrat eines Wertes und addiert ihn selbst hinzu. Die Funktion zum Quadrieren wird dabei auf einer unteren Ebene eingebaut: sqplus :: Int -> Int sqplus x = let square = x*x in (square x)+x 7 Kapitel 2: Grundlagen von Curry Die Funktion „square“ ist ausschließlich innerhalb der Funktion „sqplus“ zu sehen, außerhalb ist sie nicht zugänglich. Mittels „let“ lässt sich im Gegensatz zur Verwendung von „where“ feiner steuern, welche Funktionen und Variablen auf welcher Ebene sichtbar sind. So kann man mit „let“ auch mehrere Unterebenen erschaffen, mit „where“ dagegen nur eine [AnH07]. 2.5.2 Lokale Definitionen mittels „where“ Mittels „where“ wird ebenfalls eine Unterebene geschaffen, hier jedoch nach Definition der Funktion, nicht vorher. Setzt man das Beispiel von oben mit „where“ um, so erhält man folgende Funktion: sqplus :: Int -> Int sqplus x = (square x)+x where square = x*x Der Effekt bei diesem kleinen Beispiel ist der gleiche: „square“ ist nur innerhalb von sqplus sichtbar, nicht außerhalb. Ist es jedoch vonnöten, mehrere Ebenen zu verschachteln, so ist es besser, „let“ zu benutzen. 2.6 Nicht-Determinismus Eine wichtige Eigenschaft, die Curry aufweist, ist, dass Funktionen auch nichtdeterministisch sein können. Dies sind keine Funktionen im mathematischen Sinne, sondern sie liefern mehrere mögliche Ausgaben für eine Eingabe. Nichtdeterministische Funktionen sind aber keinesfalls ein exotischer Sonderfall, sondern werden auch als Programmiertechnik empfohlen, die es erleichtert, bestimmte Probleme zu programmieren und zu lösen [An97]. Eine einfache Funktion, die nichtdeterministisch ist, ist im folgenden Beispiel [AnH07, S. 18] gezeigt: home Ford = Beteigeuze home Ford = Earth home Trillian = Earth home VogonJeltz = Vogsphere Beim Aufruf von „home Ford“ ist nicht klar, welcher Wert zurückgeliefert wird, da sowohl der Wert „Earth“ als auch der Wert „Beteigeuze“ möglich sind. Dass dieser NichtDeterminismus aber durchaus seine Berechtigung hat und Programme einfacher gestalten kann, ist in folgendem Zusatz zu sehen: Angenommen, man möchte wissen, ob man 8 Kapitel 2: Grundlagen von Curry sich auf der Heimatwelt der Person x aufhält, wenn der aktuelle Aufenthaltsort in „location“ gespeichert ist: homeworld x | home x == location = True | otherwise = False Ohne eine nicht-deterministische Funktion wäre der Aufwand der Realisierung beider Funktionen deutlich größer: man müsste in einer Datenstruktur hinterlegen, welche Person welche Heimatwelt hat; zudem müsste die Funktion „homeworld“ deutlich aufwändiger gestaltet werden [AnH07, S. 18]. 2.7 Constraints und freie Variablen Die bisher vorgestellten Eigenschaften von Curry haben hauptsächlich Currys funktionale Eigenschaften behandelt. Curry enthält als funktional-logische Programmiersprache aber auch Eigenschaften logischer Programmiersprachen, wie etwa die Constraints und die damit in Zusammenhang stehenden freien Variablen. [Ha06] Die Idee hinter den freien Variablen ist, für sie Werte zu ermitteln, sodass ein Ausdruck auf einen Wert reduzierbar oder ein Constraint erfüllbar ist. Die Idee ist eine ähnliche wie etwa beim Lösen von Gleichungssystemen: Wähle eine oder mehrere Variablen so, dass alle Gleichungen zugleich erfüllbar sind. Die Werte für die freien Variablen, die diese Bedingung erfüllen, sind dann die Lösung. Freie Variablen finden insbesondere in den Constraints Verwendung. Dies sind Bedingungen, die zu erfüllen sind, damit etwa ein bestimmter Teilausdruck einer Funktion ausgeführt wird oder eine ganze Funktion ein Ergebnis erhält. Constraints haben die Form x =:= y, wobei x eine oder mehrere freie Variablen enthält, die so verändert werden müssen, dass x und y reduziert das gleiche Ergebnis liefern. Ein Beispiel: favoriteDrink Arthur = Tea favoriteDrink Zaphod = PanGalacticGargleBlaster Möchte man nun wissen, wessen Lieblingsgetränk Tee ist, so lautet der Constraint dazu favoriteDrink x =:= Tea. x muss nun so instantiiert werden, dass auf beiden Seiten dieses Constraints das gleiche steht. Dazu muss x auf „Arthur“ gesetzt werden. Die Lösung für x ist also „Arthur“. Näheres zum Finden dieser Lösung wird unter „Narrowing“ beschrieben. 9 Kapitel 3: Besonderheiten von Curry 3 Besonderheiten von Curry 3.1 Lazy Evaluation Ausdrücke, die rechts in Funktionen stehen, müssen auf eine gewisse Art und Weise reduziert werden, um auf das letztendliche Ergebnis zu kommen. So muss z.B. der Ausdruck square (6+6*6) mit square x = x*x auf eine bestimmte Art und Weise reduziert werden, um auf das Ergebnis 1764 zu kommen. Dafür gibt es prinzipiell mehrere Möglichkeiten: Eine Möglichkeit wäre, grundsätzlich den am weitesten rechts stehenden, innersten reduzierbaren Ausdruck (auch „Redex“, von „reducible expression“, genannt), also zunächst den Ausdruck 6*6 zu vereinfachen. Damit würde der Ausdruck zunächst zu 6+36, dann zu 42 vereinfacht. Darauf folgt dann der Ausdruck „square 42“, der ausgewertet „42*42“ ergibt, das wiederum zu 1764 vereinfacht werden kann. Allerdings hat diese Methode einige Nachteile, weswegen sie von Curry nicht verwendet wird. Das größte Problem dieser Auswertungsmethode ist, dass sie nicht für alle die Ausdrücke tatsächlich Lösungen liefert, für die das mit einer anderen Methode – etwa Lazy Evaluation – durchaus möglich wäre, das heißt, die Methode normalisiert nicht [Ha97(1)]. Ein Beispiel dazu ist z.B. folgender Ausdruck: 0+0 ≤ f für natürliche Zahlen (inkl. 0) mit f = f. Wird nun der am weitesten rechts liegende, innerste Ausdruck ausgewertet (f), so ist das Programm in einer Endlosschleife gefangen, obwohl es eine eindeutige und leicht zu ermittelnde Lösung gibt: 0 ist ≤ jede Zahl im Zahlenraum der natürlichen Zahlen. Nutzte Curry die oben genannte Strategie, widerspräche dies auch der Semantik von Curry, nach der alle Ergebnisse, die prinzipiell berechenbar sind, auch berechnet und ausgegeben werden [AnH07]. Dabei hat die obige Strategie gleich zwei Schwachstellen: Zum einen ist festgelegt, dass immer erst der rechte Teilausdruck ausgewertet wird – das kann falsch sein, wie in dem Beispiel deutlich wird. Hier wäre es besser, zunächst den linken Teilausdruck auszuwerten, da sofort True zurückgegeben werden kann, wenn dort 0 steht; dazu müsste nur 0+0 ausgewertet werden, f wäre irrelevant. Zum anderen ist auch die Auswertung des innersten möglichen Ausdrucks eine unglückliche Wahl; wertet man von außen aus, so werden bestimmte nicht-terminierende oder undefinierte Ausdrücke unter Umständen überhaupt nicht benötigt, um ein Ergebnis zu erhalten. 10 Kapitel 3: Besonderheiten von Curry Daher muss Curry neben der Wahl des äußersten möglichen Redex eine Strategie zur Auswertung von Ausdrücken nutzen, die nur die Ausdrücke reduziert, die auch tatsächlich zur weiteren Reduzierung des Ausdrucks benötigt werden. Im obigen Fall war das für f beispielsweise nicht nötig, das Ergebnis kann auch ohne diese Auswertung ermittelt werden. Um allerdings genau dies zu erreichen, muss Curry ermitteln, welcher Ausdruck zur weiteren Reduzierung denn tatsächlich ein sogenannter „needed redex“, also ein benötigter reduzierbarer Ausdruck ist, und welcher nicht. Generell kann allerdings nicht errechnet werden, welcher Redex benötigt wird und welcher nicht. Es gibt allerdings einige Unterklassen von Funktionen, bei denen dies durchaus möglich ist [Ha97(2)]. Diese Klassen von Funktionen werden „induktiv-sequenziell“ („inductive sequential“) genannt [Ha97(2)]. Für diese Unterklassen kann ein sogenannter „definitorischer Baum“ („definitional tree“) aufgebaut werden, in dem festgelegt ist, in welcher Reihenfolge Ausdrücke vereinfacht werden. Wann allerdings eine Funktion genau „induktiv-sequenziell“ genannt werden kann, ist unklar: Hanus erwähnt zum einen, dass eine Funktion genau dann induktiv-sequenziell ist, wenn ein definitorischer Baum für diese Funktion existiert [Ha97(1)], zum anderen aber auch, dass das nur dann der Fall ist, wenn der definitorische Baum kein or oder and enthält [Ha97(2)]. Ein definitorischer Baum T besteht aus Regeln und Ästen, die wiederum aus dem zu ersetzenden Ausdruck, einer Variablen, die angibt, welcher Teilausdruck dafür ausgewertet werden muss, einer Regel oder ein Ast, der angewendet werden kann, nachdem der entsprechende Teilausdruck vereinfacht wurde, sowie möglichen weiteren Regeln bzw. Ästen, die angewendet werden, falls das Muster der vorherigen Regel nicht auf den Ausdruck passt. Im obigen Beispiel könnte gefordert sein, dass zunächst die linke Seite des Ausdrucks daraufhin überprüft wird, ob sie bereits 0 ist, dann kann True zurückgegeben werden. Ist dies nicht der Fall, so wird die linke Seite ausgewertet und überprüft, ob diese Auswertung ergibt, dass auf der linken Seite 0 steht. Auch dann kann True zurückgegeben werden. Danach wird die rechte Seite ausgewertet, steht dort 0, wird False zurückgegeben, ansonsten linke Seite ≤ rechte Seite. Ein weiterer Aspekt der „Lazyness“ von Curry ist, dass einmal ausgewertete Variablen für jedes Vorkommen dieser Variablen gleichzeitig ausgewertet werden [Ha06]. Dies hat etwa bei dem Ausdruck square (6+6) mit square x = x*x den Effekt, dass (nach einem Schritt der Vereinfachung nach dem „Lazy Evaluation“-Prinzip) der Aus11 Kapitel 3: Besonderheiten von Curry druck (6+6)*(6+6) nicht etwa zu 12*(6+6), sondern zu 12*12 ausgewertet wird. Dies hat seine Ursachen im so genannten „call time choice“ [Ha06] der deklarativen Semantik: Sobald eine Variable ausgewertet wird, muss dieser ein eindeutiger Wert zugewiesen werden. Es muss beachtet werden, dass dies nicht für Funktionen gilt. Diese werden separat voneinander behandelt. [Ha06] 3.2 Zwei Wege zur Berechnung logischer Variablen Der Berechnung logischer Variablen kommt in funktional-logischen Sprachen eine besondere Rolle zuteil: Sie wird als die wichtigste Eigenschaft einer funktional-logischen Sprache überhaupt bezeichnet [AnH07]. Dazu gibt es zwei unterschiedliche Verfahren, die sehr unterschiedlich arbeiten und auch in unterschiedlichen Fällen zu Ergebnissen führen: Das so genannte „Residuation“ und das „Narrowing“, das weiterentwickelt zu „Lazy Narrowing“ und „Needed Narrowing“ wurde. Needed narrowing ist sogar die beste Methode, um induktiv-sequenzielle Curry-Programme auszuführen [AEL99]; induktiv-sequenzielle Programme sind Programme, die ausschließlich induktivsequenzielle Funktionen enthalten. Im Folgenden werden beide Verfahren vorgestellt. 3.2.1 Residuation Residuation ist ein Verfahren zur Ermittlung logischer, also freier Variablen, das folgendermaßen arbeitet: Die Funktionsaufrufe werden gleichzeitig soweit wie möglich reduziert, das heißt, Ergebnisse, die schon ermittelbar sind, werden auch ermittelt. Wenn es nun aber beispielsweise eine Funktion e gibt, deren Berechnung einen Wert v benötigt, dieser aber noch nicht vorliegt, wird die Berechnung der Funktion e pausiert [AnH07]. Diese Pausierung hat so lange Bestand, bis der Wert v beispielsweise durch eine andere Funktion f ermittelt werden kann. Ist der Wert ermittelt, wird die Berechnung der Funktion e wieder aufgenommen. Erreicht wird dieses Verhalten programmiertechnisch dadurch, dass die definitorischen Bäume (siehe Kapitel „Lazy Evaluation“) verändert werden: Alle Nicht-booleschen Funktionen werden mit der Eigenschaft „rigid“ versehen. Dies hat den Effekt, dass ein Funktionsaufruf gestoppt wird, sobald ein noch nicht vorliegender Wert einer Variable dieses Funktionsaufrufs benötigt wird. Im Gegensatz zum im nächsten Kapitel behan12 Kapitel 3: Besonderheiten von Curry delten Narrowing werden logische Variablen, deren Wert sich nicht über andere Funktionsaufrufe ermitteln lässt, beim „Residuation“ nicht auf einen passenden Wert gesetzt; kann also kein Ergebnis ermittelt werden, bricht die Berechnung der Funktion ab. Zudem werden die Funktionen beim „Residuation“ ausschließlich deterministisch gelöst [Ha97(2)], das „Narrowing“ ist dagegen ein nicht-deterministisches Verfahren [AnH07]. 3.2.2 Narrowing, Lazy Narrowing und Needed Narrowing Narrwoing ist ein Verfahren, das es ermöglicht, auch dann Ergebnisse zu ermitteln, wenn sich logische Variablen nicht durch die Pausierung und den Aufruf anderer Variablen ermitteln lassen. Es kombiniert die funktionalen und logischen Eigenschaften Currys, in dem die Verfahren zum Reduzieren von Funktionstermen und zur Instantiierung logischer Variablen eingesetzt werden [AEL97]. Vereinfacht gesagt „rät“ das Narrowing-Verfahren einen Wert für eine logische Variable, falls deren Wert nicht ermittelbar ist. Dabei wird darauf geachtet, dass der Wert, der „geraten“ wurde, derart bestimmt wird, dass die Funktion auch weiterhin berechnet werden kann [AnH07]. Durch Narrowing können einige Funktions- und Programmabläufe deutlich einfacher programmiert werden, so ist Narrowing z.B. beim Umgang mit Listen ein Mittel, um den Quelltext deutlich kürzer und übersichtlicher zu halten [AnH07]. Bei der Strategie, die beim Narrowing zum Einsatz kommt, ist es wichtig, eine „richtige“ Narrowing-Strategie zu entwickeln, die mit relativ wenigen Schritten auskommt aber dennoch alle Ergebnisse ermittelt: So haben einfache Narrowing-Strategien oft einen sehr großen Suchraum [AHL99], der auch unendlich groß sein kann [Ha97(2)]. In Hinblick auf diese Probleme wurden „Lazy Narrowing“-Strategien entwickelt, die den Suchraum deutlich verkleinern. Allerdings ist auch diese Strategie nicht optimal in Hinblick auf die Anzahl der benötigten Schritte [AEH07]. Lazy Narrowing ist eine Strategie, deren Grundidee der Idee der Lazy Evaluation ähnelt: Es wird immer der äußerste mögliche Teilterm behandelt. Narrowing an inneren Teiltermen findet nur statt, wenn es zur Berechnung benötigt wird, was anhand einer Regel festgelegt wird [AHL99]. Nach Alpuente, Hanus, Lucas und Vidal ist die Lazy-Narrowing-Strategie definiert als Funktion λlazy(t), die ein Tripel (p, R, σ) zurückgibt, wobei p angibt, welche Stelle in t durch Narrowing ermittelt werden muss, R, welche Regel dabei angewendet wird und σ, wo- 13 Kapitel 3: Besonderheiten von Curry durch die Stelle in t nach Anwendung der Regel ersetzt werden kann. Als Beispiel werden folgende Narrowing-Regeln für ≤ und natürliche Zahlen angegeben: 0 ≤ n → True; S(m) ≤ 0 → False; S(m) ≤ S(n) → m ≤ n; 0+n → n; S(m)+n → S(m+n) Soll nun die logische Variable x in der Gleichung x ≤ x+x instantiiert werden, so ergeben sich aus den drei Regeln drei Lazy-Narrowing-Schritte: x ≤ x+x →x→0True; x ≤ x+x →x→0 0 ≤ 0; x ≤ x+x →x→S(m) S(m) ≤ S(m+S(m)) Dabei liefern die Anwendung der ersten und die Anwendung der zweiten Regeln das gleiche Ergebnis: x=0 und True. Die zweite Regel ebenfalls anzuwenden ist also zur Ermittlung des Ergebnisses nicht notwendig, weswegen Lazy Narrowing auch nicht optimal im Hinblick auf die Anzahl der benötigten Schritte ist. Eine in Hinblick auf benötigte Schritte und Anzahl der ermittelten Ergebnisse optimale Strategie zum richtigen Instantiieren logischer Variablen existiert für induktivsequenzielle Funktionen [AEH07]. Diese Strategie wird „Needed Narrowing“ genannt. Needed Narrowing ist dabei die Weiterentwicklung des Lazy Narrowing, die allerdings – wie schon erwähnt – nur für eine Unterklasse aller möglichen Funktionen anwendbar ist. Dies hat seine Ursache darin, dass zur Ermittlung der benötigten Narrowing-Schritte ein definitorischer Baum benutzt wird [AHL99]. Im Vergleich zur Lazy-NarrowingFunktion weiter oben wird nun noch als Argument ein definitorischer Baum P verwendet. Der definitorische Baum des Beispiels entspricht dem Baum des Beispiels aus dem Kapitel „Lazy Evaluation“. Als Ergebnis des obigen Beispiels ergibt sich die Menge {( Λ, 0 n → True, {x → 0}), (2, S(m) + n → S(m + n), {x → S(m)})}, was den Narrowing-Schritten 1 und 3 aus obigem Lazy-Narrowing-Beispiel entspricht. Bei der Benutzung von Needed Narrowing anstelle von Lazy Narrowing ergibt sich in diesem Beispiel also schon eine Einsparung eines Schrittes. 3.3 Die operationelle Semantik Currys Die operationelle Semantik einer Programmiersprache beschreibt allgemein, wie ein Programm, als einzelne Rechenschritte aufgefasst, abläuft. Diese Rechenschritte zusammen ergeben dann die Arbeitsweise und die Bedeutung des Programms. Es ist also eine Art Abstraktion vom Quelltext hin zu dem, was „hinter“ dem Quelltext steht, dem, was das Programm macht. 14 Kapitel 3: Besonderheiten von Curry Die operationelle Semantik Currys ist stark von dem beeinflusst, was Curry ausmacht und was bereits weiter vorne besprochen wurde: Als funktional-logische Sprache auf der einen Seite das Auswerten von Ausdrücken auf Basis der „Lazy Evaluation“, auf der anderen Seite die Instantiierung freier Variablen [Ha06]. Für die Instantiierung gibt es wiederum zwei Strategien, die schon erwähnten „Residuation“ und „Needed Narrowing“. Sind keine logischen Variablen zu instantiieren, gleicht die operationelle Semantik Currys denen anderer nach dem „Lazy Evaluation“-Prinzip arbeitenden Sprachen wie etwa Haskell [Ha06]. Gilt es jedoch, Funktionen mit freien Variablen zu lösen, so ist zunächst einmal nicht klar, welches denn nun die beste operationelle Semantik für eine funktional-logische Sprache ist [HaK95]: Sowohl Residuation als auch Narrowing haben für sich betrachtet Vor- und Nachteile [HaK95]. Daher verwendet Curry eine Kombination aus Residuation und Narrowing, die im Sinne der funktionalen als auch der logischen Programmierung vollständig ist, sofern vom Programmierer nichts anders vorgegeben wird [HaK95]. Es ist für den Programmierer durchaus möglich, gewisse Restriktionen an die Berechnung der Variablen zu richten; diese sind jedoch ein Spezialfall und werden hier nicht weiter behandelt. Wie schon weiter vorne erwähnt, ist eine wichtige Eigenschaft Currys das Teilen berechneter Variablen. Besonders durch die Möglichkeit, nicht-deterministische Funktionen zu definieren, ist es wichtig zu wissen, dass ein Aufruf einer nichtdeterministischen Funktion, gespeichert in einer Variablen, innerhalb einer Funktion immer den gleichen Wert annimmt [AHH02]. Dies gilt ausdrücklich nicht für Funktionen: Möchte man also etwa für einen simulierten Münzwurf nicht nur die Werte 0 und 2 bei zweimaligem „Wurf“ erhalten, darf nicht die Funktion nur einmal, sondern sie muss zweimal aufgerufen werden. Da Curry zwei Programmierparadigmen kombiniert, bei denen zum einen der berechnete Wert von Bedeutung ist, zum anderen die Antwort auf einen Constraint, wird für Curry ein „Antwort-Ausdruck“ („answer expression“) von der Form „σ e“ definiert, wobei σ der bisher errechneten Antwort und e dem Ausdruck entspricht [Ha06]. Dieser Antwort-Ausdruck gilt als gelöst, falls e ein Datentyp ist [Ha06]. Da bei der Instantiierung freier Variablen durchaus mehrere Lösungsmöglichkeiten existieren können, kann die Reduzierung eines Antwort-Ausdrucks durchaus eine (Multi-)Menge von der Form {σ1 e1, σ2 e2,…, σn en} zur Folge haben. Diese Ausdrücke werden normalerweise in der Form {x=6, y=7} 42 | {x=1, y=17} 17 geschrieben [Ha06]. 15 Kapitel 3: Besonderheiten von Curry Ein Rechenschritt ist nun die Reduzierung genau eines nicht gelösten Ausdrucks, d.h., auch bei mehreren Lösungsmöglichkeiten wird jeweils nur eine vereinfacht. Falls dieser Schritt eindeutig, also deterministisch ist, wird der reduzierte Ausdruck durch genau einen neuen Ausdruck ersetzt. Ist der Schritt nicht-deterministisch, wird der Ausdruck durch alle möglichen Reduzierungen des Ausdrucks ersetzt [Ha06]. Bei der Ermittlung der Werte freier Variablen geht Curry nach folgendem Muster vor: Wird der Wert einer freien Variablen von der linken Seite einer definierten Funktion erfordert, wird das Narrowing eingesetzt, also die freie Variable auf einen „passenden“ Wert instantiiert. Ist dem nicht so, wird die Reduzierung dieses Ausdrucks zunächst pausiert, was dem „Residuation“ entspricht [Ha06]. Die Instantiierung kann allerdings auch durch den Operator „=:=“ ausgelöst werden, dann wird kein Residuation verwendet [Ha06]. Arithmetische Operatoren wie „*“ oder „+“ wenden dagegen stets Residuation an [Ha06]. 3.4 Anwendungsgebiete Currys Curry wird, wie viele deklarativen Sprachen, weit öfter in Forschung Lehre als in der Praxis eingesetzt. Das hat zum einen den Grund, dass sich deklarative (und insbesondere funktionale) Sprachen meist leichter formalisieren lassen, zum anderen aber auch, dass deklarative Sprachen in der Praxis keinen „guten Ruf“ genießen. Dies hat unter anderem damit zu tun, dass der Ansatz deklarativer Sprachen, nicht zu beschrieben, wie etwas getan werden soll, sondern was getan werden soll, zwar dazu führt, dass der Quelltext im Vergleich zu imperativen oder objektorientierten Sprachen wie C oder Java zwar meist deutlich kürzer ausfällt, dafür aber die Performance der Programme schwierig bis gar nicht zu beeinflussen ist. Eine Optimierung des Algorithmus’, der zur Lösung verwendet wird, ist eben auch deswegen nicht möglich, weil er bei deklarativer Programmierung nicht im Vordergrund steht. Dies heißt aber nicht, dass gar keine Anwendungsgebiete für Curry in der Praxis bestehen. So gibt es beispielsweise die Möglichkeit, Web-Server mittels Curry zu programmieren [Ha01]. Der Vorteil dieses Vorgehens besteht darin, dass durch den deklarativen Programmieransatz weniger „typische“ Fehler der Programmierung etwa mittels CGI entstehen. Tatsächlich wird bei diesem Anwendungsbeispiel auch CGI benutzt, allerdings nur insoweit, dass die Verwendung Currys auf CGI aufsetzt, also eine Abstraktionsebene hinzufügt [Ha01]. Diese Abstraktion findet auch auf der Ebene der HTML-Seiten statt: Statt – wie etwa bei mit 16 Kapitel 3: Besonderheiten von Curry Perl geschriebenen CGI-Scripten – wird der Quelltext von HTML-Dokumenten nicht vom Programmierer selbst geschrieben, was zu Syntaxfehlern wie etwa fehlenden Tags führen kann, sondern mit Curry umgesetzt, sodass nur spezifiziert werden muss, welches HTML-Element mit welchem Inhalt an welche Stelle im Dokument gesetzt werden muss [Ha01]. Die Übersetzung in HTML übernimmt eine Wrapper-Klasse. Auch erweitere Funktionen wie Input-Felder und der Zugang auf den Webserver mittels Curry wird in diesem Anwendungsbeispiel umgesetzt [Ha01]. Ein gänzlich anderer Ansatz ist die Programmierung selbstständiger Roboter mittels Curry [HaH02]. Damit soll ein Experiment durchgeführt werden, das die Nutzung höherer Programmierparadigmen, in diesem Fall deklarative Programmierung, für die Programmierung eingebetteter Systeme, wie es Roboter sind, zum Inhalt hat [HaH02]. Die Gründe dafür sind vielseitig: Zum einen werden laut den Autoren eingebettete Systeme im Vergleich zu herkömmlichen PC immer wichtiger, zum anderen ist es ein Experiment, um das Vorurteil, deklarative Programmiersprachen seien nicht für Systeme, die auf Signale von außerhalb des Systems reagieren, geeignet. Ein solches System liegt bei einem selbstständigen Roboter vor [HaH02]. Zudem möchten die Autoren einen Beitrag leisten, Sprachen wie Curry mehr in der Praxis einzusetzen. Tatsächlich wird Curry hauptsächlich in der Forschung und der Lehre eingesetzt. Hier nimmt es insbesondere in der Lehre eine gewisse Sonderstellung ein, da mittels Curry sowohl das funktionale als auch das logische Programmierparadigma vermittelt werden können, ohne, dass beim Wechsel vom einem zum anderen auch gleich eine völlig neue Syntax einer anderen Sprache (wie etwa Haskell und Prolog) erlernt werden müsste, was nicht dem Verständnis der Paradigmen dient. Zudem werden so die Zusammenhänge zwischen beiden Paradigmen deutlich, die durchaus gegeben sind [Ha97(3)]. Mit Curry kann erreicht werden, dass logische Programmierung als Erweiterung der funktionalen Programmierung aufgefasst wird, genau so, wie Curry der Syntax und Semantik einer funktionalen Sprache wie Haskell ähnlich ist, diese aber durch die Einbringung freier Variablen und den damit verbundenen Eigenschaften erweitert [Ha97(3)]. Hanus führt aus, dass die Lehre der deklarativen Programmierparadigmen heute meist zweigeteilt ist, zum einen in die funktionale, zum anderen in die logische Programmierung. Curry sei die beste Wahl, um beide Paradigmen zu lehren [Ha97(3)]. Zusammenfassend lässt sich sagen, dass Curry sich zwar als Sprache der Forschung und der Lehre sehr gut eignet, insbesondere, um verschiedene Aspekte der deklarativen Pro17 Kapitel 3: Besonderheiten von Curry grammierung darzustellen [Ha97(3)], in der Praxis aber wenig Relevanz besitzt. Dies gilt allgemein für deklarative Sprachen. Warum dem so ist, kann abschließend nicht beurteilt werden; eventuell ist die Hemmschwelle zur Umgewöhnung von imperativen und objektorientierten Programmiersprachen im Vergleich zum erwarteten Nutzen zu gering, um tatsächlich großflächig eingesetzt zu werden. 3.5 Beispielprogramm: „Send more money“ „Send more money“ ist das klassische Beispiel für ein Kryptogramm. Ein Kryptogramm ist ein mathematisches Rätsel, bei dem es gilt, in einem Gleichungssystem, dessen Zahlen ziffernweise durch Buchstaben ersetzt wurden, den Wert jedes einzelnen Buchstabens zu ermitteln. In diesem klassischen Beispiel lautet die Gleichung „SEND+MORE=MONEY“. Jeder einzelne Buchstabe steht dabei für eine Ziffer von 0 bis 9. Gleiche Buchstaben bedeuten gleiche Ziffern, aber auch umgekehrt können zwei Buchstaben nicht die gleichen Ziffern repräsentieren. Zudem dürfen die vorne stehenden Buchstaben nicht 0 sein. Die Lösung für das obige Problem ist S=9, E=5, N=6, D=7, M=1, O=0, R=8 und Y=2, damit ergibt sich die Gleichung 9567+1085=10652. In Curry ist dieses Problem mit relativ wenig Quelltext zu lösen. Das ergibt sich aus den Programmierparadigmen, die Curry zugrunde liegen: Da Curry eine funktional-logische Programmiersprache ist, lassen sich die Bedingungen, die das Problem enthält, als Constraints formulieren. Die einzelnen Bedingungen lauten: 1000*S+100*E+10*N+D+1000*M+100*O+10*R+E=10.000*M+1000*O+100*N+ 10*E+Y; alle Variablen müssen einen unterschiedlichen Wert zwischen 0 und 9 annehmen; S, M ≠ 0. Um die Bedingung, dass alle Variablen einen unterschiedlichen Wert annehmen müssen, einfach zu erfüllen, wird eine Funktion diff erzeugt, die zwei Variablen so instantiiert, dass beide voneinander unterschiedlich sind: diff :: Int -> Int -> Success diff x y = (x/=y)=:=True Zudem wird mit einer Funktion sichergestellt, dass jede Variable einen Wert zwischen 0 und 9 einnimmt: 18 Kapitel 4: Fazit digit :: Int -> Success digit x = (x<=9)=:=True & (x>=0)=:=True Die anderen Constraints werden in der Hauptfunktion, die auch das Ergebnis liefert, erfüllt: sendmoremoney :: Success sendmoremoney = let s,e,n,d,m,o,r,y free in (1000*s+100*e+10*n+d+1000*m+100*o+10*r+e) =:=(10000*m+1000+*o+100*n+10*e+y) & digit s & digit e & digit n & digit d & digit m & digit o & digit r & digit y & diff s e & diff s n & diff s d & diff s m & diff s o & diff s r & diff s y & diff e n & diff e d & diff e m & diff e o & diff e r & diff e y & diff n d & diff n m & diff n o & diff n r & diff n y & diff d m & diff d o & diff d r & diff d y & diff m o & diff m r & diff m y & diff o r & diff o y & diff r y Eine andere Möglichkeit, das Problem zu lösen, wäre etwa statt des zusätzlichen Constraints, dass jede Variable zwischen 0 und 9 liegen muss, einen eigenen Typ „Digit“ zu erzeugen, der die Werte von 0 bis 9 annehmen kann. 4 Fazit Curry ist in einiger Hinsicht eine besondere Sprache: Es verbindet innerhalb der deklarativen Programmierung die Programmierparadigmen der funktionalen und logischen Programmierung, die vom Grundgedanken her relativ unterschiedlich sind, sich aber dennoch kombinieren lassen – das zeigt Curry. Curry verbindet so unterschiedliche Gedanken wie das „Lazy Evaluation“ und die deterministische Ermittlung von Funktionsergebnissen aus den funktionalen Sprachen mit nicht-deterministischer Suche nach zu instantiierenden freien Variablen aus logischen Sprachen und zeigt, dass diese Kombination doch mehr ist als die Summe seiner Teile, da sie Programmierstile ermöglicht, die in rein funktionalen oder rein logischen Sprachen nicht möglich sind. Curry ist seiner Abstammung als deklarative und insbesondere als funktionale Sprache sehr gut formal beschreibbar, was den Effekt hat, dass sich die Korrektheit ganzer Programme 19 Kapitel 4: Fazit wesentlich leichter überprüfen lässt als etwa bei imperativen oder objektorientierten Sprachen. Allerdings hat Curry in der Praxis nur wenig Bedeutung, hier herrschen immer noch die genannten Programmierparadigmen vor – zum einen sicherlich aufgrund von der fehlenden Performance, die einige Curry-Programme auszeichnet. Zum anderen sieht der Praktiker wohl keine Vorteile darin, seine Programme auf Korrektheit zu überprüfen oder in einer formal einfach beschreibbaren Sprache zu programmieren. Dies ist z.T. durchaus verständlich, da diese Dinge in der Praxis tatsächlich nicht so relevant sind. Ein Programm, dass „allem Anschein nach“ korrekt funktioniert, ist meist ausreichend. Curry ist nach eigener Beurteilung eine sehr interessante Sprache. Einige Programmierprobleme lassen sich mittels Curry sehr elegant und „knackig“ formulieren, was nicht nur die Übersichtlichkeit erhöht, sondern auch die Gefahr, Fehler zu machen, vermindert. Allerdings ist die Umgewöhnung vom „gewohnten“ imperativen bzw. objektorientierten Programmierstil doch recht schwer und viele Eigenschaften des deklarativen Programmierens zunächst ungewohnt oder sogar ungewollt – man muss umdenken, will man deklarativ programmieren. Das ist der Nachteil der kurzen Formulierung von Programmen in Curry: Insbesondere bei häufiger Verwendung erweiterter Programmiertechniken kann das Verstehen des Quelltextes für nicht geübte Programmierer schwer fallen, tendenziell vermutlich schwerer als bei niederen Programmiersprachen. Curry wird nach eigener Einschätzung wohl eine „Nischensprache“ bleiben. Falls nicht doch die Vorteile deklarativer Programmiersprachen in der Praxis relevanter werden sollten, dürften die niederen Programmiersprachen für fast alle Anwendungszwecke „gut genug“ sein, zumal praxisrelevantere Gebiete wie Performance-Optimierung in Curry nur schwer oder nur über die Änderung der Arbeitsweise des Compilers möglich sind. Als Sprache für Forschung und Lehre ist sie jedoch weiterhin gut geeignet, wenn formale Aspekte im Vordergrund stehen. Wo und ob eine Weiterentwicklung Currys möglich ist, vermag der Autor nicht zu sagen. Diese Weiterentwicklungen werden nach eigener Einschätzungen eher selten die Sprache erweitern, sondern eher „unter der Haube“ stattfinden, da hier die Algorithmen wie etwa zum Narrowing deutlich verbessert werden können, was mit der Entwicklung des „Needed Narrowing“ gezeigt wurde. 20 Literaturverzeichnis Literaturverzeichnis [AEH07] Sergio Antoy, Rachid Echahed, Michael Hanus: A Needed Narrowing Strategy, Journal of the ACM (ACM) 47 (4), S. 776-822, 2007. [AEL99] María Alpuente, S. Escobar, Salvador Lucas: Incremental Needed Narrowing, Proc. of the International Workshop on Implementation of Declarative Languages (IDL’99), 1999. [AHH02] Elvira Albert, Michael Hanus, Frank Huch, Javier Oliver, Germán Vidal: An Operational Semantics for Declarative Multi-Paradigm Languages, Proc. of the 11th International Workshop on Functional and (Constraint) Logic Programming (WFLP 2002), S. 7-20, 2002. [AHL99] María Alpuente, Michael Hanus, Salvador Lucas, Germán Vidal: Specialization of Inductively Sequential Functional Logic Programs, Proc. of the International Conference on Functional Programming (ICFP'99), ACM Press, S. 273-283, 1999. [An97] Sergio Antoy: Optimal Non-Deterministic Functional Logic Computations, Proc. International Conference on Algebraic and Logic Programming (ALP’97), S. unbekannt, 1997. [AnH07] Sergio Antoy, Michael Hanus: Curry: A Tutorial Introduction, 2007. [Ha97(1)] Michael Hanus: A Unified Computation Model for Declarative Programming, Joint Conference on Declarative Programming, S. 9-24, 1997. [Ha97(2)] Michael Hanus: A Unified Computation Model for Functional and Logic Programming, Proc. of the 24th Annual SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL’97), ACM Press, S. 80-93, 1997. [Ha97(3)] Michael Hanus: Teaching Functional and Logic Programming with a Single Computation Model, Proc. Ninth International Symposium on Programming Languages, Implementations, Logics, and Programs (PLILP'97), Springer, S. 335-350, 1997 [Ha01] Michael Hanus: High-Level Server Side Web Scripting in Curry, Proc. of the Third International Symposium on Practical Aspects of Declarative Languages (PADL'01), Springer, S. 76-92, 2001. [Ha06] Michael Hanus: Curry: An Integrated Functional Logic Language, 2006. [HaH02] Michael Hanus, Klaus Höppner: Programming Autonomous Robots in Curry, Proc. 11th International Workshop on Functional and (Constraint) Logic Programming (WFLP 2002), S. 89-102, 2002 [HaK95] Michael Hanus, Herbert Kuchen: Curry: A Truly Functional Logic Language, Proc. ILPS’95 Workshop on Visions for the Future of Logic Programming, S. 95-107, 1995.