Funktionale Programmierung mit Constraints

Werbung
Seminar Programmiersprachen und Programmiersysteme
SS 2010
Seminarausarbeitung
Thema:
Funktionale Programmierung mit
Constraints
von Jan Rasmus Tikovsky
Betreuer: Prof. Dr. Michael Hanus
Literatur:
Tom Schrijvers, Peter Stuckey and Phil Wadler:
Monadic Constraint Programming
1
Inhaltsverzeichnis
1
Einleitung
3
2
Grundlagen
4
2.1
2.2
3
4
Monaden
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
2.1.1
Formale Denition . . . . . . . . . . . . . . . . . . . . . . . . .
4
2.1.2
Beispiel für die Anwendung von Monaden . . . . . . . . . . . .
5
Continuations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
MCP Framework Monadische Constraint Programmierung
7
3.1
Modellierung des Constraint-Problems . . . . . . . . . . . . . . . . . .
7
3.1.1
Konjunktive Constraint-Modelle
7
3.1.2
Erweiterung zu Disjunktiven Constraint-Modellen
. . . . . . .
9
3.1.3
Dynamische Modellierung . . . . . . . . . . . . . . . . . . . . .
10
3.2
Constraint Solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
3.3
Suchstrategien und -Transformer
. . . . . . . . . . . . . . . . . . . . .
14
. . . . . . . . . . . . . . . . .
3.3.1
Primitive Suchalgorithmen
. . . . . . . . . . . . . . . . . . . .
14
3.3.2
Einfache Such-Transformer
. . . . . . . . . . . . . . . . . . . .
15
3.3.3
Kombinierbare Such-Transformer . . . . . . . . . . . . . . . . .
18
Zusammenfassung und Ausblick
21
Literatur
22
Anhang
23
2
1
Einleitung
Constraint Programming beschäftigt sich allgemein gesprochen mit der Lösung von
Problemen durch die Beschreibung von Bedingungen oder Eigenschaften (Constraints),
die die Lösung dieses Problems erfüllen muss. Constraints können also auch als Fakten einer partiellen Lösung des Problems betrachtet werden, die die Eigenschaften
unbekannter Objekte und deren Beziehungen untereinander beschreiben. Diese Eigenschaften und Beziehungen werden durch Variablen modelliert, die ihrerseits einen
endlichen oder unendlichen Wertebereich haben können. Bei endlichem Wertebereich
spricht man auch von Finite Domain Constraint Problems.
Constraint Programming besteht üblicherweise aus zwei Teilen: einer Modellierungskomponente und einer Komponente zur Lösung des modellierten Problems (Solver).
Die Modellierung der Problemstellung und ihrer Constraints erfolgt auf deklarative
Weise mit Hilfe von Regeln. Die Lösung des Problems durch die Solver-Komponente
geschieht durch Speicherung, Kombination und Vereinfachung der Constraints durch
eektive Algorithmen. Dabei werden im Laufe der Bearbeitung Variablenbelegungen
ausprobiert und propagiert (Constraint Propagation), so dass der Wertebereich der
Constraint-Variablen eingeschränkt wird. Diese Vorgehensweise wird iterativ angewendet, bis eine Lösung gefunden wird oder ein Widerspruch auftritt. In diesem Fall
wird mit einer anderen Variablenbelegung neu begonnen (Backtracking).
Anwendungsgebiete des Constraint Programming sind neben der Lösung von Rätseln wie Sudoku oder n-Damen-Problem auch vermehrt im kommerziellen Bereich zu
nden, z. B. für die Erstellung von Fahr- und Stundenplänen sowie in der Produktionsplanung und Personaleinsatzplanung.
Einerseits kommen im Constraint Programming logische Programmiersprachen wie
PROLOG zum Einsatz, andererseits gibt es spezielle Softwaresysteme zur Modellierung und Lösung von Constraint-Problemen wie z.B. ECLiPSe oder Gecode (Generic
Constraint Development Environment). Der dieser Ausarbeitung zugrundeliegende
Artikel nutzt Abstraktionen und Mechanismen (Monaden, Funktionen höherer Ordnung, Continuations und lazy Evaluation) funktionaler Programmiersprachen, um ein
Constraint Programming System in Haskell zu implementieren. Dazu wurde ein Framework entwickelt bestehend aus
•
einer in Haskell eingebetteten Modellierungssprache (EDSL) zur Beschreibung
von Constraint-Problemen mittels einer Baumstruktur,
•
einer Solver-Einheit zur Auswertung des Modells mittels grundlegender Suchalgorithmen wie Breiten- und Tiefensuche und
•
einer Bibliothek aus kombinierbaren Such-Transformern, die die Implementierung komplexerer Suchalgorithmen ermöglichen.
Beispielhaft wird die Funktionalität des Frameworks
in dieser Ausarbeitung am n-Damen-Problem erläutert: Dabei soll ein n x n Felder groÿes Schachbrett
so mit n Damen besetzt werden, dass keine Dame eine andere schlagen kann. Das bedeutet, dass in jeder
Reihe, jeder Spalte sowie Diagonale darf jeweils nur
eine Dame platziert werden darf. Eine mögliche Lösung des 8-Damen-Problems zeigt das Bild links.
Abbildung
che
Lösung
1:
eine
des
mögli-
8-Damen-
Problems
3
2
Grundlagen
2.1
Monaden
Programme geschrieben in einer rein funktionalen Sprache wie Haskell sind im Grunde genommen nur Mengen von Funktionen, deren Ergebnis nur von den jeweiligen
Eingabeparametern abhängt. Ein Vorteil solcher Programme ist, dass die Reihenfolge
der Auswertung keine Rolle spielt. Da es auch keine Seiteneekte gibt, lässt sich die
Fehlerfreiheit solcher Programme leichter nachweisen und ermöglicht die sogenannte lazy Auswertung von Funktionen. Diese Seiteneektfreiheit kann aber auch zu
einem Nachteil werden: Es ist nämlich erheblicher Aufwand nötig, um Daten vom
Punkt ihrer Erzeugung zu ihrer letztendlichen Nutzung durchzureichen. Dadurch ist
eventuell der ursprüngliche Sinn der Funktion nur noch schwer erkennbar. Dieser
Nachteil kann durch den Einsatz von Monaden umgangen werden. Sie erlauben, erwünschte Seiteneekte (z.B. Zustandsänderungen, Fehlerbehandlung usw.) ohne den
beschriebenen (Durchreich-)Aufwand zu integrieren bzw. die Auswertungsreihenfolge
zu bestimmen.
2.1.1
Formale Denition
Formal lassen sich Monaden als Tripel (m,
return, (>>=)) denieren bestehend aus
einem einstelligen Typkonstruktor m und den zwei Operationen:
(1)
return
::
a
−>
m a
Diese Funktion macht aus einem einfachen Wert eine monadische Berechnung, die nur
diesen Wert zurückgibt und sonst nichts tut.
(2)
(>>=)
::
m a
−>
(a
−>
m b)
−>
m b
Dieser Operator - auch Bindeoperator genannt - dient dazu, die Auswertungsreihenfolge zu kontrollieren. Er führt zunächst eine monadische Berechnung aus und übergibt
deren Ergebnis an eine weitere monadische Berechnung. Die Monaden-Typklasse in
Haskell sieht wie folgt aus:
c l a s s Monad m where
(>>=)
::
m a
−>
(a
−>
−>
m b)
m b
return : : a −> m a
fail
: : String −> m a
f a i l s = error s
(>>)
::
m a
−>
m b
m >> k = m >>= \_
−>
−>
m b
k
Neben den bereits eingeführten Operationen gibt es noch eine Funktion zur Erzeugung
von monadischen Fehlermeldungen
fail
sowie den Sequenzoperator
(>>).
Dieser ist
durch den Bindeoperator deniert und verhält sich genauso bis auf die Tatsache, dass
das Ergebnis der ersten monadischen Berechnung nicht weitergereicht wird. Für den
Sequenzoperator gelten die folgenden Gesetze:
return ( ) >> m = m
return ( ) = m
m >>
m >> ( n >> o )
Das heiÿt,
(>>)
= (m >> n ) >> o
return ()
wirkt als eine Art neutrales Element und der Sequenzoperator
ist assoziativ. Beide Operationen bilden zusammen im mathematischen Sinne
einen Monoid. Anstelle der Operatoren
(>>)
und
(>>=)
kann man auch Haskells do-
Notation verwenden. Auf diese Weise sind Sequenzen besser lesbar. Die do-Notation
wird eingeleitet durch das Schlüsselwort do und die Länge der Sequenz wird durch
die O-Side-Rule bestimmt. Beispiel:
main =
getChar >>= \ c
−> putChar
4
c
Entsprechend in do-Notation:
do
getChar
putChar c
main =
c <−
2.1.2
Beispiel für die Anwendung von Monaden
Der Vorteil der Verwendung von Monaden soll am Beispiel eines einfachen Interpreters verdeutlicht werden. Er interpretiert Terme, bei denen es sich entweder um
Integerkonstanten oder Quotienten einer ganzzahligen Division zweier Integerwerte
handelt:
data Term = Con Int
|
Div Term Term
Deniert wird dieser simple Interpreter durch die Funktion:
Term
−> Int
eval
::
eval
( Con a ) = a
eval
( Div
t1
t2 ) = eval
` div `
t1
eval
t2
Ist der Dividend Null, so wirft der GHCi erst bei der Auswertung des Quotienten eine
divide by zero -Exception. Will man erreichen, dass der Interpreter schon frühzeitig
solch einen Fehlerfall abfängt, muss in jedem rekursiven Aufruf der eval-Funktion eine
Fehlersuche und -behandlung hinzufügt werden. Dazu muss sowohl die Datenstruktur
als auch die Interpreterfunktion angepasst werden:
data M a = R a i s e E x c e p t i o n
type E x c e p t i o n = String
eval
::
eval
( Con a )
Term
eval
( Div
t1
Raise
e
Return
a
−>
M
Return
Return
a
Int
= Return
t2 ) =
a
case e v a l
−> R a i s e e
−> case e v a l
e −> R a i s e e
b −>
Raise
|
t2
t1
of
of
i f b == 0
then R a i s e " d i v i d e by z e r o "
e l s e R e t u r n ( a ` div ` b )
Bei jedem Aufruf der eval-Funktion muss das Ergebnis geprüft werden: Wurde eine Exception geworfen, so wird diese durchgereicht, sonst wird mit dem erhaltenen
Wert weitergerechnet. Durch den Einsatz von Monaden lässt sich dieser Aufwand
jedoch reduzieren, indem man einen monadischen Term-Interpreter inklusive Fehlerbehandlung verwendet. Dazu deniert man die Monadenoperationen (a) sowie die
Interpreterfunktion (b) folgendermaÿen:
(a)
return a = R e t u r n a
= case m o f R a i s e
m >>= k
e
Return
raise
::
raise
e = Raise
Exception
−>
a
−>
−>
Raise
k
e
a
M a
e
Durch diese Denition ist implizit gewährleistet, dass bei der Verknüpfung von TermAuswertungen mit dem monadischen Bindeoperator eventuelle Exceptions durchgereicht werden.
(b)
eval
eval
eval
Int
return a
( Div t 1 t 2 ) = do
::
Term
−>
( Con a )
a <− e v a l
M
=
t1
5
b <− e v a l
t2
i f b == 0 then r a i s e " d i v i d e by z e r o "
e l s e return ( a ` div ` b ) ) )
Wird ein Quotient
(Div t1 t2)
ausgewertet, so wird zunächst die monadische Be-
rechnung (die Auswertung des Terms
t1) ausgeführt und deren Ergebnis dann an die
zweite monadische Berechnung (die Auswertung des Terms t2) weitergereicht. Dabei
wird das Ergebnis von
t2
auf Null überprüft und dementsprechend entweder eine
Exception geworfen oder der Quotient berechnet.
Mehr zu Monaden und weitere Beispiele ndet man in [3].
2.2
Continuations
Normalerweise sind Programme im sogenannten direkten Stil geschrieben. Das heiÿt,
nach der erfolgreichen Ausführung einer Funktion wird der berechnete Wert über
den Stack zurückgegeben und über die Rücksprungadresse zur aufrufenden Funktion
verzweigt, um die Berechnung dort fortzusetzen. Mit Hilfe von Continuations - im sogenannten Continuation-Passing-Style (CPS) denierten Funktionen - kann der Kontrolluss eines Programms abweichend von diesem normalen Ablauf explizit gesteuert
werden. Dazu erhält eine Funktion eine Nachfolgerfunktion als zusätzliches Argument.
Nach Ausführung der aufgerufenen Funktion wird nun nicht über die Rücksprungadresse zurückverzweigt, sondern diese Nachfolgerfunktion aufgerufen und zwar mit
dem berechneten Ergebnis der Ursprungsfunktion. Mit diesen Nachfolgerfunktionen,
die auch als Fortführungen bzw. im Englischen als Continuations bezeichnet werden, kann man nun beliebig lange Funktionsketten bilden, die garantiert unmittelbar
hintereinander ausgeführt werden.
Bei
ausschlieÿlicher Verwendung von Funktionen im CPS gilt: Weder Funk-
tionsergebnisse noch Rücksprungadressen werden auf dem Stack abgelegt, da keine
Funktion im traditionellen Sinne zur aufrufenden Funktion zurückspringt . Durch
Speicherung der aktuellen Continuation (Parameter und Folgefunktion) wird quasi
ein snapshot des aktuellen Programmzustands gespeichert. Mit Hilfe dieses Mechanismus kann das Programm zu einem späteren Zeitpunkt wieder an der Stelle und in
exakt dem festgehaltenen Zustand fortgesetzt werden, an der der snapshot erzeugt
wurde.
Beispielprogramm einmal im direkten Stil (1) und einmal im Continuation-PassingStyle (2):
(1)
(2)
Int
−> Int
|
n == 0
= 1
|
otherwise = n
fac
::
fac
n
fac ' cps
::
fac ' cps
n k
∗ Main>
Int
−>
fac
( Int
( n − 1)
−>
r)
−>
r
|
n == 0
|
otherwise = f a c ' c p s ( n − 1) ( \ facnm1
fac ' cps
= k
∗
5
1
( ` div `
−>
k
(n
2)
60
Weiterführende Informationen zu Continuations sind in [4] zu nden.
6
∗
facnm1 ) )
3
MCP Framework Monadische Constraint Programmierung
Das MCP Framework ist ein in der funktionalen Sprache Haskell geschriebenes, generisches Framework zur Modellierung und Lösung von Constraint-Problemen. Durch
die Verwendung verschiedener Abstraktionen erlaubt es dem Benutzer, ConstraintProbleme in einer baumartigen Struktur zu modellieren und eigene Constraint Solver
sowie Suchalgorithmen für diese Probleme zu implementieren.
3.1
Modellierung des Constraint-Problems
Zum Lösen eines Constraint-Problems muss man zunächst eine geeignete Darstellung
nden, mit der das Problem modelliert werden kann. Üblicherweise verwendet man
dazu eine sogenannte Modellierungssprache.
Das MCP Framework ist implementiert als eine Embedded Domain Specic Language
(EDSL). Es bettet eine Sprache zur Modellierung von Constraint-Problemen direkt
in die Programmiersprache Haskell ein.
3.1.1
Konjunktive Constraint-Modelle
Ein Modell für ein Constraint-Problem wird im MCP Framework mit Hilfe einer
baumartigen Datenstruktur repräsentiert, die wie folgt deniert ist: .
data T r e e
solver
= Return
|
NewVar
|
Add
a
a
( Term
solver
( Constraint
−>
Tree
solver )
solver
( Tree
a)
solver
a)
Der generische Datentyp Tree erhält zwei Typparameter: Durch die Typvariable
solver
wird das Modell an einen speziellen Constraint Solver und damit auch an einen konkreten Constraint-Wertebereich gebunden. Die Typvariable
a
ist ein im Baummodell
gespeicherter Typ, durch den die Tree-Datenstruktur zu einer Monade wird. Durch
diese beiden Typparameter ist die Typsicherheit des Modells für beliebige Solver- und
Ergebnistypen gewährleistet.
Diese Datenstruktur bietet zunächst nur die folgenden drei Konstruktoren:
• Return
ist der Basiskonstruktor. Er stellt ein triviales, erfüllbares Modell dar
und weist daher auf das Ende einer Modellierung hin.
•
Der Konstruktor
NewVar, der eine Funktion f als Parameter erhält, repräsentiert
ein um eine neue Constraint-Variable erweitertes Modell. Das Modell wird durch
die Funktion
•
Mit dem
f
erzeugt.
Add-Konstruktor
c erweitert.
wird ein bestehendes Modell
t
um ein zusätzliches
Constraint
Wie bereits erwähnt, soll
Tree solver zu einer Instanz von Haskells Monad-Typklasse
return und (>>=) werden dann wie folgt imple-
gemacht werden. Deren Funktionen
mentiert:
instance Monad ( T r e e
return = R e t u r n
(>>=)
solver )
where
= extendTree
( Return
x)
` extendTree `
k = k
( NewVar
f)
` extendTree `
k = NewVar
` extendTree `
k = Add c
( Add c
t)
x
7
(\ v
(t
−>
f
v
` extendTree `
` extendTree `
k)
k)
Durch den Aufruf von
return
wird ein triviales Modell erzeugt, das bei Auswertung
durch den Solver (siehe 3.2) den übergebenen Parameter zurückgibt.
Der monadische Bindeoperator
(>>=)
wird durch eine Funktion
extendtree
imple-
mentiert, die ein bestehendes Baummodell erweitert. Diese Erweiterung wird durch
das bestehende Modell bis zu einem
Return-Knoten
durchgereicht, der dann durch
die Erweiterung ersetzt wird. Die Verwendung von monadischen Modellen hat den
Vorteil, dass bei der Berechnung eines Werts als Seiteneekt ein Modell erzeugt wird.
Auÿerdem kann auf die neu denierten Operatoren
(>>)
und
(>>=)
zurückgegrien
und Haskells do-Notation verwendet werden (siehe unten).
Jetzt kann man das n-Damen-Problem mit der kennengelernten Baumstruktur modellieren:
nqueens n = e x i s t n $ \ queens −> model queens n
model queens n = queens ` a l l i n ` ( 1 , n ) /\
a l l d i f f e r e n t queens
/\
d i a g o n a l s queens
a l l i n queens r a n g e = c o n j [ q ` in_domain ` r a n g e | q <− queens ]
a l l d i f f e r e n t queens = c o n j [ q i @/= q j | q i : q j s <−
queens ,
q j <− q j s ]
d i a g o n a l s queens = c o n j [ q i @/= ( q j @+ d ) /\ q j @/= ( q i @+ d )
| q i : q j s <−
queens ,
( qj , d ) <−
qjs [ 1 . . ] ]
conj =
(/\) true
tails
tails
zip
foldl
Aus Platzgründen wird hier nur eine kurze textuelle Beschreibung der Hilfsfunktionen
angegeben. Ihre genaue Denition kann man im Anhang A.2 nachlesen.
exist n erzeugt eine Liste von n neuen Constraint-Variablen mit Hilfe
NewVar-Konstruktors, um die n Damen auf dem Schachbrett zu modellieren. Dann
ruft sie die Funktion model auf, die die nötigen Beschränkungen für die Variablen
in der Liste generiert. Dies geschieht durch Erzeugung eines Add-Knotens für jedes
Die Funktion
des
Constraint.
model
queens ein
allin
Beispielsweise fügt die von
aufgerufene Funktion
Variable aus der Liste
Constraint hinzu, das diese auf den Wertebereich
für jede Constraint-
alldifferent und diagonals erzeugen zusätzqueens Constraints, die diese derart beschränken,
{1,...,n} beschränkt. Die Funktionen
lich für alle Variablen aus der Liste
dass je zwei durch die Variablen modellierten Damen nicht in der gleichen Reihe und
nicht auf der gleichen Diagonalen stehen. Dazu wird der Operator
(@/=)
verwendet,
mit dem man Ungleichheit zwischen zwei Variablen ausdrücken kann. Die Beschränkung, dass in jeder Spalte des Schachbretts auch nur genau eine Dame steht, wird
durch die Constraint-Variablen selbst modelliert. Somit repräsentiert jede Variable
auch jeweils eine Spalte des Bretts.
Der Operator
(/\)
verknüpft die einzelnen Constraints zu einer Konjunktion von
Constraints. Er ist nur syntaktischer Zucker für den monadischen Sequenzoperator
und damit für die Funktion
extendTree. Er sorgt also dafür, dass die einzelnen geneReturn (), (Newvar f) und (Add c t)- zu
rierten Knoten des Modell-Datentyps -
einem (einzigen) Modell zusammengesetzt werden. Man spricht daher auch von einem
konjunktiven Constraint-Modell.
Für das 2-Damen-Problem erzeugt die obige Funktion dann beispielsweise das folgende
Modell (Unter Verwendung der Constraint- und Term-Typen aus dem Anhang A.1):
NewVar $
\ q1
NewVar $
Add
−>
−>
\ q2
(Dom q1
Add
1
(Dom q2
Add
( Diff
Add
2)
1
q1
( Diff
Add
$
2)
q1
( Diff
Return
$
q2 )
$
( Plus
q2
q2
( Plus
( Const
q1
()
8
1)))
( Const
$
1)))
$
3.1.2
Erweiterung zu Disjunktiven Constraint-Modellen
Bislang kann man mit der in Haskell eingebetteten Sprache zur Modellierung von
Constraint-Problemen nur Konjunktionen von Constraints beschreiben. Aber oftmals
sind solche Konjunktionen nicht ausreichend, um über ihre Erfüllbarkeit zu entscheiden. Um das Problem der Unvollständigkeit zu beheben, müssen solange weitere
Constraints hinzugefügt werden, bis eine Lösung gefunden oder Inkonsistenz erreicht
wird. Da nicht bekannt ist, welche Constraints hinzugefügt werden müssen, muss
man verschiedene Alternativen ausprobieren. Dazu erweitert man die oben vorgestellte Modell-Datenstruktur um zwei weitere Konstruktoren:
data T r e e
=
...
|
Try
|
Fail
Mit dem
solver
( Tree
a
solver
Try-Konstruktor
a)
( Tree
solver
a)
kann man Verzweigungen in einem Modell erzeugen. Da-
mit wird auch verständlich, warum das Modell stets als baumartige (Daten-)Struktur
bezeichnet wurde. Durch Einführung des
Try-Konstruktors
werden Modelle zu Bi-
närbäumen. Verwendet man solche Verzweigungen, so kann man mit dem linken und
rechten Teilbaum zwei disjunkte Belegungen der Constraint-Variablen formulieren.
Man kann unter Verwendung der
Try-Knoten
also verschiedene Variablenbelegungen
und damit mögliche Lösungen ausprobieren. Modelle, die diesen Konstruktor nutzen,
werden auch als disjunktive Modelle bezeichnet.
Der zweite neue Konstruktor
Fail
kommt in Zweigen des Modells vor, die zu keiner
Lösung führen.
Auch für die neuen Konstruktoren ist die Funktion zur Erweiterung des Modells deniert:
Fail
( Try
l
r)
` extendTree `
k = Fail
` extendTree `
k = Try
( l
` extendTree `
k)
(r
` extendTree `
k)
Fail-Knoten eine Sackgasse im Modell darstellt, wird er nicht erweitert.
Try-Knoten erweitert, wird diese Erweiterung auf seine beiden Zweige
Da ein
Falls man einen
angewandt.
Mit den neuen Konstruktoren ist es jetzt möglich, die verschiedenen Belegungen der
Variablen im Modell des n-Damen-Problems als disjunkte Alternativen aufzuzählen.
nqueens
n =
exist
n
$
\ queens
−>
model
queens
enumerate
enumerate
enum
var
queens
values = conj
values =
disj
=
disj
foldl
(\/)
/\
[
enum
|
q u e e n <− q u e e n s ]
[ v a r @= v a l u e
queen
n
queens
[1..n]
values
v a l u e <− v a l u e s ]
|
false
Zunächst werden durch den Aufruf der Funktion
model
wie im obigen Beispiel n
neue Constraint-Variablen und ein konjunktives Modell erzeugt. Die Variablen werden dabei den Bedingungen des n-Damen-Problems entsprechend beschränkt. Die
enumerate-Funktion sorgt dann mit ihrer Hilfsfunktion enum dafür, dass für jede Variable alle möglichen Belegungen aufgezählt werden.
Dazu wird mit der
enum-Funktion
zunächst für jede Variable eine Liste von n
Add-
Knoten erzeugt, wobei jeder dieser Knoten ein anderes Gleichheits-Constraint hinzufügt. Jedes dieser Gleichheits-Constraints steht wiederum für jeweils eine mögliche
Belegung dieser Variablen mit einem Wert aus der Menge {1,...,n}. Aus dieser Liste
von
Add-Knoten
(\/) Try-Konstruktor ist - für jede Va-
wird dann durch Aualtung mit dem Disjunktionsoperator
der nichts weiter als syntaktischer Zucker für den
riable ein einzelner Binärbaum mit Verzweigungen für alle alternativen Belegungen
erzeugt.
9
Die
enumerate-Funktion
faltet dann mit dem Konjunktionsoperator
(/\)
die Liste
dieser Binärbäume zu einem einzigen binären Modell auf. Das heiÿt, die Binärbäume
werden jetzt so zusammengesetzt, dass in dem Ergebnismodell jede Kombination von
möglichen Belegungen der Constraint-Variablen als ein Pfad vorkommt.
Zur Verdeutlichung wird hier der erhaltene Binärbaum mit den Aufzählungen aller
Variablenbelegungen für das 2-Damen-Problem angegeben:
Try
( Add
( Same
( Try
q1
( Const
( Add
1))
( Same
q2
Return
( Try
( Add
( Const
1))
())
( Same
q2
Return
( Const
2))
())
Fail )))
( Try
( Add
( Same
( Try
q1
( Add
( Const
2))
( Same
q2
Return
( Try
( Add
( Const
1))
())
( Same
q2
Return
( Const
2))
())
Fail )))
Fail )
3.1.3
Dynamische Modellierung
Das Aufzählen aller möglichen Variablenbelegungen in einem disjunktiven Modell ist
sehr aufwendig: Ein solches Modell des n-Damen-Problems hat
auch die Anzahl der
Try-Verzweigungen
nn
Blattknoten und
wächst mit der Problemgröÿe. Auÿerdem
sind viele Zweige des Modells einfach überüssig, weil die Variablenbelegungen dort
sofort zu Inkonsistenzen führen. Auch wenn durch Haskells Prinzip der lazy Auswertung nicht der gesamte Binärbaum erzeugt wird, wäre es ezienter den Baum unter
Einbeziehung von Informationen des Constraint Solvers dynamisch zu konstruieren.
Dazu nimmt man an, dass der hier verwendete Solver für nite domain-ConstraintProbleme zusätzlich zu den Funktionen der Solver-Typklasse (siehe Abschnitt 3.2) die
beiden folgenden Funktionen implementiert:
domain
::
FDTerm
value
::
FDTerm
Mit der Funktion
−>
−>
domain
FD
FD
[ Int ]
Int
kann man den Wertebereich einer Constraint-Variable be-
stimmen und zwar in Abhängigkeit des aktuellen Zustands des Solvers. Das bedeutet,
dass es sich nicht mehr um den statischen Wertebereich {1,...,n} für das n-DamenProblem handeln muss, sondern dass dieser Wertebereich durch Propagierung von
hinzugefügten Constraints gegebenenfalls weiter eingeschränkt wurde.
Die Funktion
value
gibt den Wert einer Constraint-Variable zurück, der ihr im ak-
tuellen Zustand des Solvers zugewiesen wurde.
Damit man anstelle der statischen Wertebereiche auf die dynamischen, durch die
Constraint-Propagierung möglicherweise kleineren Wertebereiche der Variablen zugreifen kann, muss man einen weiteren Konstruktor zur Modell-Datenstruktur hinzufügen:
data T r e e
=
|
solver
a
...
Dynamic
Dieser
( solver
( Tree
Dynamic-Konstruktor
solver
a ))
ermöglicht es, die Aufzählungen aller möglichen Varia-
blenbelegungen in einem disjunktiven Modell des n-Damen-Problems dynamisch zu
erzeugen und zwar abhängig vom Solver-Zustand:
10
nqueens
n
=
exist
n
$
\ queens
−>
model
queens
enumerate
enumerate
label
label
Die
= Dynamic
.
n
/\
queens
label
return ( )
( v : v s ) = do d <− domain v
return $ enum v d /\ e n u m e r a t e v s
[]
=
enumerate-Funktion
entspricht jetzt einem Aufruf des
Dynamic-Konstruktors,
label erhält. Die-
der als Parameter das Ergebnis einer neuen Hilfsfunktion namens
se bestimmt für jede Variable aus der Liste der Constraint-Variablen zunächst deren dynamischen Wertebereich und gibt diesen dann an die Funktion
Wie im obigen Beispiel erzeugt die
enum-Funktion
enum
weiter.
zu jeder Variablen einen Binär-
baum mit Verzweigungen für alle möglichen Belegungen. Diese hängen jetzt allerdings nicht mehr vom statischen sondern vom dynamischen Wertebereich der Variablen ab. Da der
Dynamic-Konstruktor
kein Modell als Parameter erwartet, sondern
eine Solver-Berechnung, die ein Modell einkapselt, ruft
return-Funktion
label
noch die monadische
des Solvers auf.
Bislang wurden die Constraint-Variablen zur Modellierung des n-Damen-Problems
immer in ihrer natürlichen Reihenfolge mit Werten belegt. Die Wahl der Reihenfolge
der Variablenbelegung hat keinen Einuss auf die Form der Lösungen, jedoch auf
die Gröÿe des Modells. Daher ist es durchaus sinnvoll, die
label-Funktion
derart
anzupassen, dass die Strategien zur Auswahl der Variablenreihenfolge aber auch zur
Wahl der Reihenfolge ihrer Belegung mit Werten dynamisch gewechselt werden kann.
Beispielsweise erhält man ein deutlich kleineres Modell, wenn man die Variable mit
dem am meisten eingeschränkten Wertebereich zuerst auswählt. Das heiÿt, man ordnet die Variablen aufsteigend nach der Gröÿe ihres (dynamischen) Wertebereichs an.
Durch die Bevorzugung der am meisten eingeschränkten Variable verhindert man das
frühzeitige Auftreten von Inkonsistenzen. Man nennt diese Strategie auch Heuristik
der maximal eingeschränkten Variablen bzw. im Englischen rst-fail-Prinzip.
Speziell für das n-Damen-Problem ist es zudem günstig bei der Belegung der Variablen zunächst Positionen aus der Mitte des Schachbretts auszuwählen und sich dann
langsam zu den Rändern des Bretts vorzuarbeiten. Dies führt zwar nicht zu einer
weiteren Verkleinerung des erzeugten Modells, kann aber Lösungen in den linken Teil
des Modells verschieben. Falls man das Modell von links nach rechts auswertet, kann
dies zu einem schnelleren Finden der Lösungen beitragen. Diese Strategie zur Auswahl
der Reihenfolge der Werte für die Variablenbelegungen bezeichnet man im Englischen
auch als middleout value ordering.
Die Implementierung dieser Strategien für die Auswahl der Variablen bzw. der Werte
aus dem Wertebereich ndet man im Anhang A.3.
3.2
Constraint Solver
Bislang wurde nur gezeigt, wie mit Hilfe der MCP-Modellierungssprache ConstraintProbleme durch eine monadische baumartige Datenstruktur repräsentiert werden können. Der zweite zentrale Bestandteil eines Constraint-Frameworks, nämlich die Einheit
zum Lösen eines Constraint-Problems (Constraint Solver bzw. Constraint-Löser) soll
in diesem Abschnitt vorgestellt werden.
Allgemein ist es das Ziel eines Constraint Solvers, den Wertebereich der ConstraintVariablen zu verkleinern oder die Anzahl der betrachteten Constraints zu verringern.
Gleichzeitig muss er weiterhin in einem konsistenten Zustand bleiben. Dazu wird das
Prinzip der Constraint-Propagierung angewandt. Dabei werden die Wertebereiche der
Constraint-Variablen iterativ durch eine lokale Betrachtung der gültigen Constraints
einschränkt, bis eine eindeutige Belegung aller Variablen gefunden wurde.
Der Zustand eines Constraint Solvers kann entweder konsistent, inkonsistent oder unbekannt sein. Er ergibt sich aus den im Constraint-Speicher vorhandenen Constraints
11
und deren Interpretation durch den Solver. Solange der Zustand nicht inkonsistent
ist, kann er durch Hinzufügen neuer Constraints erweitert werden.
Ein Constraint Solver im MCP Framework ist im Grunde genommen nur ein Interpreter für die bereits kennengelernten baumförmigen Modelle. Ein generisches Interface
für einen solchen Solver ist durch die folgende Typklasse gegeben:
c l a s s Monad s o l v e r => S o l v e r s o l v e r where
type C o n s t r a i n t s o l v e r : : ∗
type Term s o l v e r : : ∗
type L a b e l s o l v e r : : ∗
newvar
::
solver
add
::
Constraint
( Term
mark
::
solver
goto
::
Label
run
::
solver
solver )
solver
( Label
solver
−>
a
−>
Bool
solver
solver )
−>
solver
()
a
In dieser Denition der Typklasse wird verlangt, dass auch ein Constraint Solver eine
Monade ist. Denn man setzt im Allgemeinen voraus, dass der Solver eine zustandsorientierte Berechnung einkapselt, um seinen internen Zustand, den Constraint-Speicher,
vor dem Benutzer zu verbergen.
Des Weiteren muss ein Constraint-Typ angegeben werden, der vorgibt, welche Arten
von Constraints - z.B. Gleichheits-Constraint oder (Werte-)Bereichs-Constraint - verwendet werden (Beispiel siehe Anhang A.1). Der Term-Typ bestimmt, welche Form
die Terme haben, über die sich die Constraints erstrecken. Beispiele für Terme sind
Variablen, Konstanten, Summenterme etc. Schlieÿlich benötigt man auch noch einen
Label-Typ, der den internen Zustand eines Solvers repräsentiert. Ein Label ist entweder einfach eine Kopie des Zustands (copying) oder es ist ein Trace aller Operationen,
die zum Erreichen dieses Zustands geführt haben (trailing).
Auÿerdem verlangt das Interface, dass folgende Funktionen implementiert werden:
•
Die Funktion
newvar
ist das Solver-Gegenstück zum
NewVar-Konstruktor
des
Modells. Sie erzeugt als Berechnung des Solvers eine neue Constraint-Variable.
•
In der gleichen Weise ist die
add-Funktion das Solver-Pendant zum Add-Konstruk-
tor. Sie fügt ein neues Constraint zum Constraint-Speicher des Solvers hinzu und
gibt durch ihren booleschen Rückgabewert an, ob dessen Zustand danach noch
konsistent (True) ist oder nicht (False).
•
Die Funktion
mark
liefert das Label (Zustandsmarke) des aktuellen Zustands
eines Constraint Solvers.
•
Mit der
goto-Funktion
kann man den zum übergebenen Label zugehörigen Zu-
stand des Solvers wiederherstellen. Diese und die
mark-Funktion
benötigt man
für das Backtracking in disjunkten Modellen.
•
Die Funktion
run
führt eine Aktion in der Monade aus und entnimmt das Er-
gebnis aus der Berechnung des Solvers.
Um den vorgestellten Solver auf ein konkretes Modell eines Constraint-Problems anzusetzen und eine Lösung zu berechnen, verwendet man die folgenden Funktionen:
solve
::
Solver
s o l v e = run
eval
::
eval
model
.
Solver
eval '
( Return
eval '
( Add c
=> T r e e
solver
solver
a
−>
[a]
eval
solver
=> T r e e
= eval '
x)
t)
solver
model
a
−>
solver
[]
do x s <− c o n t i n u e w l
return ( x : x s )
w l = do b <− add c
i f b then e v a l ' t
wl
e l s e c o n t i n u e wl
wl =
12
[a]
f)
wl =
do v <− newvar
wl =
do now <− mark
eval '
( NewVar
eval '
( Try
eval '
Fail
wl = c o n t i n u e
eval '
( Dynamic m)
wl =
eval '
l
r)
(f
eval '
l
v)
wl
( ( now , r ) : w l )
wl
do t <− m
eval '
t
wl
Wie bereits erwähnt wurde, ist der Solver einfach nur ein Interpreter für Baummodelle,
der abhängig vom aktuell interpretierten Knoten spezielle Solver-Aktionen durchführt.
Verwendet wird dafür die Hilfsfunktion
eval',
die in jedem Schritt eine Arbeitsliste
von Tupeln mitführt. Jedes dieser Tupel speichert eine Zustandsmarke (label) des
Solvers zusammen mit dem in diesem Zustand noch zu interpretierenden Zweig des
Baummodells. Anfangs ist diese Liste - im pattern matching durch die Variable
wl
gematcht - leer.
Mit Hilfe der Funktion
continue (siehe unten) wird die Interpretation des Modells in
einem anderen Zweig als dem aktuellen - gespeichert in dieser Liste - fortgesetzt.
Wird beispielsweise ein
Return-Knoten
ausgewertet, so bedeutet dies, dass das Ende
des gerade interpretierten Zweigs im Baummodell erreicht wurde. Die Auswertung
wird dann per
continue
im nächsten Zweig gemäÿ der Liste fortgesetzt und das
Ergebnis des aktuellen Zweigs in die spätere Ergebnisliste eingetragen.
Interpretiert der Solver dagegen einen
per
add-Funktion
Add-Knoten,
so trägt er das neue Constraint
in seinen Constraint-Speicher ein. Abhängig von dem sich dadurch
ergebenden neuen Zustand entscheidet er, wo die Auswertung fortgesetzt wird: bei
Konsistenz im aktuellen Zweig, bei Inkonsistenz in einem anderen Zweig.
NewVar-Knoten auswertet, erzeugt er mit seiner
newvar-Funktion zunächst eine neue Constraint-Variable, die der Funktion f des
NewVar-Knotens übergeben wird. Dann wird die Interpretation auf dem durch die
Ausführung von f erzeugten Baummodell fortgesetzt.
Falls der Constraint Solver einen
Bei der Interpretation eines
Try-Knotens ermittelt der Solver zunächst das Label sei-
nes aktuellen Zustands und trägt dieses zusammen mit dem rechten Zweig (r) dieses
Knotens als Tupel am Anfang der Arbeitsliste ein. Auf diese Weise kann er später
seinen Zustand zurücksetzen und dann auch noch den rechten Zweig interpretieren
(Backtracking). Zunächst setzt er aber die Auswertung im linken Zweig (l) der Verzweigung fort.
Erreicht der Solver bei der Auswertung einen
Fail-Knoten, so bedeutet dies, dass der
continue wird
aktuelle Zweig des Modells zu keiner Lösung führt. Durch Aufruf von
in diesem Fall in einem anderen Zweig weiter nach Lösungen gesucht.
Für die Interpretation eines
Dynamic-Knotens wird das in der Solver-Berechnung ein-
gebettete Baummodell entnommen und weiter interpretiert.
continue
[]
continue
( ( p a s t , t ) : wl ) =
=
return [ ]
do g o t o p a s t
eval '
Wird die
continue-Funktion
t
wl
über einer leeren Arbeitsliste ausgeführt, so ist die In-
terpretation abgeschlossen und die eingekapselte Ergebnisliste wird zurückgegeben.
Das Ergebnis wird durch den Aufruf der
Ist diese Liste nicht leer, so wird per
run-Funktion
goto-Funktion
entnommen.
der dort im ersten Tupel ge-
speicherte frühere Zustand des Solvers wiederhergestellt und der im gleichen Tupel
gespeicherte Zweig als nächstes interpretiert.
13
3.3
Suchstrategien und -Transformer
Es wurde bereits vorgestellt, wie man Constraint-Probleme mit Hilfe des MCP Frameworks modelliert und das erzeugte Modell dann mit Hilfe des vorgestellten Constraint
Solvers interpretiert und löst. Dabei wurde allerdings nicht darauf eingegangen, welche Suchstrategien der Solver bei der Auswertung des Modells verwenden kann. Das
folgende Kapitel behandelt die Möglichkeiten, die das MCP Framework bietet, um
durch Such-Algorithmen und -Transformationen die Auswertungsreihenfolge zu beeinussen und zu steuern.
3.3.1
Primitive Suchalgorithmen
Die primitiven Suchalgorithmen bestimmen im Kontext des MCP Frameworks, in
welcher Reihenfolge die Zweige und Knoten eines Baummodells durch den Constraint
Solver interpretiert werden. Die Realisierung der Interpreterfunktion in Abschnitt 3.2
nutzt eine Arbeitsliste für die noch auszuwertenden Zweige, die dem LIFO-Prinzip
genügt und somit einem Stack entspricht. Es wird also ein Zweig des Modells so
lange ausgewertet, bis eine Inkonsistenz auftritt (Fail) bzw. der Zweig zu Ende ist
(Return
()).
Dann wird der nächste Zweig nach dem gleichen Prinzip ausgewertet.
Dies entspricht einer Tiefensuche.
Diese Suchstrategie muss aber nicht grundsätzlich die beste Wahl für die Interpreterfunktion des Constraint Solvers sein. Um auch andere Suchalgorithmen wie die
Breitensuche oder eine heuristisch über Prioritäten gesteuerte Suche (im Englischen
als best-rst search bezeichnet) zu realisieren, nutzt das MCP Framework das folgende Interface eines generischen Queue-Datentyps als Ersatz für die oben erwähnte
Arbeitsliste.
c l a s s Queue q where
type Elem q : : ∗
emptyQ
::
isEmptyQ
::
popQ
::
pushQ
::
Elem q
−> q
−> Bool
q −> ( Elem q , q )
Elem q −> q −> q
q
q
ist dabei der Typ der in der Queue gespeicherten Elemente. Ansonsten stellt
das Interface noch Funktionen zur Erzeugung einer leeren Queue, zum Prüfen auf
Leerheit, zum Entfernen eines Elements aus der Queue sowie zum Hinzufügen eines
Elements zur Queue zur Implementierung bereit.
Damit die Auswertungsfunktion des Constraint Solvers auf das Queue-Interface zurückgreifen kann, ändert man ihre Typsignatur wie folgt:
eval '
::
( Solver
Elem
=> T r e e
solver ,
q ~
Queue q ,
( Label
solver
a
−>
s o l v e r , Tree
q
−>
solver
solver
a ))
[a]
Der Typ der in der Queue gespeicherten Elemente entspricht also einem Tupel bestehend aus einer Zustandsmarke des Constraint Solvers und dem in diesem Zustand
noch nicht ausgewerteten Zweig des Modells.
An den Implementierungen der
eval'-
und
continue-Funktion
ändert sich kaum
etwas:
...
eval '
( Try
l
r)
wl
=
do now <− mark
continue
$
pushQ
( now , l )
$
pushQ
( now , r )
wl
...
continue
wl
|
isEmptyQ
|
otherwise
wl =
=
return [ ]
l e t ( ( p a s t , t ) , wl ' ) = popQ wl
in do g o t o p a s t
eval '
14
t
wl '
Zum Eintragen bzw. Entfernen eines Elements in die bzw. aus der Queue muss jetzt
pushQ- bzw. popQ-Funktion zurückgegrien werden. Bei der InterpreTry-Knotens werden jetzt beide Zweige, erst der rechte, dann der linke,
Queue eingetragen und anschlieÿend die continue-Funktion aufgerufen. Auf
nur auf deren
tation eines
in die
diese Weise kann man unterschiedliche Implementierungen der Queue-Typklasse nun
auch andere Suchalgorithmen als die Tiefensuche realisieren.
Der Vorteil dieser Implementierung ist, dass man neue primitive Suchalgorithmen jetzt
einfach denieren kann, indem man eine neue Instanz der Queue-Typklasse deniert.
Der Code für die Interpreterfunktion des Constraint Solvers muss nicht umgeschrieben werden, da er nur auf die dann neu denierten Funktionen des Queue-Interfaces
zugreift. Um die Tiefen-, die Breiten- und die best-rst Suche zu realisieren, implementiert man die Queue-Typklasse jetzt einfach als Stack, FIFO-Queue bzw. Priority
Queue.
3.3.2
Einfache Such-Transformer
Aufbauend auf den kennengelernten primitiven Algorithmen wie der Tiefensuche können weitere komplexere Suchalgorithmen implementiert werden. Dazu führt das MCP
Framework sogenannte Such-Transformer ein, die Transformationen des zugrundeliegenden Baummodells darstellen. Beispiele für solche Transformationen sind Kürzungen des Baumes wie das Abschneiden ab einer bestimmten Knotentiefe (tiefenbeschränkte Suche) oder das zufallsgesteuerte Vertauschen von Zweigen im Modell.
Vorstellung des Transformer-Interface
Das Verhalten eines solcher Transformers wird durch die folgende Typklasse beschrieben, die er implementieren muss:
c l a s s T r a n s f o r m e r t where
type E v a l S t a t e t : : ∗
type T r e e S t a t e t : : ∗
leftT ,
rightT
leftT _
::
t
−>
TreeState
t
−>
TreeState
t
id
=
rightT _ = l e f t T
nextT
::
SearchSig
solver
q
t
a
nextT = e v a l '
initT
::
t
−>
Die beiden Typen
( EvalState
t , TreeState
t)
EvalState (Auswertungsstatus) und TreeState (Baumstatus) be-
stimmen den internen Zustand eines Such-Transformers anhand dessen er entscheidet, wie die Suche weiter verlaufen soll. Während der
EvalState von einem durch die
Interpreterfunktion auszuwertenden Knoten zum nächsten weitergereicht wird, wird
der
TreeState
nur bei einem Übergang von einem Vater- zu einem Kindknoten im
EvalState wird bei jedem Schritt
TreeState wird jeweils nur in einem Zweig
Modell durchgereicht. Anders formuliert: Der
der
Auswertung weitergegeben und der
des
Modells durchgereicht. Der Sinn und Zweck dieser beiden Zustandstypen wird in den
konkreten Beispielen weiter unten deutlich.
Mit den Funktionen
Übergang von einem
leftT und rightT kann man denieren, wie der TreeState beim
Try-Knoten auf dessen linken bzw. rechten Kindknoten weiterge-
geben werden soll, also die Vererbung des Baumstatus vom Vater- an die Kindknoten. In der Default-Implementierung verhält sich
rightT
wie
leftT
leftTTreeState
und die
Funktion kopiert durch die Verwendung der Identitätsfunktion einfach den
des Vaterknotens.
Wie bereits erwähnt, werden Such-Transformer auf primitive Suchstrategien aufgesetzt. Sie entscheiden vor der Auswertung eines jeden Knotens, wie bzw. an welcher
Stelle im Modell die Suche fortgeführt werden soll, und reichen dann den entsprechenden Knoten an den zugrunde liegenden Suchalgorithmus weiter. Konkret wird dafür
die
nextT-Funktion verwendet, die
eval' aufruft. Die beiden
funktion
per Default einfach die angepasste AuswertungsFunktionen haben die folgende Typsignatur:
15
type S e a r c h S i g
( Solver
Elem
q ~
=> T r e e
−>
solver
solver ,
( Label
t
a =
Transformer
s o l v e r , Tree
solver
−>
a
solver
q
Queue q ,
q
−>
t
solver
−>
t ,
a , TreeState
EvalState
t
−>
t ))
TreeState
t
[a]
Die Elemente der Queue sind jetzt Tripel. Sie enthalten eine Zustandsmarke des Solvers sowie den in diesem Zustand noch nicht ausgewerteten Zweig des Modells und den
TreeState. Des Weiteren erhalten die Funktionen nextT und
eval' den verwendeten Transformer sowie dessen Zustand gegeben durch EvalState
und TreeState als zusätzliche Parameter (im Vergleich zur eval'-Funktion aus Abzum Zustand gehörigen
schnitt 3.3.1).
Die Implementierung der
eval'-Funktion und der continue-Funktion passt man fol-
gendermaÿen an:
eval '
( Try
l
r)
wl
t
es
ts =
do now <− mark
l e t wl ' = pushQ ( now , l , l e f t T
pushQ
continue
continue
wl
t
wl '
t
( now , r , r i g h t T
t
ts )
t
$
ts )
wl
es
es
|
isEmptyQ
|
otherwise
wl =
=
return [ ]
l e t ( ( p a s t , t r e e , t s ) , wl ' ) = popQ wl
in do g o t o p a s t
nextT
Bei der Interpretation eines
Try-Knotens
tree
wl '
t
es
ts
TreeState
leftT bzw. rightT
wird der TreeState
wird jetzt zusätzlich der neue
für den linken bzw. rechten Kindknoten mit Hilfe der Funktionen
bestimmt und in die Arbeitsliste (Queue) eingetragen. Dadurch
wie oben beschrieben stets vom Vater- an die Kindknoten durchgereicht
EvalState in analoger Weise von einem auszuwertenden Knoten zum nächscontinue-Funktion neben dem verwendeten Transformer auch dessen EvalState als zusätzlichen Parameter. Anstelle der eval'-Funktion
ruft sie nun die nextT-Funktion auf. So entscheidet der Transformer, ob und wenn ja
Um den
ten weiterzureichen, erhält die
an welcher Stelle die Auswertung des Modells fortgesetzt wird. Das Verhalten eines
Transformers hängt dabei von der konkreten Denition seiner Funktionen ab (siehe
Beispiele).
Die Funktion
initT
der Transformer-Typklasse dient, wie der Name vermuten lässt,
dazu den Zustand eines Transformers zu initialisieren, bevor mit der Interpretation
eines Modells begonnen wird.
Implementierungsbeispiele für Such-Transformer
Nun kann man das vorgestellte Interface nutzen, um konkrete Transformationen zu
implementieren. Beispielsweise kann man einen Transformer denieren, der dafür
sorgt, dass nur Knoten bis zu einer bestimmten Tiefe im Baummodell ausgewertet
werden und alle darunterliegenden Knoten quasi abgeschnitten werden. Ein solcher
Transformer mit Tiefenbeschränkung lässt sich folgendermaÿen implementieren:
newtype DepthBoundedST = DBST Int
instance T r a n s f o r m e r DepthBoundedST where
type E v a l S t a t e DepthBoundedST = Bool
type T r e e S t a t e DepthBoundedST = Int
i n i t T (DBST n ) = ( False , n )
leftT _ ts
nextT
tree
= ts
q
t
es
−
1
ts
|
t s == 0
|
otherwise = e v a l '
q
t
True
tree
q
t
= continue
es
ts
Die Tiefe n, ab der das Modell abgeschnitten werden soll, wird im
Wert eingebettet. Der
EvalState
DepthBoundedST-
gibt durch einen booleschen Wert an, ob das aus-
zuwertende Modell bereits abgeschnitten wurde (True) oder nicht (False). Mit dem
16
TreeState
wird gezählt, bis zu welcher Tiefe - relativ zur Tiefe des aktuellen Kno-
tens - der Baum in diesem Zweig noch ausgewertet werden darf. Initialisiert werden
beide Zustände daher mit False bzw. dem Wert für die Tiefenbeschränkung n. Die
leftT-Funktion (und damit gemäÿ Interface-Denition auch die der
rightT-Funktion) sorgt dafür, dass jedes Mal wenn der linke bzw. rechte Zweig eines
Try-Knotens zur weiteren Auswertung ausgewählt wird, der TreeState und damit
der Wert für die relative Tiefe dekrementiert wird. Die nextT-Funktion beschreibt
Denition der
schlieÿlich das eigentliche Verhalten dieses Transformers: Wurde die maximal zulässige Tiefe im aktuellen Zweig erreicht, so wird der
EvalState
auf True gesetzt. Die
Auswertung wird dann in einem anderen Zweig des Modells fortgesetzt, der durch den
zugrunde liegenden Suchalgorithmus bestimmt wird. Ansonsten wird die Interpretation des Modells an der aktuellen Position fortgeführt und der gegenwärtige Zustand
des Transformers durchgereicht.
In ähnlicher Weise kann man weitere Transformer denieren, die das Modell an bestimmten Stellen verkleinern, indem sie Zweige abschneiden. Beispielsweise kann man
die Auswertung auf eine feste Anzahl von Knoten beschränken (Transformer
mit
Knotenbeschränkung). Eine weiteres Beispiel sind Transformer mit beschränkter Abweichung (Englisch: Limited Discrepancy Transformer), die eine maximale
Anzahl dafür festlegen, wieviele rechte Verzweigungen bei der Auswertung eines Pfades durch den Baum verfolgt werden dürfen.
In diesen Beispielen wirkt der Transformer immer dann auf den Interpreter ein, wenn
dieser auf die Queue zugreift. Das MCP Framework ermöglicht es aber auch Transformer zu denieren, die an anderen Stellen auf den Ablauf der Auswertung einwirken.
So wäre beispielsweise ein Transformer denkbar, der die Auswertungsreihenfolge des
linken und rechten Zweigs eines
eines solchen
Try-Knotens
zufällig festlegt. Die Implementierung
Zufallsgesteuerten Transformers sieht wie folgt aus:
newtype RandomizeST = RDST Int
instance T r a n s f o r m e r RandomizeST where
type E v a l S t a t e RandomizeST = [ Bool ]
type T r e e S t a t e RandomizeST = ( )
i n i t T (RDST s e e d ) = ( randoms $ mkStdGen s e e d , ( ) )
tryT
( Try
l
r)
q
t
i f b then e v a l '
else eval '
Für den
EvalState
( b : bs )
ts =
( Try
r
l )
q
t
bs
ts
( Try
l
r)
q
t
bs
ts
verwendet man eine zufällig erzeugte Liste von booleschen Wer-
ten. Initialisiert wird diese durch den Aufruf eines Zufallswert-Generators, der eine
unendliche lazy Liste von Zufallswerten (in diesem Fall vom Typ Bool) erzeugt. Der
TreeState
() initialisiert.
leftT-, rightT- und nextT-Funktion. Stattdessen
deniert dieser Transformer eine Funktion namens tryT, die abhängig von den zufällig
erzeugten booleschen Werten in der Liste entscheidet, ob die Zweige eines Try-Knotens
wird für diesen Transformer nicht benötigt und daher mit
Ebenfalls nicht benötigt werden die
vor der Auswertung vertauscht werden. Falls der oberste Wert aus dieser Liste True
ist, so werden die beiden Zweige vertauscht, anderenfalls bleibt die ursprüngliche Reihenfolge erhalten. In beiden Fällen wird die Interpreterfunktion auf den (gegebenfalls
modizierten)
Try-Knoten
angesetzt, wobei der aktualisierte
EvalState
(= Restliste
der booleschen Werte) weitergegeben wird. Schlieÿlich bestimmt der primitive Suchalgorithmus, in welchem Zweig die Auswertung fortgesetzt wird.
Mit dem vorgegebenen Transformer-Interface des MCP Frameworks ist die Umsetzung
vieler weiterer Transformationen denkbar, beispielsweise
•
Suche nur der ersten k Lösungen eines Problems,
•
Wechsel der Strategie für die Auswahl der Constraint-Variablen ab einer bestimmten Tiefe oder
•
Variation der Reihenfolge für die Variablenbelegung ab einer bestimmten Tiefe.
17
3.3.3
Kombinierbare Such-Transformer
Bisher wurden nur einfache Transformationen des Modells und damit auch der Suche nach Lösungen präsentiert. Die Kombination mehrerer solcher Transformationen
führt in der Regel zu einem komplexeren jedoch auch ezienteren Transformer. Kombiniert man beispielsweise einen Transformer mit Knotenbeschränkung mit einem mit
Tiefenbeschränkung, so wird das Modell an einer anderen Stelle gekürzt als bei unabhängiger Anwendung der jeweiligen einfachen Transformer.
Da das im Abschnitt 3.3.2 vorgestellte Transformer-Interface keine Unterstützung für
das Zusammensetzen mehrerer Transformer bietet, führt das MCP Framework ein
neues Interface ein. Dieses erlaubt es dem Benutzer, auf unkomplizierte Weise die bereits vorgestellten einfachen Transformer zu beliebig komplexen Transformern zusammenzubauen. Dazu stellt das Framework die CTransformer-Typklasse (für Composable Transformer, also zusammensetzbare Transformer) zur Verfügung. Dieses Interface
deniert Typen und Funktionen, die bis auf ihre Benennung im Grunde genommen
denen des Transformer-Interfaces entsprechen. Zum Beispiel wird der Zustand eines
CEvalState
kombinierbaren Transformers durch die beiden Typen
und
CTreeState
nextCT-
bestimmt. Der einzige wirkliche Unterschied steckt in der Typsignatur der
Funktion:
type C S e a r c h S i g
( Solver
=> T r e e
−>
solver
solver ,
solver
c
a
−>
c)
−> C E v a l S t a t e
a ) −> (CONTINUE
c
(EVAL
solver
c
−>
solver
[a]
Im Unterschied zur
a =
CTransformer
nextT-Funktion
c
−>
CTreeState
solver
c
c
a)
der Transformer-Typklasse greift diese Funktion
nicht mehr auf die Queue-Datenstruktur zurück. Dafür erwartet sie zwei zusätzliche
Parameter, deren Typen folgendermaÿen deniert sind:
type EVAL s o l v e r
c
a
= ( Tree
−>
type CONTINUE s o l v e r
c
solver
solver
a = ( CEvalState
a
−>
CEvalState
c
[a])
−>
c
solver
[a])
Bei diesen beiden Parametern handelt es sich um Continuations, also um Funktionen, die nach Beendigung der
nextCT-Funktion
ausgeführt werden. Sie erhalten den
Rückgabewert dieser Funktion, um damit weiterzurechnen. Ein Transformer wird nun
nicht mehr notwendigerweise aufbauend auf einem primitiven Suchalgorithmus wie
der Tiefensuche ausgeführt, sondern eventuell aufgesetzt auf einen Stack von anderen
Transformern. Deshalb darf er nicht mehr direkt die Funktionen
eval' und continue
zur Steuerung des Ablaufs der weiteren Auswertung aufrufen, denn damit würde er
(EVAL
solver c a) bzw. (CONTINUE solver c a) der eval'- bzw. continue-Funktion sordie anderen Transformer aus der Kombination übergehen. Die Abstraktionen
gen dafür, dass die anderen Transformer auf dem Stack berücksichtigt werden, bevor
die eigentlichen Funktionen, also
eval'
bzw.
Zusätzlich führt man eine Datenstruktur
continue,
aufgerufen werden.
Composition ein, mit der man zwei CTrans-
former zu einem zusammenfassen kann:
data C o m p o s i t i o n e s
(: −)
::
ts
( CTransformer
=> a
−>
−>
where
a,
CTransformer
b)
b
Composition
( CEvalState
a , CEvalState
b)
( CTreeState
a , CTreeState
b)
Dazu verwendet man den Kompositionskonstruktor
(:-). Eine Komposition speichert
nur die internen Zustände der kombinierten Transformer in Form zweier Tupel, eines
für die Auswertungsstatus und eines für die Baumstatus. Alle anderen Bestandteile
der Transformer sind nicht sichtbar.
Die Idee ist, dass eine solche Komposition sich nun selbst wieder wie ein CTransformer,
also ein kombinierbarer Transformer, verhält. Dies wird gewährleistet, indem man die
Composition-Datenstruktur
zu einer Instanz der CTransformer-Typklasse macht:
18
instance CTransformer ( Composition e s t s ) where
type CEvalState ( Composition e s t s ) = e s
type CTreeState ( Composition e s t s ) = t s
initCT ( c1 : − c2 ) = let ( es1 , t s 1 ) = initCT c1
( es2 , t s 2 ) = initCT c2
in ( ( es1 , e s 2 ) , ( t s 1 , t s 2 ) )
l e f t C T ( c1 : − c2 ) ( t s 1 , t s 2 ) = ( l e f t C T c1 t s 1 , l e f t C T c2 t s 2 )
rightCT ( c1 : − c2 ) ( t s 1 , t s 2 ) = ( rightCT c1 t s 1 , rightCT c2 t s 2 )
nextCT t r e e ( c1 : − c2 ) ( es1 , e s 2 ) ( t s 1 , t s 2 ) e v a l ' c o n t i n u e =
nextCT t r e e c1 e s 1 t s 1
( \ t r e e ' es1 ' −> nextCT t r e e ' c2 e s 2 t s 2
( \ t r e e ' ' es2 ' −> e v a l ' t r e e ' ' ( es1 ' , es2 ' ) )
( \ es2 ' −> c o n t i n u e ( es1 ' , es2 ' ) ) )
( \ es1 ' −> c o n t i n u e ( es1 ' , e s 2 ) )
EvalinitCT-Funktion zuder des anderen (c2) in-
Um den Zustand einer solchen Komposition gegeben durch die Tupel für den
und den
TreeState
zu initialisieren, werden durch Aufruf der
nächst der Zustand des einen Transformers (c1) und dann
itialisiert.
leftCT und rightCT
rightCT-Funktionen der an
Nach dem gleichen Prinzip deniert man die Funktionen
auf
Kompositionen. Dazu wendet man die
der
leftCT-
und
Komposition beteiligten Transformer komponentenweise an, das heiÿt für den jeweiligen
TreeState
die zugehörige Funktion.
nextCT-Funktion auf Kompositionen von Transformern ist im CPS (Continuationeval'
und continue. Zunächst wird die nextCT-Funktion des ersten Transformers aus der
Die
Passing-Style) deniert. Als Continuation-Parameter erhält sie die Funktion
Komposition aufgerufen, die ihrerseits zwei lambda-Funktionen als ContinuationParameter erhält. Falls der erste Transformer entscheidet die Auswertung an der
aktuellen Stelle im Modell fortzusetzen, so wird die
Komponente aufgerufen (erste lambda-Funktion bzw.
Transformers
c1).
nextCT-Funktion der zweiten
EVAL-Continuation des ersten
Bestimmt der erste Transformer dagegen mit der Auswertung in
einem anderen Zweig des Modells fortzufahren, so wird der zweite Transformer aus
continue-Funktion einfach übersprungen
CONTINUE-Continuation des ersten Transformers c1).
der Komposition durch direkten Aufruf der
(zweite lambda-Funktion bzw.
Die
nextCT-Funktion
der zweiten Komponente verwendet ebenfalls zwei lambda-
Funktionen als Continuation-Parameter. Für den Fall, dass der zweite Transformer
festlegt, die Auswertung im aktuellen Zweig des Modells fortzusetzen, so wird die
eval'-Funktion
aufgerufen (EVAL-Continuation des zweiten Transformers
c2).
An-
sonsten sorgt der zweite Transformer aus der Komposition für die Ausführung der
continue-Funktion (CONTINUE-Continuation
des zweiten Transformers
c2).
Um nun zu erreichen, dass sich ein kombinierbarer Transformer - hierbei kann es
sich auch um mehrere in einer Komposition zusammengefasste Transformer handeln
- wieder wie ein gewöhnlicher Transformer verhält, führt das MCP Framework den
TStack-Transformer ein:
data TStack e s
TStack
::
where
ts
CTransformer
=> c
−>
TStack
c
( CEvalState
c)
( CTreeState
c)
Der TStack-Transformer speichert den Zustand eines übergebenen, kombinierbaren
Transformers. Damit sich ein solcher Transformer wieder wie ein gewöhnlicher Transformer verhält, macht man ihn zu einer Instanz der Transformer-Typklasse, die im
Abschnitt 3.3.2 vorgestellt wurde:
instance T r a n s f o r m e r ( TStack e s t s ) where
type E v a l S t a t e ( TStack e s t s ) = e s
type T r e e S t a t e ( TStack e s t s ) = t s
initT
( TStack
c)
= initCT
c
leftT
( TStack
c)
= leftCT
c
rightT
( TStack
c ) = rightCT
c
19
nextT
tree
nextCT
q t@ ( TStack
tree
(\ tree '
c
es
es '
−>
−>
(\ es '
c)
es
ts =
ts
eval '
tree '
continue
q
q
t
t
es '
ts )
es ' )
Die Funktionen der Transformer-Typklasse werden durch ihr jeweiliges Pendant aus
dem kombinierbaren Transformer implementiert, den man an den TStack-Transformer
übergibt. Die
nextT-Funktion wird durch die nextCT-Funktion deniert. Diese vereval' als EVAL-Continuation und die Funktion continue als
wendet die Funktion
CONTINUE-Continuation.
Mit den vorgestellten Werkzeugen des MCP Frameworks kann man jetzt einen Transformer mit einer Tiefenbeschränkung von 5 und einen mit einer Knotenbeschränkung
von 12 folgendermaÿen zu einem einfachen Transformer zusammensetzen:
( TStack
(CDBST 5
: − CNBST 1 2 ) )
Dazu muss man nur das Interface für kombinierbare Transformer entsprechend für
die beiden Transformer-Varianten implementieren, die implementierten CTransformer mit Hilfe des Kompositionskonstruktors
(:-)
zu einem neuen CTransformer zu-
sammensetzen und diesen dann an den TStack-Transformer übergeben. Das folgende
Bild veranschaulicht die Wirkung zweier zusammengesetzter Transformer anhand des
obigen Beispiels.
Abbildung 2: Tiefensuche (oben) und Breitensuche (unten) mit Tiefenbeschränkung
5, Knotenbeschränkung 12 - nur schwarze Knoten werden besucht
Das MCP Framework stellt in einer Bibliothek eine ganze Reihe von solchen kombinierbaren Transformern zur Verfügung, die der Benutzer beliebig zusammensetzen kann, um neue, komplexere Transformationen für die Auswertung seines Modells
durch den Constraint Solver zu erzeugen.
Darunter sind auch bekannte Optimierungstechniken wie Branch-and-Bound. Hierbei
handelt es sich um ein Verfahren zum Finden optimaler Lösungen. Nachdem eine
Lösung gefunden wurde, wird versucht, eine bessere zu nden. Weitere Verfahren, die
sich mit Hilfe des MCP Frameworks implementieren lassen, sind Iterative Deepening
(iterative Tiefensuche) oder Restart Optimization.
20
4
Zusammenfassung und Ausblick
Das MCP Framework ist ein generisches Framework zur Modellierung und Lösung
von Constraint-Problemen. Es stellt eine in Haskell eingebettete Modellierungssprache zur Repräsentation von Constraint-Problemen als eine monadische, baumartige
Struktur zur Verfügung. Die Verwendung von Monaden hat den Vorteil, dass Modelle als Seiteneekte von Berechnungen erzeugt werden, und ermöglicht es, Haskells
imperative do-Notation zu nutzen.
Weiterhin bietet das Framework einen monadischen Constraint Solver, der als Interpreter für die erzeugten Modelle dient. Abhängig von der Art des aktuell interpretierten Knotens des Baummodells führt er verschiedene Aktionen wie das Hinzufügen
neuer Constraints in seinen Constraint-Speicher oder das Erzeugen neuer ConstraintVariablen durch. Primitive Suchalgorithmen wie Breiten- oder Tiefensuche werden
durch die Implementierung eines generischen Queue-Interfaces bereitgestellt. Auf diese Weise wird die Auswertungsreihenfolge bei der Interpretation eines Modells durch
den Solver gesteuert.
Des Weiteren ermöglicht das MCP Framework während der Auswertung Transformationen am Modell vorzunehmen. Diese sind auf die grundlegenden Suchstrategien
aufgesetzt und modizieren das Baummodell und damit die Suche nach Lösungen
(beispielweise durch Abschneiden des Baums an bestimmten Stellen). Dazu stellt das
Framework ein Transformer-Interface zur Verfügung, das für einige konkrete Transformationen wie die Tiefenbeschränkung implementiert wurde.
Schlieÿlich bietet es noch die Möglichkeit, durch Implementierung eines Interface für
zusammensetzbare Transformer und Verwendung eines speziellen TStack-Transformers
beliebig viele gewöhnliche Transformer zu einer einzigen, komplexeren Transformation
zu kombinieren.
Aufbauend auf dem bisherigen MCP Framework haben die Entwickler mittlerweile ein
weiteres Framework speziell für Constraint-Probleme mit endlichen Wertebereichen
(Finite Domain) implementiert, das FD-MCP Framework (siehe [2]). Mit dieser Erweiterung ist es einerseits möglich, die auch in dieser Ausarbeitung vorgestellten Haskellbasierten Constraint Solver für die Lösung eines FD-Constraint-Problems zu nutzen.
Andererseits kann man auch einen in C++ implementierten Solver namens Gecode
(siehe [5]) als Backend für das mit der FD-MCP-Modellierungssprache beschriebene
Modell verwenden. Dazu stellt das Framework zum Einen eine in Haskell geschriebene Abstraktion des Gecode-Solvers mit entsprechenden Term- und Constraint-Typen
zur Verfügung, sowie eine Funktion, mit der sich generische FD-Modelle in GecodeModelle übersetzen lassen. Zum Anderen gibt es eine Funktion, die ein in Haskell
erzeugtes Gecode-Modell in entsprechenden C++ Code übersetzt.
Mit einem Benchmark haben die Entwickler festgestellt, dass die mit dem MCP Framework geschriebenen Programme zur Modellierung eines Constraint-Problems nur
halb so lang sind, wie ihr Gecode C++ Pendant. Der in C++ übersetzte Code eines solchen Programms ist zwar deutlich länger als die direkt in Gecode modellierte
Variante, erreicht aber die gleiche Performance.
Folgende Forschungsschwerpunkte sind für die Zukunft geplant:
•
Generalisierung des Frameworks für beliebige Suchalgorithmen
•
Weitergehende Erforschung der Performance-Charakteristik des Frameworks wie
z.B. den durch Transformer erzeugten Overhead
•
Erzeugung ezienter Suchalgorithmen für Gecode aus High Level Spezikationen mit Hilfe der kombinierbaren Such-Transformer des MCP Frameworks
•
Entwicklung einer zusätzlichen Instanz des FD-MCP Frameworks als LaufzeitFrontend für Gecode zur Vermeidung unnötiger Übersetzungsschritte
21
Literatur
[1] Tom Schrijvers, Peter Stuckey and Phil Wadler. Monadic Constraint Programming. Journal of Functional Programming, 2009.
[2] Pieter Wuille and Tom Schrijvers: Monadic Constraint Programming with Gecode.
In Proceedings of the 8th International Workshop on Constraint Modelling and
Reformulation, pages 171185, 2009
[3] Philip Wadler. Monads for Functional Programming. In Advanced Functional Programming, First International Spring School on Advanced Functional Programming Techniques-Tutorial Text, pages24-52, London, UK, 1995.
[4] defmacro.org. Functional Programming for the Rest of Us - Abschnitt Continuations, Link:
http://www.defmacro.org/ramblings/fp.html#part_9
[5] GecodeTeam.
Available from
Gecode:Generic
Constraint
http://www.gecode.org
Development
Environment,
2006.
[6] Frank Huch. Script zur Vorlesung Funktionale Programmierung - Institut für
Informatik, Arbeitsgruppe für Programmiersprachen und Übersetzerkonstruktion,
CAU Kiel
22
Anhang
A.1 Unterstützte Constraints und Terme für einen einfachen Finite DomainSolver:
data F D C o n s t r a i n t s =
Less
( FDExpr
s)
( FDExpr
s)
Diff
( FDExpr
s)
( FDExpr
s)
|
Same
( FDExpr
( FDExpr
s)
|
Dom ( FDExpr
|
|
s)
Int Int
s)
−−
−−
−−
−−
[ Less x y ]
[ Diff x y ]
[ Same x y ]
[Dom x y z ]
=
=
=
=
[x]
[x]
[x]
[x]
−−
−−
−−
−−
−−
[ Var v ]
[ Const n ]
[ Plus x y ]
[ Minus x y ]
[ Mult x y ]
=
=
=
=
=
[v]
n
[x] + [y]
[x] − [y]
[x] ∗ [y]
...
data FDExpr s = Var ( FDTerm s )
| C o n s t Int
|
Plus
|
Minus
( FDExpr
|
Mult
|
...
s)
( FDExpr
( FDExpr
( FDExpr
s)
s)
s)
( FDExpr
( FDExpr
s)
s)
< [y]
/= [ y ]
== [ y ]
in {y , . . . , z}
A.2 Syntaktischer Zucker für die Modellierung von Constraint-Problemen:
exist
n k = f
n
[]
where f 0 a c c = k a c c
f
v
e
n
a c c = NewVar $
` in_domain `
( l , u)
@= n
\v
−>
( n − 1)
f
= Add
(Dom v
l
= Add
( Same
e
e 1 @/= e 2
= Add
( Diff
e1
e 1 @+ e 2
= ( Plus
true
= Return
false
= Fail
(/\)
= (>>)
(\/)
= Try
e1
u)
( v : acc )
true
( Const
e2 )
n))
true
true
e2 )
()
A.3 Implementierung der rst-fail-Strategie zur Variablenauswahl und der
middleout-Reihenfolge für die Belegung mit Werten:
enumerate
label
v s = Dynamic
varsel
valsel
.
vs =
( label
firstfail
do v s ' <− v a r s e l
label '
where l a b e l '
label '
[]
middleout
vs )
vs '
return ( )
= do d <− v a l s e l $ domain v
return $ enum v d /\
=
( v : vs )
Dynamic
firstfail
middleout
vs
.
( label
varsel
valsel
do d s <− mapM domain v s
return [ v | ( d , v ) <− z ip d s v s
, then s o r t W i t h by ( length d )
l = l e t n = ( length l ) ` div ` 2 in
i n t e r l e a v e ( drop n l ) ( reverse $ take n l )
vs )
vs =
interleave
[]
ys = ys
interleave
( x : xs )
ys = x : i n t e r l e a v e
23
ys
xs
]
A.4 Modellierung eines minuplu-Rätsels mit der MCP-Modellierungssprache:
9x
3-
a1
2-
a2
a3
a4
b2
b3
b4
8x
2-
c1
c2
c3
c4
d3
d4
Die Feldbezeichnungen a1...a4,...,d1,...d4 entsprechen
den Constraint-Variablen im Modell. Zusätzlich müssen die jeweils angegebenen Rechenoperationen und
Ergebnisse in den jeweiligen Teilkästchen durch die
Einträge erfüllt werden, z.B. bedeutet 9x, dass in
12x
d1
doku - in jeder Spalte / Zeile jede Zahl aus dem
Wertebereich (hier 1 ... 4) nur einmal vorkommen.
5+
b1
Bei einem minuplu-Rätsel darf - wie bei einem Su-
d2
dem Teilkästchen oben links folgendes gelten muss:
a1 * b1 * b2 = 9.
Abbildung 3: 4 x 4 minupluRätsel
Die Modellierung dieses Rätsels mit dem MCP Framework zeigt der folgende Code:
−− b e i d e Berechnungen n ö t i g , da S u b t r a k t i o n n i c h t kommutativ
m i n u s C o n s t r a i n t v1 v2 e = v1 − v2 @= e \/ v2 − v1 @= e
minuplu
exist
exist
exist
exist
=
4 $ \as@ [ a1 , a2 , a3 , a4 ] −>
4 $ \bs@ [ b1 , b2 , b3 , b4 ] −>
4 $ \cs@ [ c1 , c2 , c3 , c4 ] −>
4 $ \ds@ [ d1 , d2 , d3 , d4 ] −>
v a r s = a s ++ bs ++ c s ++ ds
rows = a s : bs : c s : ds : [ ]
cols =
rows
v a r s ` a l l i n ` ( 1 , 4 ) /\
conj (
a l l D i f f rows ) /\
conj (
a l l D i f f c o l s ) /\
a1 ∗ b1 ∗ b2 @= 9 /\
m i n u s C o n s t r a i n t a2 a3 3 /\
m i n u s C o n s t r a i n t a4 b4 2 /\
b3 + c3 + c2 @= 5 /\
c1 ∗ d1 @= 8 /\
d2 ∗ d3 @= 12 /\
m i n u s C o n s t r a i n t c4 d4 2 /\
vars
let
transpose
in
map
map
return
−−
−−
−−
−−
Erzeugung d e r 16
C o n s t r a i n t −V a r i a b l e n
je 4 für jede Zeile
des R ä t s e l s
−−
−−
−−
−−
−−
−−
−−
−−
L i s t e der Z e i l e n
L i s t e der Spalten
W e r t e b e r e i c h s −C o n s t r a i n t
C o n s t r a i n t s : Z e i l e n − und
Spalteneinträge verschieden
Constraints für die
im " minuplu " v o r g e g e b e n e n
Berechnungen
−− Rückgabe d e r Lösung
Ergebnisausgabe:
C:\monadiccp-0.6.1\examples> minuplu overton_run
(107,[[3,1,4,2,1,3,2,4,4,2,1,3,2,4,3,1]])
A.5 Selbstgeschriebener Transformer (sorgt dafür, dass nur die ersten k
Lösungen ausgegeben werden):
newtype CKSolutionsST ( s o l v e r : : ∗ −> ∗ ) a = CKSST Int
instance S o l v e r s o l v e r => CTransformer ( CKSolutionsST s o l v e r
type CEvalState ( CKSolutionsST s o l v e r a ) = Int
type CTreeState ( CKSolutionsST s o l v e r a ) = ( )
type CForSolver ( CKSolutionsST s o l v e r a ) = s o l v e r
type CForResult ( CKSolutionsST s o l v e r a ) = a
initCT (CKSST
returnCT _ e s
| e s <= 1
|
otherwise
k) = (k , ( ) )
continue exit
= exit es
= c o n t i n u e ( es − 1)
24
a)
where
Herunterladen