Previous Contents Next

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: Note that these events are not independant and constitute a lattice which top is on_subst and bottom is 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: 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:

To define a reifiable constraint, the two following optional arguments must also be specified:

Finally two other optional arguments may be specified: 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.

Previous Contents Next