1 Seminar Programmiersprachen Curry Autor: Björn Weinbrenner Betreuer: Prof. Dr. Herbert Kuchen 2 Inhaltsverzeichnis 1. Allgemeines 2. Typsystem 3. Funktionen 4. Suche 5. I/O 6. Quick Sort 7. Fazit 3 1. Allgemeines Einordnung • deklarative Programmiersprache • funktional-logische Programmiersprache • Programm beschreiben das Problem, nicht den Lösungsweg • Die Lösungsfindung ist Aufgabe des Computers 4 Funktionale Programmiersprachen • basieren auf dem Funktionsbegriff der Mathematik • Ein Ausdruck besteht aus verschachtelten Funktionsaufrufen • Ausdrücke einer funktionalen Sprache werden deterministisch reduziert • Am Ende der Berechnung steht ein Ausdruck, der nur Konstruktoren enthält • keine Schleifen • Häufige Verwendung rekursiver Funktionen • Funktionen können höherer Ordnung sein 5 Logische Programmierung • Ausdrücke sind Suchziele, die Unbekannte enthalten. • Das Ergebnis der Berechnung sind Bindungen für diese Unbekannten • Die Berechnung selbst ist die Suche nach diesen Bindungen • Dabei führen Ausdrücke ggf. zu nichtdetermistischen Berechnungen 6 Funktional-logische Sprachen • vereinen die Eigenschaften der beiden Programmierparadigmen • vereinendes Sprachkonstrukt sind sog. Constraints (Randbedingungen) • Auswertung dieser Constraints durch zwei wichtige operationale Prinzipien: Residuation und Narrowing • Anwendung je nach Art der Funktionen, die innerhalb des Constraints aufgerufen werden 7 Entstehungsgeschichte der Sprache • Benennung nach dem Logiker Haskell B. Curry, nach dem auch die funktionale Sprache Haskell benannt ist • Curry ist praktisch eine Erweiterung der Sprache Haskell • Entwicklung durch internationale Initiative von Hochschulen • Erfolgreiche Nutzung im Bereich der Hochschullehre, um logische und funktionale Programmierung mit einer Sprache zu lehren • Webseite mit praktisch allen Informationen zum Thema Curry: http://www.informatik.uni-kiel.de/~curry/ • Grundlagendokumente für Programmierer und Softwareentwickler 8 Entwicklungsumgebung • Siehe "Report on Curry" [Hanus] • interaktive Programmumgebung in Form eines Kommandozeileninterpreters • Programmdateien können geladen werden • Keine direkte Ausführung, wie in anderen Sprachen üblich • Stattdessen Benutzereingabe eines Ausdrucks über die Kommandozeile • Auswertung mit Hilfe der Definitionen in den geladenen Dateien 9 Implementierungen • Es gibt wenige Implementierungen der Sprache Curry • Bekannteste und am weitesten entwickelte Implementierung ist das PAKCS (Portland Aachen Kiel Curry System) • Münster Curry von Wolfgang Lux ist die schnellste Curry-Umgebung basierend auf C • Implementierungen enthalten Cross-Compiler nach PROLOG, C oder Java, weshalb ist u.U. ein weiterer Compiler benötigt wird • Es gibt keine Implementierung für Windows 10 Syntax • Die Syntax der Sprache entspricht weitesgehend der Syntax von Haskell • Layout-Regel: Relevanz, wie weit eine Zeile eingerückt ist • Literative Programmierung möglich 11 Literative Programmierung • "Normale" Schreibweise *.curry-Datei -- Jetzt kommt eine Funktion f x = x * x • literative Schreibweise in *.lcurry-Datei <i>Jetzt kommt eine Funktion</i> <pre> > f x = x * x </pre> 12 Curry-Programme • Menge von Typdeklarationen und Funktionsdefinitionen 13 Beispiel: data Bool = True | False and :: Bool -> Bool -> Bool and True y = y and False _ = False or :: Bool -> Bool -> Bool or True _ = True or False y = y not :: Bool -> Bool not True = False not False = True 14 Benutzeranfragen • Der Benutzer stellt nun Fragen in Form von Ausdrücken an das System: and (or True False) (and False True) • Auswertung zu false • Nur Nutzung des funktionalen Teils der Sprache 15 Benutzeranfragen and (or True False) (and X True) =:= True • Die Ausgabe im PAKCS lautet darauf: Free variables in goal: X Result: success Bindings: X=True ? • Eingegebener Ausdruck ist ein Contraint • Nutzung des logischen Teils der Sprache • Auswertung zu success, wenn Bindungen für freien Variable gefunden, werden, so dass die Constraint-Gleichung erfüllt ist. 16 2. Typsystem • Verwendung eines Hindley-Milner-ähnliches polymorphes Typsystem. • Typnotationen sind nicht erforderlich, solange der Compiler alle fehlenden Typen aus dem Kontext des Programms rekonstruieren kann not :: Bool -> Bool (nicht erforderlich) not True = False not False = True • Viele Fehler können daher beim Kompilieren festgestellt werden. 17 Typsystem • Jedes Objekt hat einen eindeutigen Typ • Existenz eines allgemeinen Typs, der durch eine Typvariable ausgedrückt wird • Beispiel Identitätsfunktion: id :: a -> a id x = x • Anwendung auf alle Typen möglich • Der Rückgabewert entspricht dem dem Typ des übergebenen Parameters 18 Polymorphismus • Das beschriebene Konstrukt wird parametrischer Polymorphismus genannt. • Keine Unterstützung von Ad-hoc-Polymorphismus (Überladen von Funktionen) • Die Funktion (+) kann z.B. nur auf Integerwerte angewandt werden • Für Float-Werte muss eine andere Funktion definiert sein. 19 Typdeklaration • Es gibt viele Möglichkeiten, eigene Typen zu deklarieren • Beispiel: data Bool = True | False • Bool ist dabei der Typkonstruktor, True und False sind Datenkonstruktoren 20 Rekursive Typdeklaration • Es ist erlaubt Typen rekursiv zu definieren • Eine Liste vom Typ a ist dann folgendermaßen definiert: data list a = nil | cons a (list a) • Eine Liste ist eine leere Liste oder ein Kopfelement gefolgt von einer Liste, cons fügt ein einzelnes Element und eine Liste zusammen. • Es lassen sich in Curry mit wenig Code komplexe Datenstrukturen (z.B. Bäume) definieren. 21 Standardtypen / Listen • Standardtypen: Bool, Int, Float, Char, String. • Listen sind geordnete Zusammenstellungen von Daten ähnlich zu Arrays • Die Elemente einer Liste haben alle den selben Typ • Listen sind rekursiv definiert, ähnlich wie im obigen Beispiel • Statt list a einfachere Schreibweise [a] für Liste vom Typ a • type String = [Char] (Synonymdefinition) 22 Listen [] leere Liste x:xs Liste mit Head x und Tail xs [1,2,3,4,5] Aufzählung der Glieder [1..15] arithmetische Sequenz [1..] unendliche Liste [1,3..] Liste ungerader Zahlen [x * x | x <- [0..]] Liste von Quadratzahlen [x | x <- [0..], isPrim x] Liste von Primzahlen 23 Funktionen • Typ einer Funktion wird aus den Typen der Parameter und dem Typ des Funktionswertes komponiert • Beispiel: add :: Int -> Int -> Int • Diese Notation ist eine andere Abkürzung für add :: Int -> (Int -> Int) 24 Partielle Anwendung • Wenn eine Funktion mit weniger Parametern aufgerufen wird als Ihr Typ erwartet, wird sie wiederum zu einer Funktion ausgewertet • Beispiel: succ :: Int -> Int succ = add 1 • add wird nur mit einem statt mit zwei Parametern aufgerufen • Der Ausdruck add 1 ist daher eine Funktion vom Typ Int -> Int 25 Constraints (Randbedingungen) • Cointraint sind vom einelementigen Typ Success • Den einzigen Wert, den dieser Typ bereitstellt, ist der Konstruktor success • Dieser wird zurückgegeben, wenn eine Constraint-Gleichung durch Bindung der auftretenden freien Variablen erfüllt ist. 26 3. Funktionen • Ausdrücke mit einem Namen • können Parameter haben • Wiederverwendbarkeit • Möglichkeit, ein Problem in kleinere Probleme zu gliedern • Definition durch eine oder mehrere Funktionsgleichungen 27 Beispiel: quadrat :: Int -> Int quadrat x = x * x • Funktionsaufruf durch Aneinanderreihung des Funktionsnamens und seiner Parameter quadrat 2 28 Pattern-Matching • Eine Funktionsgleichung kann auf der linken Seite statt Variablen auch Konstruktoren haben • Beispiel der Funktion not: not True = False not False = True • Beim Aufruf der Funktion wird für jede Funktionsgleichung geprüft, ob das "Parameter-Muster" auf den Funktionsaufruf zutrifft • Jede passende Gleichung wird dann angewandt 29 Pattern-Matching • Sind die Parameter Listen, können sie auch in Doppelpunktnotation geschrieben werden, so dass direkt auf head und tail zugegriffen werden kann head :: [a] -> a head (x:xs) = x 30 Nicht-Determinismus • Werden mehr als eine Funktionsgleichung angewandt, ist die Funktion nicht deterministisch • Sie folgt dann nicht mehr dem Funktionsbegriff der Mathematik • Wird eine nichtdetermistische Funktion aufgerufen, die mehr als einen Rückgabewert hat, wird nacheinander jede Lösung ausgegeben. 31 Beispiel coin = 0 coin = 1 • Wird coin aufgerufen, werden nacheinander die Lösungen 0 und 1 ausgegeben • 2 * coin führt zu den Lösungen 0, 2 • coin + coin führt zu 0, 1, 1, 2 32 Vordefinierte Operatoren und Funktionen • Zuweisungsoperator, arithmetische Operatoren, Vergleichsoperatoren, Boolsche Operatoren • Constraint-Gleichheitsoperator (=:=) • Alle Operatoren sind auch Funktionen, d.h. alle erwähnten Eigenschaften von Funktionen treffen auch auf die Operatoren zu • Sind die Operatoren geklammert, gilt die gewohnte Präfixschreibweise von Funktionen • Beispiele: (+) 2 3 oder (==) x True 33 Einige praktische Operationen auf Listen • Zugriff auf das Kopfelement • Länge der Liste • die Umkehrung der Liste • das Zusammenfügen zweier Listen. 34 Konditionale Ausdrücke • if_then_else bedingung ausdruck1 ausdruck2 • if bedingung then ausdruck1 else ausdruck2 • Guards: max x y | x > y = x |otherwise = y • Sukzessive Überprüfung aller Bedingungen bis die erste Bedingung zutrifft • otherwise (True) leitet "default case" ein 35 Rekursive Beispiel • Fakultätsfunktion fak n | n == 0 = 1 fak n | n >= 1 = n * (fak (n-1)) • Fibonacci-Funktion fib n | n == 0 = 0 fib n | n == 1 = 1 fib n | n >= 2 = (fib (n-2)) + (fib (n-1)) 36 Funktionen höherer Ordnung • Funktionen die als Parameter oder Funktionswert wiederum Funktionen haben • Beispiel: map wendet eine Funktion auf jedes Glied einer Liste an map :: (a -> b) -> [a] -> [b] map f (x:xs) = (f x) : (map f xs) • Die Funktion f wird als Parameter übergeben und kann innerhalb von map verwendet werden • map succ [0,1,2] wird z.B. zu [1,2,3] ausgewertet. 37 Lokale Variable und lokale Funktionen • Jede Funktion oder Variable hat implizit einen Sichtbarkeitsbereich, in dem über ihren Bezeichner auf sie zugegriffen werden kann. • Funktionsdefinitionen auf oberster Ebene für alle anderen Funktionen sichtbar • Eingrenzung der Sichtbarkeit durch Definition lokaler Variable oder Funktionen • Lokale Variable ist Funktion ohne Parameter und wird einmalig berechnet • Zwei Konstrukte: let ... in ... und where 38 let ... in ... • Deklaration lokaler Variable und Funktionen im let-Teil • Verwendung im in-Teil • Beispiel: let x = 3 in x * x • Verwendung in allen Ausdrücken 39 where • Verwendung in Funktionsdefinitionen • Dem Schlüsselwort where folgen die lokalen Definitionen • Beispiel: f = x * x where x = 3 40 Constraints • Ähnliche Notation wie bei Guards möglich • Beispiel: last list | l ++ [x] =:= list = x where x, l free • Suche nach Werten für freie Variable x und l, so dass ConstraintGleichung erfüllen ist • x und l werden an Werte gebunden und können auf der rechten Seite der Funktionsgleichung verwendet werden. 41 Verzögerte Auswertung (lazy evaluation) • Ausdrücke werden in Curry erst dann berechnet, wenn ihr Wert benötigt wird • Das Gegenteil ist in vielen Sprachen der Fall, wenn Argumente einer Funktion berechnet werden, bevor die Funktion ausgeführt wird • In Curry kann eine unendlich lange Liste an eine Funktion übergeben werden, ohne dass versucht wird, diese Liste vollständig zu berechnen • Es werden nur die Elemente der Liste extrahiert, die benötigt werden. 42 Beispiel liste = [1..] head(x:xs) = x • Der Aufruf head liste kann in Curry berechnet werden. 43 4. Suche 44 Logische Variable • Funktionen können unbekannte Variable beinhalten, wenn diese als free deklariert sind und in einem Constraint enthalten sind • Beispiel: last list | l ++ [x] =:= list = x where x, l free • Es wird versucht die Unbekannten an Werte zu binden, die die ConstraintGleichheit (=:=) erfüllen • Gelingt dies, kann der Aufruf weiter berechnet werden • Andernfalls wird die Berechnung abgebrochen mit der Meldung, dass das Suchziel suspendiert wurde 45 Gekapselte Suche • Weitere Möglichkeit, nach Unbekannten zu suchen • Sie wird durch einige im Standard enthaltene Funktionen ermöglicht • Nicht alle diese Funktionen sind in PAKCS implementiert sind • Vorstellung einer dieser Funktionen: findall :: (a -> Success) -> [a] 46 findall • Erwarteter Parameter ist vom Typ (a -> Success), also eine Funktion von einem Typ a zum Typ Success • Eine solche Funktion definiert das Suchziel • findall gibt eine Liste zurück, die alle Werte enthält, die an a gebunden den Constraint erfüllen • Beispiel: goal :: (Bool -> Success) goal x = (x || True) =:= True • Der Aufruf findall goal wird zu [True,False] ausgewertet. 47 Gekapselte Suche • Suchvorgang findet in einem gekapselten Ausdruck und nicht global statt • Globale Suche ist in manchen Fällen nicht effizient, da sie in der Regel nach dem Backtracking-Prinzip funktioniert • Mit der gekapselten Suche kann mehr Einfluss auf die Art der Suche genommen werden • Diese Möglichkeiten werden allerdings in den aktuellen Implementierungen noch unzureichend angeboten 48 Suchprinzipien • Curry unterstützt die zwei wichtigsten Prinzipien, um Lösungen für logische Variable zu finden: Residuation und Narrowing 49 Residuation • Können freie Variable nicht direkt gebunden werden, werden erst alle anderen Ausdrücke, aus denen eine Bindung entstehen könnte, ausgewertet • Erst wenn die Variable gebunden ist, wird mit der Auswertung fortgefahren. • Ist die freie Variable nicht gebunden und alle anderen Ausdrücke sind berechnet oder warten auch auf eine Bindung, wird die Berechnung abgebrochen, das Suchziel wird "suspendiert". 50 Narrowing • Unter Verwendung des Narrowing-Prinzips wird versucht, die Bindung der freien Variablen aus dem Ausdruck herzuleiten • Diese Vorgehensweise entspricht dem Auflösen einer mathematischen Gleichung, auch wenn das in Curry nicht ohne weiteres möglich ist 51 Residuation oder Narrowing • Welche Strategie angewendet wird, hängt von der Art der Funktionen ab, in denen die freien Variablen auftreten • Arithmetische Operationen sind sog. starre Funktionen (rigid), die immer dem Residuation-Prinzip folgen • Eine Berechnung der folgenden Art führt zu einer Suspendierung: nullstellen a b c | a*x*x + b*x + c =:= 0 = x where x free • Andere Funktionen sind flexibel (flexible) 52 • Der Operator ++, der zwei Listen zusammenfügt, gehört dazu, so dass die oben definierte Funktion last nach dem Narrowing-Prinzip berechnet wird 53 Beispiel Narrowing and True y = y and False _ = False or True _ = True or False y = y f | and (or True False) (and x True) =:= True = x where x free • f führt zu True 54 Beispiel Residuation • Berechnung von Nullstellen • Prämisse: x ist Ganzzahl zwischen -20 und 20 each (x:xs) = x each (x:xs) = each xs generator = each [-20..20] nullstellen a b c | a*x*x + b*x + c =:= 0 & x =:= generator = x where x free • nullstellen 1 0 (-1) führt zu -1 und 1 55 5. Input/Output • Unter Verwendung der bisher erläuterten Sprachmittel treten keine Seiteneffekte auf • Das bedeutet, dass bei Aufruf einer Funktion Objekte außerhalb der Funktion nicht verändert werden • Diese Tatsache ist charakteristisch für rein funktionale Sprachen • Eingabe- und Ausgabefunktionen müssen daher besonders behandelt werden, weil diese Seiteneffekte z.B. beim Schreiben einer Datei auftreten würden 56 Monaden • Die Lösung des Problems wird in Curry durch die Verwendung sog. Monaden geleistet • Monaden sind nicht die I/O-Operationen selbst sondern Objekte, die I/OOperationen (Aktionen) bereitstellen • Zur Laufzeit werden diese Aktionen dann auf die Welt außerhalb des Programms angewandt • Das Problem der Seiteneffekte tritt dann außerhalb des funktionalen Gerüsts der Sprache aus, so dass die Grundsätze der funktionalen Programmierung weiterhin gelten können • Es bestehen einige Einschränkungen bezüglich der I/O-Operationen 57 • Da Nichtterminismus in Zusammenhang mit Ein- und Ausgabe zu Schwierigkeiten führen kann, da z.B. der Ausführungszeitpunkt der Operationen nicht vorhersagbar ist, ist die Verwendung von I/OOperationen nicht innerhalb von Funktionen erlaubt, die nicht selbst monadisch sind • Innerhalb von monadischen Funktionen müssen nichtterministische Ausdrücke gekapselt werden, z.B. durch eine gekapselte Suchfunktion 58 6. Quicksort • Vergleich Curry und Java am Beispiel Quicksort • Vergleich von Implementierung und Ausführung in Curry und Java • Vergleich bzgl. Dauer der Implementierung, Anzahl der Zeilen des Programms und Ausführungszeit • Gewählten Entwicklungsumgebungen o Java 2 Platform Standard Edition, Version 1.4.2 o PAKCS Version 1.6.0 o Münster Curry 0.9.6 59 Eingabe der Daten • Daten in Datei mit 10.000 Zufalls-Integerzahlen • Sortieren und Speichern in zweiter Datei • Lese- und Schreib-Operationen unberücksichtigt bei Vergleich der Implementierungszeit und Code-Länge 60 Vergleich • Signifikante Unterschiede in alle drei betrachteten Größen • Implementierungszeit betrug in Curry weniger als 5 Minuten und in Java 30 Minuten. • Das Curry-Programm lässt sich in zwei Codezeilen (im Folgenden auf 4 Zeilen verteilt) ausdrücken, während das Java Programm 20 Zeilen einnimmt. 61 Das Curry-Programm: quickSort [] = [] quickSort (x:xs) = (quickSort (filter (<= x) xs) ++ [x] ++ (quickSort (filter (> x) xs) • Die filter-Funktion ist standardmäßig definiert und filtert eine Liste unter Verwendung eines Prädikates 62 Ausführungszeit (Lesen, Sortieren und Schreiben) • PAKCS ca. 16 Sekunden • Münster ca. 1,5 Sekunden • Java weniger als 1 Sekunde 63 7. Fazit Vergleich • Vergleich zeigt Grundsatz deklarativer Sprachen auf • In deklarativen Sprachen wird das Problem und nicht dessen Lösung betrachtet • Die Lösungsfindung wird dem Computer überlassen • Dementsprechend besteht Tendenz, dass die Implementierungszeit geringer ist als in anderen Sprachen • Dies geschieht jedoch auf Kosten der Ausführungszeit 64 Anwendung • Bisher keine große Verbreitung • Beschränkt auf Hochschullehre und -forschung