Picat -- eine Multiparadigmen-Programmiersprache

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