Sebastian Luhn - Universität Münster

Werbung
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.
Herunterladen