Chapter 3 Advanced Usage
3.1 Search Control
3.1.1 Basic mechanisms
FaCiLe implements a standard depth first search with backtrack. OR
control is handled with a stack (module Stak
), while AND
control is handled with continuations.
OR control can be modified with a cut à la Prolog: a level is
associated to each choice-point (node in the search tree) and
choice-points created since a specified level can be removed,
i.e. cut (functions Stak.level
and Stak.cut
).
OR and AND controls are implemented by the Goals.solve
function. AND is mapped on the imperative sequence. OR is based on the
exception mechanism: backtrack is caused by the exception
Stak.fail
which is raised by failing constraints. Note that
this exception is catched and handled only by the Goals.solve
function.
3.1.2 Combining Goals with iterators
Functional programming allows the programmer to compose higher-order
functions using iterators. An iterator is associated to a
datatype and is the default control structure to process a value in
the datatype. There is a strong isomorphism between the datatypes and
the corresponding iterators and this isomorphism is a simple
guideline to use them.
Imitating the iterators of the standard OCaml library, FaCiLe
provides iterators for arrays and lists. While standard Array
and List modules allows to construct sequences (with a ;
)
of imperative functions (type 'a -> unit
), GlArray and
GlList modules of FaCiLe allows to construct conjunction
(with a &&~
) and disjunction (with a ||~
) of goals (type
Goals.t
).
The simplest iterator operates on integers and provides a standard
for-to loop by applying a goal to consecutive integers:
Goals.forto 3 7 g = (g 3) &&~ (g 4) &&~ ... &&~ (g 7)
Of course, iterators may be composed so a deterministic enumeration
of couples may be written as follows:
let enum_couples =
Goals.forto 1 3
(fun i ->
Goals.forto 4 5
(fun j ->
Goals.create (fun () -> Printf.printf "%d-%d\\n" i j))) in
Goals.solve enum_couples;;
1-4
1-5
2-4
2-5
3-4
3-5
- : bool = true
Arrays: module Goals.GlArray
Standard loop
The polymorphic Goals.GlArray.iter
function applies uniformally
a goal to every element of an array, connecting them with a
conjunction (&&~
).
Goals.GlArray.iter g [|e1; e2; ...; en|] = (g e1) &&~ (g e2) &&~ ... &&~ (g en)
Labeling of an array of variables is the iteration of the instanciation
of one variable (Goals.indomain):
let labeling_array = Goals.GlArray.iter Goals.indomain;;
val labeling_array : Facile.Var.Fd.t array -> Facile.Goals.t = <fun>
A matrix is an array of arrays; following the isomorphism, labeling of
a matrix must be simply a composition of the array iterator:
let labeling_matrix = Goals.GlArray.iter labeling_array;;
val labeling_matrix : Facile.Var.Fd.t array array -> Facile.Goals.t = <fun>
Changing the order
The
iter_h
(iterator with Heuristic) gives the user the possibility
to choose the order the elements are considered. The first argument of
iter_h
is a function which is applied to the array by the
iterator and which must return the index of one element on which the
goal is applied. This function must raise the Not_found
exception to stop the loop
For example, if we want to apply the goal only on unbound variables of
an array, we may write:
let first_unbound array =
let n = Array.length array in
let rec loop i = (* loop until free variable found *)
if i < n then
match Fd.value array.(i) with
Unk _ -> i
| Val _ -> loop (i+1)
else
raise Not_found in
loop 0;;
val first_unbound : Facile.Easy.Fd.t array -> int = <fun>
let iter_unbounds = Goals.GlArray.iter_h first_unbound;;
val iter_unbounds :
(Facile.Easy.Fd.t -> Facile.Goals.t) ->
Facile.Easy.Fd.t array -> Facile.Goals.t = <fun>
Note that the function iter_h
is polymorphic and can be used
for any array.
The function Goals.GlArray.choose_index
facilitates the
construction of heuristic functions; it constructs such a function from
an ordering function on variable attributes (free variables are
ignored). For example, the standard ``min size'' strategy will be
implemented as follows:
let min_size_order =
Goals.GlArray.choose_index (fun a1 a2 -> Var.Attr.size a1 < Var.Attr.size a2);;
val min_size_order : Facile.Var.Fd.t array -> int = <fun>
let min_size_strategy = Goals.GlArray.iter_h min_size_order;;
val min_size_strategy :
(Facile.Var.Fd.t -> Facile.Goals.t) ->
Facile.Var.Fd.t array -> Facile.Goals.t = <fun>
let min_size_labeling = min_size_strategy Goals.indomain;;
val min_size_labeling : Facile.Var.Fd.t array -> Facile.Goals.t = <fun>
Lists: module Goals.GlList
FaCiLe Goals.GlList
module provides similar iterators for
lists except of course iterators which involve index of elements.
3.2 Constraints Control
Constraints may be seen operationnally as ``reactive objects''. They
are attached to variables, more precisely to events related to
variable modifications. A constraint is mainly a function (the update field) which is called when the constraint is woken.
The update function usually do a propagation using the event (i.e. the
modification of one variable) to process new domains for other
variables.
3.2.1 Events
An event (of type Var.Attr.event
) is a modification of the
attribute (i.e. the domain) of a finite domain variable. FaCiLe
currently provides only four specific events:
-
Modification of the domain (
on_refine
);
- Substitution of the variable, i.e. reduction of the domain to a singleton (
on_subst
);
- Modification of the minimum value of the domain (
on_min
);
- Modification of the maximum value of the domain (
on_max
).
Note that these events are not independant and constitute a lattice which top is on_subst
and bottom is on_refine
:
-
on_subst
implies all other events1;
-
on_min
and on_max
imply on_refine
.
Constraints are attached to the variables through these events. In
concrete terms, lists of constraints (one per event) are put in the
attribute of the variable. Note that this attachement occurs only when
the constraint is posted.
3.2.2 Wakening, Queuing, Priorities
When an event occurs, related constraints are woken and put in a
queue. The queue is processed after each sequence of waking. This
processing is protected against reentrance. Constraints are considered
one after the other and propagation (update) is called. Propagation
may fail by raising an exception or succeed. Propagation of one
constraint is also protected against rewaking by itself. When a
constraint is triggered, the propagation does not know anything about
the event and even does not get information about the variable
responsible of the event. A constraint may be woken once by two
distinct events. Note also that the queue contains constraints and not
variables.
FaCiLe (currently) implements three ordered queues and ensures that
a constraint in a lower queue is not propagated before a constraint
present in a higher queue. The queue is chosen according to the priority of a constraint (abstract type Cstr.priority
). The
priority is specified when the constraint is defined. It cannot be
changed neither when the constraint is posted nor later. Priorities
are defined in module Cstr
: immediate
, normal
or
later
.
3.2.3 Constraints Store
FaCiLe handles the constraints store of all the posted and
active constraints (a constraint becomes inactive if it is
solved, i.e. if the update returns true, see 3.3). For
debugging purpose, this store can be consulted using the function
Cstr.active_store
and Cstr.t
access functions (id
and name
).
3.3 User's Constraints
The Cstr.create
function allows the user to build new
constraints from scratch.
To define a new simple (unreifiable) constraint, very few arguments
must be passed to the create function as numbers of them are
optional (thus labeled) and have default values. Merely the two
following arguments are actually needed to build a new constraint:
-
update
should perform propagation
(domains reduction) and return true iff the constraint is consistent;
-
delay
specifies on which events the update
function will be called.
However we recommend to name a new constraint and precise its printing
facility, which may obviously help debugging, by specifying the two
following optional arguments:
-
?name
should be a relevant string describing the purpose of the
constraint;
-
?fprint
to print more accurate information on the constraint state (variables domains, maintained data structures value, ...).
To define a reifiable constraint, the two following optional arguments
must also be specified:
-
?check
should return true if the constraint is entailed, false if
its negation is entailed and raise DontKnow
otherwise. check
is
called when the constraint is reified and should therefore not
perform any domain modification.
-
?not
should return the negation of the constraint. It is only
called when the negation of a reified constraint is entailed.
Finally two other optional arguments may be specified:
-
?priority
should be passed to the create
function to precise the
priority of the new constraint in the constraints queue. Constraints
with lower priority are waken only when there is no more constraint
of higher priority in the waking queue. Time costly constraints should
get a later
while quick elementary constraints should be
immediate
, and standard constraints normal
(default value).
-
?init
is executed as soon as the post
function is called on the
constraint to perform initialization of inner data structures
needed by update
(thus not called when dealing with a reified
constraint).
The example below defines a new constraint stating that variable x
should be different from variable y
:
diff.ml
open Facile
open Easy
let cstr x y =
let name = "different" in
let fprint c =
Printf.fprintf c "%s: %a <> %a\n" name Fd.fprint x Fd.fprint y
and delay ct =
Var.delay [Var.Attr.on_subst] x ct;
Var.delay [Var.Attr.on_subst] y ct
and update () =
(* Domain reduction is performed only when x or y is instantiated *)
match (Fd.value x, Fd.value y) with
(Val a, Val b) -> a <> b || Stak.fail name
(* If one of the two variables is instantiated, its value is
removed in the domain of the other variable *)
| (Val a, Unk attr_y) ->
let new_domy = Domain.remove a (Var.Attr.dom attr_y) in
Fd.refine y new_domy;
true (* Constraint is solved *)
| (Unk attr_x, Val b) ->
let new_domx = Domain.remove b (Var.Attr.dom attr_x) in
Fd.refine x new_domx;
true (* Constraint is solved *)
| _ -> false (* Constraint is not solved *)
and check () =
match (Fd.value x, Fd.value y) with
(Val a, Val b) -> a = b
| (Val a, Unk attr_y) when not (Var.Attr.member attr_y a) -> true
| (Unk attr_x, Val b) when not (Var.Attr.member attr_x b) -> true
| _ -> raise Cstr.DontKnow
and not () =
fd2e x =~ fd2e y in
(* Creation of the constraint. *)
Cstr.create ~name ~fprint ~check ~not update delay;;
Let's compile the file
ocamlc -c diff.ml
and use the produced object:
#load "diff.cmo";;
let x = Fd.interval 1 2 and y = Fd.interval 2 3;;
val x : Facile.Easy.Fd.t = <abstr>
val y : Facile.Easy.Fd.t = <abstr>
let diseq = Diff.cstr x y;;
val diseq : Facile.Cstr.t = <abstr>
Cstr.post diseq;;
- : unit = ()
let goal =
Goals.indomain x &&~ Goals.indomain y
&&~ Goals.create (fun () -> Cstr.fprint stdout diseq)
&&~ Goals.fail in
while (Goals.solve goal) do () done;;
different: 1 <> 2
different: 1 <> 3
different: 2 <> 3
- : unit = ()
3.4 User's Goal
3.4.1 Atomic Goal: Goals.create
The simplest way to create a deterministic atomic goal is to use the
Goals.create
function which ``goalify'' any unit (i.e. of type
unit -> unit
) function.
Let's write the goal which writes a variable on the standard output:
let gprint_fd x = Goals.create (fun () -> Printf.printf "%a\\n" Fd.fprint x);;
val gprint_fd : Facile.Easy.Fd.t -> Facile.Goals.t = <fun>
To instantiate a variable inside a goal, we write the following
definition :
let instantiate x v = Goals.create (fun () -> Fd.unify x v);;
val instantiate : Facile.Easy.Fd.t -> int -> Facile.Goals.t = <fun>
let v = Fd.interval 0 3 in
if Goals.solve (instantiate v 2) then Fd.fprint stdout v;;
2- : unit = ()
This goal creation can be used to pack any side effect function :
let gprint_int x = Goals.create (fun () -> print_int x);;
val gprint_int : int -> Facile.Goals.t = <fun>
Goals.solve (Goals.forto 0 5 gprint_int);;
012345- : bool = true
The main point when creating goals is to precisely distinguish the
time of creation of the goal from the time of its execution. For example, the following goal does not produce what you
maybe expect:
let wrong_min_or_max var =
let min = Fd.min var and max = Fd.max var in
(instantiate var min ||~ instantiate var max);;
val wrong_min_or_max : Facile.Easy.Fd.t -> Facile.Goals.t = <fun>
The min and max of variable the var are processed when
the goal is created and may be different from the min and max of the
variable when the goal will be called. To fix the problem, min
and max must be computed in the goal. Then the latter must
return the disjunction, something it is not possible to do with
Goals.create.
3.4.2 Arbitrary Goal: Goals.make
The Goals.create
function does not allow to construct goals
which construct new goals (similar to Prolog clauses). The
Goals.make
function ``goalify'' a function which may return
another goal; the argument of Goals.make
returns a
t option
, i.e. either Some
(nothing) or Some g
(the goal
g
).
Let's write the goal which try to instantiate a variable to its
minimum value or to its maximum :
let min_or_max v =
Goals.make
(fun () ->
let min = Fd.min v and max = Fd.max v in
Some (instantiate v min ||~ instantiate v max))
();;
val min_or_max : Facile.Easy.Fd.t -> Facile.Goals.t = <fun>
The other difference of Goals.make
with Goals.create
is
the argument of the goalified function which may be of any type
('a
) and which must be passed as the second argument to
Goals.make
. In the previous example, we use ()
.
The Goals.make
allows the user to define recursive goals by a
mapping on a recursive function. In the next example, we iterate a
goal non-deterministally on a list.
let iter_disj fgoal list =
let rec loop l =
match l with
| [] -> None
| x::xs -> Some (fgoal x ||~ Goals.make loop xs) in
Goals.make loop list;;
val iter_disj : ('a -> Facile.Goals.t) -> 'a list -> Facile.Goals.t = <fun>
let gprint_int x = Goals.create (fun () -> print_int x);;
val gprint_int : int -> Facile.Goals.t = <fun>
let gprint_list = iter_disj gprint_int;;
val gprint_list : int list -> Facile.Goals.t = <fun>
if Goals.solve (gprint_list [1;7;2;9] &&~ Goals.fail ||~ Goals.success) then
print_newline ();;
1729
- : unit = ()
3.4.3 Recursive Goals: Goals.make_rec
FaCiLe provides also a constructor for intrinsic recursive
goals. Expression [Goals.make_rec f]
is similar to
[Goals.make f]
except that the argument of the function
f
is the created goal itself.
The simplest example using this feature is the classic repeat
predicate of Prolog implementing a non-deterministic loop:
let repeat = Goals.make_rec (fun self -> Some (Goals.success ||~ self));;
val repeat : Facile.Goals.t = <abstr>
The goalified function simply returned the disjunction of a success
and itself.
The Goals.indomain
function which non-non-deterministically
instantiates a variable is written using Goals.make_rec
:
let indomain var =
Goals.make_rec
~name:"indomain"
(fun self ->
match Fd.value var with
Val _ -> None
| Unk attr ->
let dom = Var.Attr.dom attr in
let remove_min =
Goals.create
(fun () -> Fd.refine var (Domain.remove_min dom))
and min = Domain.min dom in
Some (instantiate var min ||~ remove_min &&~ self));;
val indomain : Facile.Easy.Fd.t -> Facile.Goals.t = <fun>
The goal first checks if the variable is already bound and do nothing
in this case. If it is an unknown, it returns a goal trying to
instantiate the variable to its minimum or to remove it before
continuing with the remaining domain.
- 1
- It means that
on_min event occurs if a variable is instantiated to its
minimum value. This choice is arguable and could be thrown back into
question into further releases.