Arbeitsgruppe Programmiersprachen und Übersetzerkonstruktion Institut für Informatik Christian-Albrechts-Universität zu Kiel Seminararbeit Picat – eine Multiparadigmen-Programmiersprache Sven Hüser WS 2014/2015 Betreut von Jan Tikovsky Inhaltsverzeichnis 1 Einleitung 1 2 Grundlagen 2.1 Funktionale Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Logische und Constraint Programmierung mit B-Prolog . . . . . . . . . . 2 2 3 3 Picat 3.1 Pattern-Matching und (Nicht-)Determinismus . . . 3.2 Imperative Programmierung . . . . . . . . . . . . . 3.3 Eine Schnittstelle für verschiedene Constraint-Löser 3.4 Actors . . . . . . . . . . . . . . . . . . . . . . . . . 3.5 Tabling . . . . . . . . . . . . . . . . . . . . . . . . 3.6 Planner . . . . . . . . . . . . . . . . . . . . . . . . 4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 8 9 11 12 13 15 ii 1 Einleitung Picat ist ein neues Mitglied in der Familie der logischen Programmiersprachen. Eine erste Betaversion wurde von Neng-Fa Zhou im Mai 2013 veröffentlicht. Auch funktionale Aspekte wie Funktionen und imperative Konstrukte wie Schleifen und Zuweisungen sind in die Sprache eingeflossen. Die hohe Leistungsfähigkeit des in Prolog vorhandenen Nichtdeterminismus wird oft nicht ausgenutzt und ist für deterministische Berechnungen nicht nötig. Aus diesem Grund müssen Regeln in Picat explizit als nichtdeterministisch angegeben werden, wenn dies gewünscht ist. Regeln und Funktionen werden in Picat nicht mittels Unifikation, sondern mit Pattern-Matching ausgewählt. Um Constraint-Satisfaction-Probleme zu lösen, stehen die drei Solver-Module CP, SAT und MIP zur Verfügung, die über eine gemeinsame Schnittstelle angesprochen und somit einfach ausgewechselt werden können. Eine Technik, Berechnungen durch Speichern und Wiederverwenden von Zwischenergebnissen zu beschleunigen, ist Tabling. Das in Picat eingesetzte System ist von B-Prolog, einer weiteren Sprache von Neng-Fa Zhou, abgeleitet. Planungsprobleme, wie sie zum Beispiel bei künstlichen Intelligenzen wie Robotern auftreten, können mit dem in Picat enthaltenen Planner-Modul auf einfache Weise modelliert werden. In Kapitel 2 werden funktionale und logische Programmierung in ihren Grundzügen vorgestellt. Haskell dient hier als Beispiel einer funktionalen Programmiersprache, anhand der die Konzepte Pattern-Matching und Funktionen höherer Ordnung erläutert werden. Die logische Programmiersprache B-Prolog wird herangezogen, um zum einen logische Programmierung, wie sie mit Prolog möglich ist, vorzustellen. Zum anderen soll NengFa Zhous Erfahrung bei der Entwicklung von Programmiersprachen beziehungsweise deren Erweiterung um bestimmte Eigenschaften aufgezeigt werden. Daher wird hier auch auf Konzepte wie Schleifen eingegangen sowie kurz eine auch in B-Prolog enthaltene Schnittstelle zum Anbinden verschiedener Solver erwähnt. Kapitel 3 gibt schließlich einen Überblick über die oben erwähnten Features von Picat und stellt diese an einigen Beispielen wie dem dem SEND + MORE = MONEY und dem N-Damen-Problem Problem dar. 1 2 Grundlagen Bei der Programmiersprache Picat handelt es sich um eine logische Programmiersprache, unter anderem mit funktionalen Einflüssen. Daher sollen in diesem Kapitel ausgewählte Bereiche funktionaler und logischer Programmierung vorgestellt werden. Beide Themen werden in den Vorlesungen Fortgeschrittene Programmierung [5], Prinzipien von Programmiersprachen [4], und Deklarative Programmiersprachen [2] behandelt. 2.1 Funktionale Programmierung Um einige Eigenschaften funktionaler Programmiersprachen zu beleuchten, soll die Sprache Haskell als Beispiel dienen. Einen kompletten Überblick über die Möglichkeiten, die funktionale Sprachen bieten, zu geben, ist im Rahmen dieses Kapitels nicht möglich. Im Gegensatz zu imperativen Programmiersprachen wie zum Beispiel C gibt es in funktionalen Sprachen keine Zuweisungen, Anweisungen oder Prozeduren, sondern nur Funktionen und Ausdrücke; diese Sprachen sind im Allgemeinen seiteneffektfrei. Desweiteren kommt es nicht auf die Auswertungsreihenfolge an, die Werte von Ausdrücken ergeben sich allein aus den Werten der Teilausdrücke. Funktionsdefinitionen können durch Muster und mit mehreren Gleichungen vorgenommen werden. Welche Regel angewandt wird, entscheidet sich anhand der Parameter und wie diese zu den angegebenen Mustern passen. Diese Auswahl wird als Pattern-Matching bezeichnet. Betrachten wir dazu die Funktion append, die zwei Listen aneinander hängt: append :: [a] -> [a] -> [a] append [] ys = ys append (x:xs) ys = x:(append xs ys) Die erste Zeile ist die Signatur der Funktion, die aussagt, dass als Parameter zwei Listen vom gleichen Typ a erwartet werden und wiederum eine Liste von diesem Typ geliefert wird. Danach folgen die Gleichungen der Funktion: das Muster [] steht für die leere Liste, ys für eine beliebige Liste und (x:xs) dafür, dass es sich um eine nicht leere Liste handelt. Hier ist : der Konstruktor für Listen. Die erste Regel wird nur dann angewandt, wenn der erste Parameter die leere Liste ist; das Ergebnis ist dann die zweite Liste. In allen anderen Fällen wird die zweite Regel gewählt. Hier wird eine neue Liste konstruiert: das erste Element x der ersten Liste wird vor das Ergebnis des erneuten Aufrufs von append mit der Restliste xs und ys gehängt. Da die Regeln der Reihe nach geprüft werden, müssen speziellere Fälle vor allgemeineren angegeben werden. Eine weitere Eigenschaft funktionaler Sprachen ist, dass Funktionen „Bürger erster Klasse“ sind und somit an jeder Stelle auftreten können, an denen Werte stehen können. 2 2 Grundlagen Diese Funktionen werden als Funktionen höherer Ordnung bezeichnet. So nimmt zum Beispiel die Funktion twice eine Funktion f und einen Wert x und wendet f zweimal hintereinander an: twice :: (a -> a) -> a -> a twice f x = f (f x) Zuletzt bietet Haskell (neben weiteren Programmiersprachen) ein syntaktisches Konstrukt, um Listen in einer mathematischen Notation anzugeben: [ (x,y) | x <- [0..5], y <- [0..5], x + y <= 6 ] Dieser Ausdruck liefert die Liste der Paare (x,y), für die gilt, dass die Summe der beiden Zahlen kleiner oder gleich 6 ist. Mit der Notation [0..5] wird die Liste der Zahlen von 0 bis 5 erzeugt, die eine nach der anderen an x beziehungsweise y gebunden werden. Bezeichnet werden Ausdrücke dieser Art als List Comprehension. 2.2 Logische und Constraint Programmierung mit B-Prolog B-Prolog ist eine Erweiterung der logischen Programmiersprache Prolog um Features wie Schleifen, Arrays oder Constraint-Solver. Auch hier kann nicht die komplette Sprache vorgestellt werden, der Leser sei deshalb an [10] verwiesen. In der Logikprogrammierung wird – anders als in der funktionalen Programmierung – nicht mit Funktionen, sondern mit Relationen gearbeitet. Am Beispiel der Verwandtschaftsbeziehungen kann die Funktionsweise der Relationen, aufgeschrieben als Fakten und Regeln, nachvollzogen werden: vater(hans,herbert). vater(anna,herbert). vater(lisa,hans). mutter(hans,claudia). mutter(anna,claudia). mutter(vanessa,anna). Diese Aussagen sind Fakten: sie werden als wahr angenommen und durch einen Punkt und folgenden Whitespace (Leerraum oder Zeilenumbruch) am Ende abgeschlossen. Möchte man nun herausfinden, wer Großvater von wem ist, kann dies mit Hilfe von Regeln angegeben werden. Diese haben die Form Head :- Body., Fakten sind lediglich Regeln ohne Body. Durch freie Variablen kann Folgendes formuliert werden: grossvater(E,G) :- vater(E,V), vater(V,G). grossvater(E,G) :- mutter(E,M), vater(M,G). Freie Variablen wie E und G beginnen dabei mit einem Großbuchstaben, Atome wie hans und anna mit einem Kleinbuchstaben. An das Prolog-System können Anfragen gestellt werden, die mittels der eingegebenen Regeln und Fakten – als Wissen bezeichnet – auf ihren Wahrheitsgehalt hin geprüft werden können: 3 2 Grundlagen | ?- vater(hans,herbert). yes | ?- mutter(lisa,claudia). no | ?- grossvater(Enkel,herbert). Enkel = lisa ?; Enkel = vanessa yes Weil in Prolog standardmäßig Backtracking eingesetzt wird, können hier alle möglichen Lösungen gefunden werden: nachdem das Atom lisa mit der ersten Regel für grossvater gefunden wurde, wird auch die zweite Regel probiert, so dass sich auch mit vanessa eine Lösung ergibt. Um den Suchraum für Lösungen einzuschränken, gibt es einen sogenannten cut-Operator !, der Backtracking über diesen hinaus verhindert. Schleifen können ein für die Modellierung hilfreiches Konstrukt sein, sie sind allerdings keine deklarativen Konstrukte und daher in den meisten Prolog-Implementierungen nicht vorhanden. In B-Prolog können foreach-Schleifen der folgenden Form verwendet werden: foreach(E1 in D1 , . . ., E1 in D1 , LocalV ars, Goal) Die sogenannten Iteratoren Ei in Di setzen sich aus einem Muster Ei (normalerweise einfach eine Variable) und einer Liste von Werten Di zusammen. Möchte man in Goal Variablen benutzen, die für jeden Durchlauf der Schleife lokal sind, müssen diese in der Liste LocalV ars angegeben werden. Viele Probleme aus der realen Welt lassen sich als Constraint-Satisfaction-Probleme (CSP) formulieren, zum Beispiel das Planen für künstliche Intelligenzen oder RessourcenAllokation. Repräsentiert werden diese Probleme mit Hilfe von Entscheidungsvariablen – Variablen mit eingeschränktem Wertebereich – und Constraints. Als Beispiel sollen den Buchstaben in SEND + MORE = MONEY Ziffern zugewiesen werden, so dass die Gleichung stimmt: sendmore(L) :L = [S,E,N,D,M,O,R,Y], L :: 0..9, % Liste der Variablen % Einschränkung des Wertebereiches alldifferent(L), % verschiedene Werte S #> 0, M #> 0, % keine 0 zu Beginn einer Zahl 1000*S + 100*E + 10*N + D + 1000*M + 100*O + 10*R + E #= 10000*M + 1000*O + 100*N + 10*E + Y, labeling(L). % Aufzählen der Variablen 4 2 Grundlagen Die Constraint-Operatoren beginnen mit einem #. Durch X :: D wird angegeben, dass die Variable X (oder alle Variablen einer Liste, wie oben L) die Werte aus der Liste D, die die Domain oder den möglichen Wertebereich angibt, annehmen kann. Durch eine Anfrage lässt sich dann eine Lösung finden: | ?- sendmore(L). L = [9,5,6,7,1,0,8,2] ? yes Neben diesen Möglichkeiten, die – abgesehen von eventuell unterschiedlichen Namen der Basisconstraints – auch in anderen Prolog-Systemen wie SWI-Prolog oder SICStus-Prolog vorhanden sind, bietet B-Prolog noch eine gemeinsame Schnittstelle für drei verschiedene Constraint-Solver: CP-Solver, IP-Solver und SAT-Solver. Diese ermöglichen es, Probleme mit unterschiedlichen Herangehensweisen zu lösen, nämlich mittels (1) Constraint Programming (CP), (2) Integer Programming (IP), und (3) SAT-Solving. Es gibt boolesche, arithmetische und globale Constraints. Boolesche und arithmetische Ausdrücke sind aus booleschen Variablen und Konstanten beziehungsweise arithmetischen Ausdrücken aufgebaut, die die entsprechenden Operatoren enthalten können. Diese werden hier mit einem $ kenntlich gemacht und umfassen boolesche Operatoren wie $/\ und $\/ für Konjunktion und Disjunktion und arithmetische Operatoren wie $\= oder $= für Ungleichheit oder Gleichheit. Zu den globalen Constraints zählt $alldifferent(L). Die verschiedenen Solver werden über cp_solve(Options,L) (respektive ip_solve oder sat_solve) aufgerufen, um eine Lösung für die Variablen in der Liste L zu finden. Das sendmore-Beispiel von oben könnte folgendermaßen zu einem sendmore2 umgebaut werden: sendmore2(L) :L = [S,E,N,D,M,O,R,Y], L :: 0..9, % Liste der Variablen % Einschränkung des Wertebereiches $alldifferent(L), % verschiedene Werte S $> 0, M $> 0, % keine 0 zu Beginn einer Zahl 1000*S + 100*E + 10*N + D + 1000*M + 100*O + 10*R + E $= 10000*M + 1000*O + 100*N + 10*E + Y, cp_solve(L). %sat_solve(L). %ip_solve(L). Wie die Constraints für die Solver transformiert werden und was mit Options angegeben werden kann, ist [13] zu entnehmen. 5 3 Picat Bei Picat1 handelt es sich um eine neue logische Programmiersprache mit funktionalen und imperativen Einflüssen, die aktiv von Neng-Fa Zhou und Jonathan Fruhman entwickelt wird. Im Kontext dieser Arbeit wird Version 0.7 vom 13. November 2014 verwendet. Der Name ist ein Akronym, der User’s Guide [11] sagt dazu Folgendes: Picat is a general-purpose language that incorporates features from logic programming, functional programming, and scripting languages. The letters in the name summarize Picat’s features: • Pattern-matching [...] • Imperative [...] • Constraints [...] • Actors [...] • Tabling [...] In diesem Kapitel werden diese Punkte einzeln vorgestellt. Das Augenmerk wird hier auf die Möglichkeiten der Constraint-Modellierung mit Picat gelegt, insbesondere auf die Schnittstelle, um die verschiedenen Solver anzusprechen [7]. Der Schwede Håkan Kjellerstrand stellt auf seiner Website2 viele selbstgeschriebene Picat-Programme sowie einen eigenen Überblick [6] über die Fähigkeiten der Sprache zur Verfügung. Zusätzlich zu den Datentypen aus Prolog gibt es in Picat Strings, auf die auch alle Funktionen für Listen angewandt werden können, Arrays und Maps genannte Hashtabellen. Da auch Funktionsaufrufe als Parameter erlaubt sind, müssen Strukturen mit einem vorangestellten Dollarzeichen gekennzeichnet werden. Andernfalls würde ein Aufruf der Form S = point(1,2) die Funktion point aufrufen und S an deren Rückgabewert binden. 3.1 Pattern-Matching und (Nicht-)Determinismus Wie auch andere logikbasierte Sprachen – zum Beispiel Curry [3] – unterstützt Picat zusätzlich zu den Relationen auch Funktionen. Wenn nur eine Antwort benötigt wird, sollten diese bevorzugt benutzt werden, da sie einen garantierten Rückgabewert haben, verschachtelt werden können, und weil sie gerichtet sind und sich somit die Lesbarkeit erhöht. 1 2 http://www.picat-lang.org http://www.hakank.org/picat/ 6 3 Picat Definiert werden Prädikate und Funktionen durch Angabe von musterorientierten Regeln, die sich etwas voneinander unterscheiden. Für Prädikate gibt es deterministische und nicht-deterministische Regeln, ein cut-Operator wie in Prolog ist dann nicht nötig: Head, Cond => Body Head, Cond ?=> Body % deterministisch % nicht-deterministisch wobei Head ein n-stelliges Prädikat p der Form p(t1 , . . . , tn ) annimmt. Cond ist eine optionale Bedingung, mit der zusätzlich festgelegt werden kann, in welchen Fällen die Regel anwendbar ist. In einem Muster kann auch das aus Haskell bekannte As-pattern vorkommen, welches die Form V @P at hat. V muss eine neue, nicht im Rest von Head benutzte Variable sein und P at ein nicht-variabler Term; ist das Matching mit P at erfolgreich, ist V eine Referenz auf den entsprechenden Term. Kann eine Regel auf einen Aufruf A angewandt werden, wird A in die Form von Body umgeschrieben. Handelt es sich um eine nicht-deterministische Regel, so wird Body wieder zurück in die Form von A geschrieben, sobald es einen Fehlschlag gab. Anschließend kann die nächste passende Regel angewandt werden. Bei deterministischen Regeln ist die Entscheidung für diese verbindlich und das Programm kann den Schritt nicht rückgängig machen. Möchte man Fakten wie in Prolog angeben, müssen die Prädikate mit einer index Deklaration versehen werden, damit sie als Daten verfügbar werden. Das Verwandtschaftsbeispiel aus Kapitel 2.2 kann (hier verkürzt) folgendermaßen aufgeschrieben werden: index (-,-) vater(hans,herbert). ... index (-,-) mutter(hans,claudia). ... grossvater(E,G) ?=> vater(E,V), vater(V,G). grossvater(E,G) => mutter(E,M), vater(M,G). Wäre die erste Regel für grossvater auch deterministisch, würde die zweite niemals ausgewählt. Funktionsdefinitionen sehen etwas anders aus, weil Funktionen einen Rückgabewert haben. Diese werden immer mit deterministischen Regeln der Form F =R, Cond => Body notiert. F ist ein Muster f (t1 , . . . , tn ) für eine Funktion, R ist der Rückgabewert. Die Kurzschreibweise F =R kann anstelle von F =X => X=R (X muss eine neue Variable sein) benutzt werden. Funktionen werden zu Prädikaten übersetzt. Sollte keine Regel zu einem Funktionsaufruf passen, weil das Muster zu allgemein ist, also unzureichend instanziierte Variablen enthält, wird eine unresolved_function_call Exception geworfen, es kommt dadurch also nicht zu einem Fehlschlag. Die take-Funktion, die die ersten n Elemente einer Liste liefert, kann in Picat folgendermaßen definiert werden: take( _Xs, 0) = []. take( [],_N) = []. take([X|Xs], N) = [X] ++ take(Xs, N-1). 7 3 Picat Das Pattern-Matching funktioniert in Picat standardmäßig wie in funktionalen Sprachen wie Haskell: wird eine Regel ausgewählt, findet keine Unifikation wie bei Prolog statt. Möchte man eine Variable an einen Wert binden, muss sie explizit aufgerufen werden. Das member-Prädikat kann wie in Prolog benutzt werden, um zu prüfen, ob ein Element in einer Liste vorkommt und um alle möglichen Listenelemente zu erhalten: member(X,[E|_]) ?=> X=E. member(X,[_|R]) => member(X,E). Im Body der ersten, nicht-deterministischen Regel wird X mit E unifiziert. In den beiden vorherigen Beispielen fällt auf, dass die Namen der freien Variablen mit einem Unterstrich anfangen, wenn diese ein Singleton ist. Picat verhält sich hier wie Prolog: wenn der Name mit einem Unterstrich beginnt, werden die solche Variablen betreffenden Warnungen deaktiviert. Einige in Picat eingebaute Funktionen und Prädikate erlauben Funktionsaufrufe als Argumente. Zum Beispiel nimmt das Prädikat call(S, A1 , . . ., An ) ein Atom oder eine Struktur (erkennbar durch ein führendes $) S und ruft das dadurch angegebene Prädikat mit den in S gegebenen Parametern und zusätzlichen Argumenten A1 , . . . , An auf: Picat> call($member(X), [a,b]). X = a?; X = b?; no Die Struktur $member muss hier verwendet werden, da ansonsten versucht würde, den Aufruf an Ort und Stelle auszuwerten. 3.2 Imperative Programmierung Bereits in B-Prolog hatte Neng-Fa Zhou Schleifen eingebaut. Der Hauptgrund, warum er eine neue Sprache entwickelt, ist seine Unzufriedenheit mit der Implementierung der Schleifen in B-Prolog [8]. Picat bietet einige traditionell aus der imperativen Programmierung bekannte Konstrukte an: foreach- und while-Schleifen sowie Zuweisungen. Schleifen werden in endrekursive Prädikate übersetzt und sehen folgendermaßen aus: foreach (E1 in D1 , Cond1 , . . ., En in Dn , Condn ) Goal end while (Cond) Goal end 8 3 Picat Wie schon bei B-Prolog sind hier Ei in Di Iteratoren, für die mit Condi eine optionale Bedingung angegeben werden kann. Für alle möglichen Kombinationen der Werte Ei , die die entsprechenden Bedingungen erfüllen, wird Goal in der foreach-Schleife ausgeführt. In der while-Schleife wird Goal analog so lange ausgeführt, wie die Bedingung Cond zutrifft. In Picat müssen auch keine lokalen Variablen für die foreach-Schleife deklariert werden, wie dies bei B-Prolog der Fall ist. Stattdessen sind alle Variablen, die nur innerhalb der Schleife auftreten, für jeden Durchlauf lokal. Da Variablen in Picat nur einmal an einen Wert gebunden werden können und dann nie wieder, gibt es den Zuweisungsoperator :=, um die aus imperativen Sprachen bekannte Funktionalität, Variablen mehrfach Werte zuzuweisen, zu simulieren. Eine Zuweisung hat die Form L := R. Das L steht für eine Variable oder einen Array- beziehungsweise Listenzugriff, das R für einen Ausdruck. Im Falle von Backtracking wird die Aktualisierung des Wertes rückgängig gemacht. Um das Problem zu umgehen, das entsteht, wenn einer bereits gebundenen Variable ein neuer Wert zugewiesen werden soll, legt der Compiler für jede dieser Zuweisungen eine neue Variable an. Durch das Übersetzen von Schleifen in endrekursive Prädikate wird auch das Problem gelöst, dass eventuell nicht vorher bekannt ist, wie viele neue Variablen aufgrund von Zuweisungen in while-Schleifen erzeugt werden müssen. Auch in Picat stehen dem Nutzer List Comprehensions zur Verfügung. Diese haben eine Form ähnlich der von foreach-Schleifen und werden in solche übersetzt: [ T : E1 in D1 , Cond1 , . . ., En in Dn , Condn ] 3.3 Eine Schnittstelle für verschiedene Constraint-Löser Picat stellt drei Module für das Lösen von Constraints bereit: Constraint Programming (CP), Mixed Integer Programming (MIP) und SAT-Solving. Wie in B-Prolog gibt es eine gemeinsame Schnittstelle, die es ermöglicht, schnell von einem Löser auf einen anderen zu wechseln. Um einen Löser zu benutzen, muss das jeweilige Modul – cp, sat oder mip – für diesen importiert werden. Über die Schnittstelle stehen dem Benutzer die Mittel zum Erstellen von Entscheidungsvariablen und Constraints sowie zum Aufrufen des Lösers zur Verfügung. Das aus B-Prolog bekannte Domain-Constraint X :: D wird auch in Picat benutzt, um den Wertebereich von Entscheidungsvariablen einzuschränken. Arithmetische Constraints haben die Form E1 R E2 mit zwei arithmetischen Ausdrücken E1 und E2 , die sich aus Integer-Werten, Entscheidungsvariablen und eingebauten arithmetischen Funktionen wie dem Absolutbetrag zusammensetzen, und einem Constraint-Operator R aus der Menge {#=, #!=, #>=, #>, #=<, #<}, deren Funktionen selbsterklärend sind. Das SEND + MORE = MONEY Beispiel lässt sich natürlich auch in Picat darstellen, hier kann man deutlich die Einflüsse von (B-)Prolog erkennen. Die Import-Anweisung in der ersten Zeile wurde bereits erwähnt, auf das in der letzten Zeile auftretende solve-Prädikat wird später eingegangen: 9 3 Picat import cp. sendmore(Digits) => Digits = [S,E,N,D,M,O,R,Y], Digits :: 0..9, all_different(Digits), S #> 0, M #> 0, 1000*S + 100*E + 10*N + D + 1000*M + 100*O + 10*R + E #= 10000*M + 1000*O + 100*N + 10*E + Y, solve(Digits). Boolesche Ausdrücke dürfen natürlich nicht fehlen. Diese bestehen aus den booleschen Konstanten 0 und 1, booleschen Variablen und den Operatoren #/\ für eine UndVerknüpfung, #\/ für Oder, dem Negationsoperator #~, #^ für Kontravalenz und #<=> und #=> für Äquivalenz und Implikation. Außerdem können auch arithmetische Constraints und Domain-Constraints als Operanden auftreten. Für die Reifikation stellt Picat eine einfache Syntax bereit: in dem Constraint der Form B #<=> (E1 #= E2) wird die Erfüllbarkeit von E1 #= E2 mit Hilfe der booleschen Variable B ausgedrückt. Zum Erlauben oder Verbieten von bestimmten Kombinationen von Variablen gibt es die Table-Constraints table_in(V , L) und table_notin(V , L). Mit V wird ein Tupel von Variablen oder eine Liste von Tupeln und mit L eine Liste von erlaubten beziehungsweise verbotenen Tupeln von Integer-Werten angegeben. Ebenfalls enthalten sind Constraints wie all_different, welches bereits im Codebeispiel oben auftritt, oder element(I, L, V), das das I-te Element der Liste L mit V unifiziert. Ist I keine Entscheidungsvariable, sondern lediglich ein Integer-Wert, kann die Schreibweise L[I] = V benutzt werden. Eine Liste aller dieser unterstützten Constraints findet sich in Kapitel 11.5 des User’s Guide [11]. Über das Prädikat solve(Options, V ars) wird der Löser gestartet, der mit der ImportAnweisung spezifiziert wird, und die Variablen aus der Liste V ars werden mit Werten versehen, wie dies in Prolog mit labeling erfolgt. In der Liste Options können dem Löser Optionen angegeben werden. Um zum Beispiel eine optimale Lösung zu erhalten, stehen die Optionen $min(E) und $max(E) bereit. Die Option $report(Call) kann in diesen Fällen genutzt werden und führt jedes Mal, wenn eine bessere Antwort gefunden wird, den Aufruf Call aus. Weitere Möglichkeiten, die Löser aufzurufen sowie alle zulässigen Optionen sind ebenfalls im User’s Guide, in Kapitel 11.6, aufgeführt. Folgendes Beispiel löst das bekannte N-Damen-Problem. Die Option ff (first fail) belegt die Variablen so, dass zuerst diejenige Variable mit dem eingeschränktesten Wertebereich belegt wird. Dadurch erhöht sich die Geschwindigkeit der Lösungsfindung enorm, weil nicht alle möglichen – im Fall von 100 Damen also 100100 – Platzierungen ausprobiert werden müssen. Auch lässt sich damit ein Einsatz der foreach-Schleife und eines Arrays, das hier anstelle einer Liste benutzt wird, demonstrieren: 10 3 Picat import cp. nqueens(N) => Q = new_array(N), Q :: 1..N, foreach (I in 1..N-1, J in I+1..N) Q[I] #!= Q[J], abs(Q[I] - Q[J]) #!= J-I end, solve([ff], Q), writeln(Q). % unterschiedliche Zeilen % nicht auf einer Diagonalen Wird das cp-Modul benutzt, werden die Constraints in Constraint-Propagatoren übersetzt, die in der Action Rules Sprache von Neng-Fa Zhou definiert sind [9]. Realisiert sind sie mittels Actors, auf die im nächsten Abschnitt kurz eingegangen wird. Der CP-Solver ist von B-Prolog übernommen worden. Als SAT-Solver kommen Lingeling3 unter Linux und OS X und MiniSat4 unter Windows zum Einsatz. Die Constraints werden in konjunktive Normalform übersetzt, wobei für jede Entscheidungsvariable mit einem maximal möglichen Wert n genau dlog2 ne boolesche Variablen benutzt werden zusätzlich einer weiteren zur Darstellung des Vorzeichens, wenn der Wertebereich sowohl positive als auch negative Werte enthält. Lücken im Definitionsbereich werden durch Constraints, die Ungleichheit erzwingen, ausgeschlossen. Die Solver-Option dump gibt die so erzeugte KNF aus. Das mip-Modul nutzt das GNU Linear Programming Kit5 zum Lösen der Constraints. Hier werden die Constraints in Ungleichungen umgeformt: aus X #!= Y wird X #=< Y-1 #\/ X #>= Y+1, woraus mit den beiden Reifikationen B1 #<=> (X #=< Y-1) B2 #<=> (X #>= Y+1) anschließend B1 #\/ B2 gemacht wird. Die Reifikationen werden abschließend in die Constraints X - Y - C1 * (1-B) #=< 0 Y - X - C2 * B #=< 0 übersetzt, wobei C1 = ubd(X) - lbd(Y) + 1 und C2 = ubd(Y) - lbd(X) + 2 zwei Konstanten sind. Mit lbd(X) und ubd(X) sind die untere beziehungsweise obere Schranke des Wertebereichs von X gemeint. 3.4 Actors Systeme wie eine GUI müssen auf Ereignisse, zum Beispiel das Klicken mit der Maus auf einen Knopf, reagieren. In Picat werden Action Rules benutzt, um ereignisgesteuerte 3 http://fmv.jku.at/lingeling/ http://minisat.se/ 5 https://www.gnu.org/software/glpk/ 4 11 3 Picat Actors zu beschreiben. Ein solcher Actor ist ein Prädikataufruf, der verzögert und zu einem späteren Zeitpunkt durch Ereignisse aktiviert werden kann. Die Kommunikation findet über Ereignis-Kanäle statt. Eine Action Rule hat die Form Head, Cond, {Event} => Body, bei der Head ein Muster für den Actor ist, Cond wie gewohnt optionale Bedingungen angeben kann und {Event} eine nicht-leere Menge von Ereignissen angibt. In Kapitel 10.2 im User’s Guide [11] findet sich eine Auflistung der möglichen Ereignisse. Folgendes Beispiel stammt ebenfalls daher: p(X), {event(X,T)} => writeln(T). Picat> p(X), X.post_event(ping), X.post_event(pong) ping pong Die post_event Aufrufe auf dem Actor p(X) aktivieren diesen, so dass der Rumpf ausgeführt wird und ping und pong ausgegeben werden. Eine eingebaute Funktion, die Actors benutzt, ist das Prädikat freeze, das so definiert ist: freeze(X,Goal),var(X),ins(X) => true. freeze(X,Goal) => call(Goal). Wenn X in dem Aufruf freeze(X,Goal) eine Variable ist, wird X ein Actor, der auf den ins-Port reagiert: wird X gebunden, aktiviert dies den Actor. Ist X weiterhin ein variabler Term, wird wieder die erste Regel angewandt und weiter gewartet. Andernfalls verschwindet der Actor, weil die zweite Regel ausgewählt wird und Goal wird ausgeführt. 3.5 Tabling Mit Tabling wird eine Technik bezeichnet, bereits berechnete Ergebnisse im Speicher zu halten, um diese nicht neu berechnen zu müssen. Das Schlüsselwort table, das vor die erste Regel für ein Prädikat oder eine Funktion geschrieben werden kann, veranlasst Picat dazu, alle Aufrufe mit Ergebnis zu cachen. Die standardmäßig rekursive Implementierung zur Berechnung der Fibonacci-Zahlen, die für große Zahlen normalerweise wegen der sehr hohen Zahl rekursiver Aufrufe sehr langsam ist, lässt sich auf diese Weise erheblich beschleunigen. Betrachte dazu diese Definition: table fib(0) = 1. fib(1) = 1. fib(N) = F, N > 1 => F = fib(N-1) + fib(N-2). Zur Demonstration des erreichten Speedup gegenüber einer Version ohne Tabling (fibnt) wurden auf einer Intel Core i5-4260U CPU mit 1,4 GHz beide Programme mit N = 40 aufgerufen: 12 3 Picat $ time picat fib 40 fib(40)=165580141 $ time picat fibnt 40 fib(40)=165580141 real user sys real user sys 0m0.013s 0m0.006s 0m0.005s 0m21.223s 0m20.880s 0m0.120s Bei beiden Messungen ist der Overhead, Picat zu starten, enthalten. Inwiefern Tabling einen Vorteil bringt, muss von Fall zu Fall ausprobiert werden. Das in Picat eingesetzte System ist von B-Prolog abgeleitet worden [12]. Picat unterstützt außerdem auch mode-directed tabling [1]. Eine table mode declaration hat die Form table (M1 , . . ., Mn ): jedes Mi kann +, -, min oder max sein, Mn kann darüber hinaus noch nt sein. Plus und Minus bezeichnen Ein- beziehungsweise Ausgabe, min und max ein zu minimierendes oder maximierendes Argument. Der letzte Wert nt steht für not tabled. Dies können globale, für alle Aufrufe gleiche oder von den Eingaben abhängende Daten sein. Wird eine solche Deklaration angegeben, speichert Picat für jedes Tupel von Eingabeargumenten nur die jeweils beste Lösung. 3.6 Planner Das bei Picat enthaltene planner-Modul macht es sehr einfach, Planungsprobleme zu implementieren. Damit ein Plan oder ein optimaler Plan gefunden werden kann, müssen der finale Zustand und die Menge der Aktionen angegeben werden und anschließend das Planungssystem auf einen Anfangszustand angesetzt werden. Für die zum Planen bereitgestellten Prädikate wird Tabling benutzt. Es wird eine Zustandsraumsuche durchgeführt, bei der jeder erreichte Zustand gespeichert wird. Das Grundgerüst eines Planers mit Tabling ist gegeben als table (+,-,min) plan(S,Plan,Cost),final(S) => Plan=[],Cost=0. plan(S,Plan,Cost) => action(S,S1,Action,ActionCost), plan(S1,Plan1,Cost1), Plan = [Action|Plan1], Cost = Cost1+ActionCost. und zeigt ebenfalls die im vorherigen Abschnitt angesprochene table mode declaration. Definiert werden müssen nun die Prädikate final und action. Das Suchen eines Planes kann dann mit einem gegebenen Startzustand S0 mit dem Aufruf plan(S0 ,P lan,Cost) gestartet werden. In dem Modul werden noch weitere Prädikate definiert, unter anderem best_plan/3, das wie plan/3 funktioniert und dieses benutzt, um einen Plan mit Kosten von 0 zu suchen. Gibt es keinen, werden die Kosten schrittweise um 1 erhöht und die Suche wird wiederholt, bis ein Plan gefunden wurde. Weitere Features des Moduls enthalten das Unterscheiden zwischen einer Suche mit und ohne Ressourcenbeschränkung. Ein Beispiel für ein Planungsproblem ist das Folgende, bei dem eine Liste mit zwölf Elementen mit 13 3 Picat den Aktionen merge und reverse sortiert werden muss. Diese Implementierung6 ist von Håkan Kjellerstrand: go => Init = [8,11,6,1,10,9,4,3,12,7,2,5], best_plan_downward(Init, Plan), writeln(Plan), writeln(len=Plan.length), nl. final(Goal) => Goal=[1,2,3,4,5,6,7,8,9,10,11,12]. table % merge move action([M1,M12,M2,M11,M3,M10,M4,M9,M5,M8,M6,M7], To, M, Cost) ? => Cost=1, M=m, To=[M1,M2,M3,M4,M5,M6,M7,M8,M9,M10,M11,M12]. % reverse move action([M12,M11,M10,M9,M8,M7,M6,M5,M4,M3,M2,M1], To,M, Cost) => Cost=1,M=r, To=[M1,M2,M3,M4,M5,M6,M7,M8,M9,M10,M11,M12]. 6 http://www.hakank.org/picat/test_planner_M12.pi 14 4 Zusammenfassung In dieser Ausarbeitung wurde die neue, sich in Entwicklung befindende Programmiersprache Picat vorgestellt, in der mehrere Paradigmen zusammenkommen: funktionale, logische und imperative Programmierung. Dazu wurde zunächst auf die Grundlagen von funktionalen und logischen Programmiersprachen wie Pattern-Matching, Funktionen höherer Ordnung, Backtracking und Constraints eingegangen. Als Beispiele dienten die weit verbreiteten Sprachen Haskell und B-Prolog, die es seit den 1990er Jahren gibt. Im Hauptteil der Arbeit wurden die wesentlichen Features von Picat präsentiert. Die funktionalen Aspekte werden durch die Einführung von Funktionen und am PatternMatching und der damit realisierten Auswahl der Regeln deutlich gemacht. In Picat ist es dem Entwickler Neng-Fa Zhou erfolgreich gelungen, imperative Konstrukte wie Schleifen und Zuweisungen zu implementieren. Constraint-Satisfaction-Probleme lassen sich elegant mit verschiedenen Lösern angehen, da es eine gemeinsame Schnittstelle für einen CP-Solver, einen SAT-Solver und einen MIP-Solver gibt. Es wurde gezeigt, wie die in Problemen spezifizierten Constraints für diese Löser aufbereitet werden. Abschließend wurden mit Actors, Tabling und dem planner-Modul weitere für die Modellierung von Problemen nützliche Werkzeuge vorgestellt. Da Picat derzeit noch weiter entwickelt wird, können und werden einige Features neu hinzukommen und bereits bestehende geändert werden. Während der Anfertigung dieser Arbeit ist am 24. Januar 2015 bereits Version 0.8 erschienen. 15 Literatur [1] H.-F. Guo und G. Gupta. Simplifying dynamic programming via mode-directed tabling. In: Software: Practice and Experience 38.1 (2008), S. 75–94. [2] M. Hanus. Deklarative Programmiersprachen. SS 2014. [3] M. Hanus. Functional logic programming: From theory to Curry. In: Programming Logics. Springer, 2013, S. 123–168. [4] M. Hanus. Prinzipien von Programmiersprachen. WS 2013/14. [5] M. Hanus und F. Huch. Fortgeschrittene Programmierung. SS 2012. [6] H. Kjellerstrand. My First Look At Picat as a Modeling Language for Constraint Solving and Planning. In: Control and Decision Conference (2014 CCDC), The 26th Chinese. IEEE. 2014, S. 351–358. [7] N.-F. Zhou. Combinatorial Search With Picat. In: arXiv preprint arXiv:1405.2538 (2014). [8] N.-F. Zhou. Motivation for Picat. Online; Zugriff am 20. Januar 2015. Dez. 2012. url: https://groups.google.com/d/msg/comp.lang.prolog/p6nUtaCAs8A/ JQ7b0LY5vfsJ. [9] N.-F. Zhou. Programming finite-domain constraint propagators in action rules. In: Theory and Practice of Logic Programming 6.05 (2006), S. 483–507. [10] N.-F. Zhou. The language features and architecture of B-Prolog. In: Theory and Practice of Logic Programming 12.1-2 (2012), S. 189–218. [11] N.-F. Zhou und J. Fruhman. A User’s Guide to Picat. 2014. [12] N.-F. Zhou, T. Sato und Y.-D. Shen. Linear tabling strategies and optimizations. In: Theory and Practice of Logic programming 8.01 (2008), S. 81–109. [13] N.-F. Zhou, M. Tsuru und E. Nobuyama. A Comparison of CP, IP, and SAT Solvers through a Common Interface. In: Tools with Artificial Intelligence (ICTAI), 2012 IEEE 24th International Conference on. Bd. 1. IEEE. 2012, S. 41–48. 16