3

My ultimate goal is to make a reified version of automaton/3, that freezes if there are any variables in the sequence passed to it. i.e. I dont want the automaton to instantiate variables.

(fd_length/3, if_/3 etc as defined by other people here on so).

To start with I have a reified test for single variables:

var_t(X,T):-
  var(X) ->
  T=true;
  T=false.

This allows me to implement:

if_var_freeze(X,Goal):-
  if_(var_t(X),freeze(X,Goal),Goal).

So I can do something like:

?-X=bob,Goal =format("hello ~w\n",[X]),if_var_freeze(X,Goal).

Which will behave the same as:

?-Goal =format("hello ~w\n",[X]),if_var_freeze(X,Goal),X=bob.

How do I expand this to work on a list of variables so that Goal is only called once, when all the vars have been instantiated?

In this method if I have more than one variable I can get this behaviour which I don't want:

?-List=[X,Y],Goal = format("hello, ~w and ~w\n",List),
if_var_freeze(X,Goal),
if_var_freeze(Y,Goal),X=bob.

hello, bob and _G3322
List = [bob, Y],
X = bob,
Goal = format("hello, ~w and ~w\n", [bob, Y]),
freeze(Y, format("hello, ~w and ~w\n", [bob, Y])).

I have tried:

freeze_list(List,Goal):-
  freeze_list_h(List,Goal,FrozenList),
  call(FrozenList).

freeze_list_h([X],Goal,freeze(X,Goal)).
freeze_list_h(List,Goal,freeze(H,Frozen)):-
  List=[H|T],
  freeze_list_h(T,Goal,Frozen).

Which works like:

 ?- X=bob,freeze_list([X,Y,Z],format("Hello ~w, ~w and ~w\n",[X,Y,Z])),Y=fred.
 X = bob,
 Y = fred,
 freeze(Z, format("Hello ~w, ~w and ~w\n", [bob, fred, Z])) .

?- X=bob,freeze_list([X,Y,Z],format("Hello ~w, ~w and ~w\n",[X,Y,Z])),Y=fred,Z=sue.
Hello bob, fred and sue
X = bob,
Y = fred,
Z = sue .

Which seems okay, but I am having trouble applying it to automaton/3. To reiterate the aim is to make a reified version of automaton/3, that freezes if there are any variables in the sequence passed to it. i.e. I don't want the automaton to instantiate variables.

This is what I have:

ga(Seq,G) :-
    G=automaton(Seq, [source(a),sink(c)],
                     [arc(a,0,a), arc(a,1,b),
                      arc(b,0,a), arc(b,1,c),
                      arc(c,0,c), arc(c,1,c)]).

max_seq_automaton_t(Max,Seq,A,T):-
  Max #>=L,
  fd_length(Seq,L),
  maplist(var_t,Seq,Var_T_List), %find var_t for each member of seq
  maplist(=(false),Var_T_List),  %check that all are false i.e no  uninstaninated vars
  call(A),!,
  T=true.
max_seq_automaton_t(Max,Seq,A,T):-
  Max #>=L,
  fd_length(Seq,L),
  maplist(var_t,Seq,Var_T_List), %find var_t for each member of seq
  maplist(=(false),Var_T_List),  %check that all are false i.e no uninstaninated vars
  \+call(A),!,
  T=false.
max_seq_automaton_t(Max,Seq,A,true):-
  Max #>=L,
  fd_length(Seq,L),
  maplist(var_t,Seq,Var_T_List), %find var_t for each
  memberd_t(true,Var_T_List,true), %at least one var
    freeze_list_h(Seq,A,FrozenList),
  call(FrozenList),
  call(A).
max_seq_automaton_t(Max,Seq,A,false):-
  Max #>=L,
  fd_length(Seq,L),
  maplist(var_t,Seq,Var_T_List), %find var_t for each
  memberd_t(true,Var_T_List,true), %at least one var
    freeze_list_h(Seq,A,FrozenList),
    call(FrozenList),
  \+call(A).

Which does not work, The following goal should be frozen until X is instantiated:

?- Seq=[X,1],ga(Seq,A),max_seq_automaton_t(3,Seq,A,T).
Seq = [1, 1],
X = 1,
A = automaton([1, 1], [source(a), sink(c)], [arc(a, 0, a), arc(a, 1, b), arc(b, 0, a), arc(b, 1, c), arc(c, 0, c), arc(c, 1, c)]),
T = true 

Update This is what I now have which I think works as I originally intended but I am digesting what @Mat has said to think if this is actually what I want. Will update further tomorrow.

goals_to_conj([G|Gs],Conj) :- 
  goals_to_conj_(Gs,G,Conj).

goals_to_conj_([],G,nonvar(G)).
goals_to_conj_([G|Gs],G0,(nonvar(G0),Conj)) :-
  goals_to_conj_(Gs,G,Conj).

max_seq_automaton_t(Max,Seq,A,T):-
  Max #>=L,
  fd_length(Seq,L),
  maplist(var_t,Seq,Var_T_List), %find var_t for each member of seq
  maplist(=(false),Var_T_List),  %check that all are false i.e no uninstaninated vars
  call(A),!,
  T=true.
max_seq_automaton_t(Max,Seq,A,T):-
  Max #>=L,
  fd_length(Seq,L),
  maplist(var_t,Seq,Var_T_List), %find var_t for each member of seq
  maplist(=(false),Var_T_List),  %check that all are false i.e no uninstaninated vars
  \+call(A),!,
  T=false.
max_seq_automaton_t(Max,Seq,A,T):-
  Max #>=L,
  fd_length(Seq,L),
  maplist(var_t,Seq,Var_T_List), %find var_t for each
  memberd_t(true,Var_T_List,true), %at least one var
  goals_to_conj(Seq,GoalForWhen),
  when(GoalForWhen,(A,T=true)).
max_seq_automaton_t(Max,Seq,A,T):-
  Max #>=L,
  fd_length(Seq,L),
  maplist(var_t,Seq,Var_T_List), %find var_t for each
  memberd_t(true,Var_T_List,true), %at least one var
  goals_to_conj(Seq,GoalForWhen),
  when(GoalForWhen,(\+A,T=false)).
repeat
  • 18,496
  • 4
  • 54
  • 166
user27815
  • 4,767
  • 14
  • 28
  • The way I see it is that `if_/3` should not be used like this... Why do you even bother using `if_/3` and not simply write `if_var_freeze(X,Goal) :- ( var(X) -> freeze(X,Goal) ; call(Goal) ).`? Or even simpler: `if_var_freeze(X,Goal) :- freeze(X,Goal).`? – repeat Oct 14 '15 at 18:24
  • Please tell us a little more about your use case! What is the difference to, say, using `ground/1` in combination with `automaton/3` and some if-then-else? Assuming, of course, that `automaton/3` always succeeds deterministically or finitely fails when used with ground data... – repeat Oct 14 '15 at 19:06
  • I am mainly just trying things out to try and get better at using these methods. I had an idea about trying to learn automata and grammars, by using a combination of generate and test, alongside constraints. Probably a bit crazy though! – user27815 Oct 14 '15 at 19:36
  • For if_var_freeze I was trying to find away to apply that to a list of vars for one goal.. – user27815 Oct 14 '15 at 19:39

3 Answers3

4

In my view, you are making great progress with Prolog. At this point it makes sense to proceed a bit more prudently though. All the things you are asking for can, in principle, be solved easily. You only need a generalization of freeze/2, which is available as when/2.

However, let us take a step back and more deeply consider what is actually going on here.

Declaratively, when we state a constraint, we mean that it holds. We do not mean "It holds only when everything is instantiated", because that would reduce the constraint to a mere checker, leading to a "generate-and-test" approach. The point of constraints is exactly to prune whenever possible, leading to a much reduced search space in many cases.

Exactly the same holds for reified constraints. When we post a reified constraint, we state that the reification holds. Not only in cases where everything is instantiated, but always. The point is exactly that the (reified) constraint can be used in all directions. If the constraint that is being reified is already entailed, we get to know it. Likewise, if it cannot hold, we get to know it. If either possibility may be the case, we need to search explicitly for solutions, or determine that none exist. If we want to insist that the constraint that is being reified holds, it is easily possible; etc.

However, the point in all cases is exactly that we can focus on the declarative semantics of the constraint, very free from extra-logical, procedural considerations like what is being instantiated and when. If I answered your literal question, it would move you closer to operational considerations, much closer than you probably need or want in actuality.

Therefore, I am not going to answer your literal question. But I will give you a solution to your actual, underlying issue.

The point is to reifiy automaton/3. A constraint reification will not by itself prune anything as long as it is open whether the constraint that is being reified actually holds or not. Only when we insist that the constraint that is being reified holds does propagation occur.

It is easy to reify automaton/3, by reifying the conjunction of constraints that constitute its decomposition. Here is one way to do it, based on code that is freely available in SWI-Prolog:

:- use_module(library(clpfd)).

automaton(Vs, Ns, As, T) :-
        must_be(list(list), [Vs,Ns,As]),
        include_args1(source, Ns, Sources),
        include_args1(sink, Ns, Sinks),
        phrase((arcs_relation(As, Relation),
                nodes_nums(Sinks, SinkNums0),
                nodes_nums(Sources, SourceNums0)), [[]-0], _),
        phrase(transitions(Vs, Start, End), Tuples),
        list_to_drep(SinkNums0, SinkDrep),
        list_to_drep(SourceNums0, SourceDrep),
        (   Start in SourceDrep #/\
            End in SinkDrep #/\
            tuples_in(Tuples, Relation)) #<==> T.


include_args1(Goal, Ls0, As) :-
        include(Goal, Ls0, Ls),
        maplist(arg(1), Ls, As).

list_to_drep([L|Ls], Drep) :-
        foldl(drep_, Ls, L, Drep).

drep_(L, D0, D0\/L).

transitions([], S, S) --> [].
transitions([Sig|Sigs], S0, S) --> [[S0,Sig,S1]],
        transitions(Sigs, S1, S).

nodes_nums([], []) --> [].
nodes_nums([Node|Nodes], [Num|Nums]) -->
        node_num(Node, Num),
        nodes_nums(Nodes, Nums).

arcs_relation([], []) --> [].
arcs_relation([arc(S0,L,S1)|As], [[From,L,To]|Rs]) -->
        node_num(S0, From),
        node_num(S1, To),
        arcs_relation(As, Rs).

node_num(Node, Num), [Nodes-C] --> [Nodes0-C0],
        { (   member(N-I, Nodes0), N == Node ->
              Num = I, C = C0, Nodes = Nodes0
          ;   Num = C0, C is C0 + 1, Nodes = [Node-C0|Nodes0]
          ) }.

sink(sink(_)).

source(source(_)).

Note that this propagates nothing whatsoever as long as T is unknown.

I now use the following definition for a few sample queries:

seq(Seq, T) :-
        automaton(Seq, [source(a),sink(c)],
                       [arc(a,0,a), arc(a,1,b),
                        arc(b,0,a), arc(b,1,c),
                        arc(c,0,c), arc(c,1,c)], T).

Examples:

?- seq([X,1], T).

Result (omitted): Constraints are posted, nothing is propagated.

Next example:

?- seq([X,1], T), X = 3.
X = 3,
T = 0.

Clearly, the reified automaton/3 constraint does not hold in this case. However, the reifying constraint of course still holds, as always, and this is the reason why T=0 in this case.

Next example:

?- seq([1,1], T), indomain(T).
T = 0 ;
T = 1.

Oh-oh! What is going on here? How can it be that the constraint is both true and false? This is because we do not see all constraints that are actually posted in this example. Use call_residue_vars/2 to see the whole truth.

In fact, try it on the simpler example:

?- call_residue_vars(seq([1,1],0), Vs).

The pending residual constraints that still need to be satisfied in this case are:

_G1496 in 0..1,
_G1502#/\_G1496#<==>_G1511,
tuples_in([[_G1505,1,_G1514]], [[0,0,0],[0,1,1],[1,0,0],[1,1,2],[2,0,2], [2,1,2]])#<==>_G825,
tuples_in([[_G831,1,_G827]], [[0,0,0],[0,1,1],[1,0,0],[1,1,2],[2,0,2],[2,1,2]])#<==>_G826,
_G829 in 0#<==>_G830,
_G830 in 0..1,
_G830#/\_G828#<==>_G831,
_G828 in 0..1,
_G827 in 2#<==>_G828,
_G829 in 0..1,
_G829#/\_G826#<==>0,
_G826 in 0..1,
_G825 in 0..1

So, the above only holds if these constraints, which are said to still flounder, also hold.

Here is an auxiliary definition that helps you label remaining finite domain variables. It suffices for this example:

finite(V) :-
        fd_dom(V, L..U),
        dif(L, inf),
        dif(U, sup).

We can now paste back the residual program (which consists of CLP(FD) constraints), and use label_fixpoint/1 to label variables whose domain is finite:

?- Vs0 = [_G1496, _G1499, _G1502, _G1505, _G1508, _G1511, _G1514, _G1517, _G1520, _G1523, _G1526],
  _G1496 in 0..1,
  _G1502#/\_G1496#<==>_G1511,
  tuples_in([[_G1505,1,_G1514]], [[0,0,0],[0,1,1],[1,0,0],[1,1,2],[2,0,2], [2,1,2]])#<==>_G825,
  tuples_in([[_G831,1,_G827]], [[0,0,0],[0,1,1],[1,0,0],[1,1,2],[2,0,2],[2,1,2]])#<==>_G826,
  _G829 in 0#<==>_G830, _G830 in 0..1,
  _G830#/\_G828#<==>_G831, _G828 in 0..1,
  _G827 in 2#<==>_G828, _G829 in 0..1,
  _G829#/\_G826#<==>0, _G826 in 0..1, _G825 in 0..1,
  include(finite, Vs0, Vs),
  label(Vs).

Note that we cannot directly use labeling in the original program, i.e., we cannot do:

?- call_residue_vars(seq([1,1],0), Vs), <label subset of Vs>.

because call_residue_vars/2 also brings internal variables to the surface that, although they have a domain assigned and look like regular CLP(FD) variables, are not meant to directly participate in any labeling.

In contrast, the residual program can be used without any problem for further reasoning, and it is in fact meant to be used that way.

In this concrete case, after labeling the variables whose domains are still finite in the case above, some constraints still remain. They are of the form:

tuples_in([[_G1487,1,_G1496]], [[0,0,0],[0,1,1],[1,0,0],[1,1,2],[2,0,2],[2,1,2]])#<==>_G1518

Exercise: Does it follow from this, however indirectly, that the original query, i.e., seq([1,1],0), cannot hold?

So, to summarize:

  1. Constraint reification does not in itself cause propagation of the constraint that is being reified.
  2. Constraint reification often lets you detect that a constraint cannot hold.
  3. In general, CLP(FD) propagation is necessarily incomplete, i.e., we cannot be sure that there is a solution just because our query succeeds.
  4. labeling/2 lets you see whether there are concrete solutions, if domains are finite.
  5. To see all pending constraints, wrap your query in call_residue_vars/2.
  6. As long as pending constraints remain, it is only a conditional answer.

Recommendation: To make sure that no floundering constraints remain, wrap your query in call_residue_vars/2 and look for any residual constraints on the toplevel.

mat
  • 40,498
  • 3
  • 51
  • 78
  • 1
    If I have `seq2(Seq, T) :- automaton(Seq, [source(a),sink(c)], [arc(a,1,b), arc(b,1,c) ], T).` I think it exhibits similar behaviour? `?- seq2([1,1],T), indomain(T). T = 0 ; T = 1. ?- call_residue_vars(seq2([1,1],0),Vs),label_fixpoint(Vs). Vs = [1, _G6161, 1, 0, _G6170, 1, 1, 0, 0|...].` – user27815 Oct 15 '15 at 09:33
  • Good! And is it the simplest case? – mat Oct 15 '15 at 09:47
  • I think its to do with the loops in the automaton. So here a successful pass goes from node A to node B consuming the 1. We have started in the source node and ended in the sink node. But if a pass uses the node A self loop to consume the one. It is not in a sink state, so fails. seq2(Seq, T) :- automaton(Seq, [source(a),sink(b)], [arc(a,1,a), arc(a,1,b) ], T). ?- call_residue_vars(seq2([1],T),Vs),label_fixpoint(Vs). T = 0, Vs = [_G1434, 1, 0, 0, 0, 1, 0, 0] ; T = 1, Vs = [_G1424, 1, 1, 1, 0, 1, 1, 1]. – user27815 Oct 18 '15 at 10:00
  • Good! 2 nodes and 2 arcs is also the simplest case I found. Now check this out: There seems to be a mistake in how the CLP(FD) library currently handles *reified* `tuples_in/2`. In the case of `automaton/4`, using `?- call_residue_vars(seq([1,1],0), Vs), label_fixpoint(Vs).` yields a *different* result compared to pasting back the residual goals from `?- call_residue_vars(seq([1,1],0),Vs).` and calling `label_fixpoint(Vs)` on the variables from the residual goals! Let's find a simpler case for this and then submit a patch or report! – mat Oct 18 '15 at 22:16
  • I am a bit lost to be honest! I can see that: `?- call_residue_vars(tuples_in([[Start,1,Stop]],[[0,1,0],[0,1,1]])#<==>T,Vs),label_fixpoint(Vs). T = 0, Vs = [Stop, Start, _G20790, 0], Start#=0#<==>_G20803, Start#=0#<==>_G20815, _G20803 in 0..1, _G20803#/\_G20842#<==>0, _G20842 in 0..1, Stop#=1#<==>_G20842, Stop#=0#<==>_G20875, _G20875 in 0..1, _G20815#/\_G20875#<==>0, _G20815 in 0..1 ; Start = Stop, Stop = 0, T = 1, Vs = [0, 0, _G17336, 1] ; Start = 0, Stop = T, T = 1, Vs = [1, 0, _G17357, 1]. ` but `56 ?- label_fixpoint([Stop, Start, _G20790, 0]). true.` is that a case? – user27815 Oct 19 '15 at 13:41
  • Yes, very good. Meanwhile, I have also looked into this (example: `(A in 0 #/\ tuples_in([[A,1,B]],[[0,0,0],[0,1,1],[1,1,2]])) #<==> 0`) and found that labeling is not meant to be used on all variables that `call_residue_vars/2` brings to the surface. Some of these variables are, in regular queries, not accessible to users, and CLP(FD) relies an them *not* participating explicitly in labeling outside of their dedicated purpose. I will adjust my answer to take this into account. Still, to make sure that no constraints remain, always use `call_residue_vars/2` to see *all* residual goals. – mat Oct 19 '15 at 13:52
  • Thanks for the update. So in general if I just use clpfd constraints and try to reifiy them, in order to check if they hold, I cant guarantee that when they say true, that they actually are true with out having to check for reisdue vars and pulling out the residual program for a specialised labelling routine? CLPFD suddenly seems a lot less useful :S. – user27815 Oct 21 '15 at 09:08
  • Is this true for clb as well? i.e I cant guarantee that a reified formula is true? – user27815 Oct 21 '15 at 09:10
  • 1
    *Incompleteness* is a fundamental and intrinsic property of all CLP(FD) systems, due to the undecidability of integer arithmetic. So, you *always* need to use `call_residue_vars/2` to see the conditions that make your queries true, also if you are *not* using any reification. CLP(FD) is incomplete in simple cases too: `t :- X #> Y, Y #> X.`. If you ask: `?- t.`, then Prolog will say `true`, but in fact this is false! Always use `call_residue_vars/2` to see the whole truth. CLP(B), in contrast, is *complete* in both SICStus and SWI: If `sat/1` succeeds, then there definitely is a solution too. – mat Oct 21 '15 at 12:32
  • Thanks for the explanations. – user27815 Oct 21 '15 at 13:33
  • I hope they help you to answer, after some more pondering, the question whether or not we can consider the automaton constraint now satisfied in the case above. It is not as easy as it first appears, and I do not expect you to answer this right away. For now, I only wanted to show you how reification can help you detect that something *certainly* does not hold or *certainly* holds. This is definitely the case if no or only satisfiable residual constraints remain after `call_residue_vars/2`. In the general case, we cannot decide it anyways due to incompleteness and undecidability of arithmetic. – mat Oct 21 '15 at 13:45
2

Consider using the widely available predicate when/2 (for details, consider reading the SICStus Prolog manual page on when/2).

Note that you can, in principle, implement freeze/2 like this:

freeze(V,Goal) :-
   when(nonvar(V),Goal).
repeat
  • 18,496
  • 4
  • 54
  • 166
  • I see I can put: `when((nonvar(X),nonvar(Y)),Goal).` But I cant put a list like: `List=[nonvar(X),nonvar(Y),nonvar(Z)],when(List,Goal). ` How does that work? – user27815 Oct 14 '15 at 19:30
  • What is it that you wish to express? "As soon `X`, `Y`, and `Z` are *all* nonvar, prove goal `G`." ? – repeat Oct 14 '15 at 20:24
  • You can have that with `when((nonvar(X),nonvar(Y),nonvar(Z)),Goal)`. – repeat Oct 14 '15 at 20:25
  • The first argument of `when/2` is not a list, but a goal composed of nothing but conjunction, disjunction, `nonvar/1`, `ground/1`, and `(?=)/2`. – repeat Oct 14 '15 at 20:27
  • Using `(?=)/2` and `when/2` one can implement logically sound term disequality. – repeat Oct 14 '15 at 20:30
  • I want to express as soon as all vars in a list are non var prove goal. How do I construct the first argument of when/2 for a large list of variables? – user27815 Oct 14 '15 at 20:36
  • Note that there are many, many conjunctions that are equivalent to `A,B,C,D,E`. So, realistically, we accept that the auxiliary predicate we will use is primarily used in one direction only, that is: given some list of goals `Gs`, what is one (out of many) conjunction of all goals `G` in `Gs`... – repeat Oct 14 '15 at 20:43
  • 1
    `vars_to_cond([G|Gs],Cond) :- vars_to_cond_(Gs,G,Cond). vars_to_cond_([],V,nonvar(V)). vars_to_cond_([V|Vs],V0,(nonvar(V0),Cond)) :- vars_to_cond_(Vs,V,Cond).` Sample uses: `?- vars_to_cond([X],Cond). Cond = nonvar(X). ?- vars_to_cond([X,Y],Cond). Cond = (nonvar(X), nonvar(Y)). ?- vars_to_cond([X,Y,Z],Cond). Cond = (nonvar(X), nonvar(Y), nonvar(Z)).` – repeat Oct 14 '15 at 21:01
  • I am not sure what you mean by there are many conjunctions equivalent to `A,B,C,D,E` ? but vars_to_cond/2 works. – user27815 Oct 14 '15 at 21:07
  • `Goal1,Goal2,Goal3` is actually `(Goal1,(Goal2,Goal3))`. Executing the conjunction `((Goal1,Goal2),Goal3)` is equivalent, even though it's syntactically different. – repeat Oct 14 '15 at 21:13
1

What you are implementing appears to me a variation of the following:

delayed_until_ground_t(Goal,T) :-
   (  ground(Goal)
   -> (  call(Goal)
      -> T = true
      ;  T = false
      )
   ;  T = true,  when(ground(Goal),once(Goal))
   ;  T = false, when(ground(Goal),  \+(Goal))
   ).

Delaying goals can be a really nice feature, but be aware of the perils of delaying forever.

Make sure to read and digest the above answer by @mat regarding call_residue_vars/2!

repeat
  • 18,496
  • 4
  • 54
  • 166