[comp.lang.prolog] Some comments on term comparison

vanroy@ji.Berkeley.EDU (Peter Van Roy) (09/08/88)

I have some comments and questions on the term comparison operator @<.

The behavior of setof
---------------------

Consider the following predicate:

	a(X,X).
	a(Y,Z).
	a(W,T).
	a(a,b).

The query

	| ?- setof(a(X,Y), a(X,Y), S).

gives the result (slightly edited for readability):

	S = [a(A,B),a(C,D),a(E,E),a(a,b)]

It would be better if one of a(A,B) and a(C,D) were deleted from this result.
In order to do this, two terms such as a(X,X) and a(Y,Y) which have the
same pattern of bound variables should be considered equal.
This requires a new meaning for the equality of two terms.

Modified term comparison
------------------------

Term comparison should do a structural comparison for all terms, including
those containing variables.  It should impose an ordering between terms
that also depends on the pattern of variable binding.  Terms such as a(X,X)
and a(Y,Z) should have a place in this ordering.  To solve the setof
problem mentioned above, a structural equality predicate @== is needed,
since a(X,X)==a(Y,Y) fails, whereas they are structurally equal.
To summarize, the following comparisons succeed:

	a(X,X) @== a(Y,Y)
	a(X,Y) @== a(A,B)

Structural comparison is easy to implement.  One possibility is to instantiate
all variables in each term temporarily to unique atoms in a sequence, do a
term comparison between the resulting ground terms, and then uninstantiate the
variables.
Structural comparison can be defined as follows in Prolog:

	:- op( 700, xfx, [@@<, @==]).
	:- dynamic copy_term/1.

	A @@< B :- inst_vars(A, CA), inst_vars(B, CB), CA@<CB.

	A @== B :- inst_vars(A, CA), inst_vars(B, CB), CA==CB.

	inst_vars(A, CA) :- copy(A, CA), inst_vars(CA, 0, _).

	inst_vars(V) --> {var(V)}, !, inc(I), {gen_atom(I, V)}.
	inst_vars(A) --> {atomic(A)}, !.
	inst_vars(S) --> {S=..L}, inst_vars_list(L).

	inst_vars_list([]) --> [].
	inst_vars_list([H|T]) --> inst_vars(H), inst_vars_list(T).

	inc(I, I, I1) :- I1 is I+1.

	gen_atom(I, A) :-
		name(I, IL),
		append("$$$", IL, AL),
		name(A, AL).

	append([], L, L).
	append([X|L1], L2, [X|L3]) :- append(L1, L2, L3).

	copy(A, C) :-
		assert(copy_term(A)),
		retract(copy_term(C)).

There is a slight bug in this version: it assumes that the atom '$$$n' for any
integer will never be used elsewhere in the program.  If the algorithm is
implemented as a part of the system then this can be ensured.
Some comments about this method:
	1. The relative order of two terms is independent of the order of the
	   variables in memory.
	2. Two terms that are equal will still be equal if they are unified
	   together.
	3. Two terms that are equal can become unequal if one of them becomes
	   more instantiated.  This is never true for ==.

A logically sound term comparison
---------------------------------

To be logically sound, if the result of a term comparison cannot be determined
solely by looking at the instantiated portion of the terms then the operation
should do one of two things:
	1. It should delay until the instantiation is done elsewhere, or
	2. It should terminate with an error condition.

Peter Van Roy
vanroy@ji.Berkeley.edu

lee@mulga.oz (Lee Naish) (09/09/88)

In article <25985@ucbvax.BERKELEY.EDU> vanroy@ji.Berkeley.EDU (Peter Van Roy) writes:
>Structural comparison is easy to implement.  One possibility is to instantiate
>all variables in each term temporarily to unique atoms in a sequence, do a
>term comparison between the resulting ground terms, and then uninstantiate the
>variables.
>	:- op( 700, xfx, [@@<, @==]).
>	<code>

I think the sentiments expressed are reasonable, but there are some
improvements which could be made.  Firstly, the @< term ordering should
be preserved as much as possible (Vars @< ints @< atoms etc).
In the implementation presented:

	X @@< a		succeeds
	X @@< ' '	fails
	X @@< 1		fails

The problem it that vars are turned into atoms before comparison (it
could be fixed in the implementation by wrapping a functor around nonvars
in the terms - this also fixes the bug mentioned previously).

Secondly, I think the subsumption (partial) ordering should be preserved
as much as possible also.  With @<, if terms dont contain repeated vars,
A sumbsumes (is more general than) B implies A @< B.  Unfortunately, with
the implementation of @@< given, f(X, X) @@< f(Y, Z).  It could be
changed by using a decreasing sequence of atoms instead of an increasing
sequence.  I think a reasonable specification of this kind of term
comparison would (almost) be:

new_compare(Result, A, B) :-
	if A subsumes B, B subsumes A then Result = (=)
	else if A subsumes B then Result = (<)
	else if B subsumes A then Result = (>)
	else compare(Result, A, B).

The reason why its not quite what we want is that
new_compare(R, f(A+A, 1), f(C+D, 2)) may return R = (<) but
new_compare(R, A+A, C+D) must return R = (>).
The reason is that because the second arguments dont unify, compare is
used for everything, rather than subsumption.  A proper specification/
implementation is much more complex, but I hope you get the idea.

Thirdly (a minor efficiency gripe), this is a perfect example of where
not to use =.. (and where to use functor and arg).

>To be logically sound,...
>	1. It should delay until the instantiation is done elsewhere, or

This is what NU-Prolog termCompare/3 does.

As was pointed out in "Negation and quantifiers in NU-Prolog", a sound
inequality predicate which returns a result is useful for implementing
logical termCompare:
	termCompare(Result, A, B) :-
		is_equal(A, B, R),	% delays until A,B sufficiently
					% instantiated
		(if R = true then	% delays until R instantiated
			Result = (=)
		else
			compare(Result, A, B)
		).

Sound inequality is used in MU-Prolog to implement == also (using an
awful hack):
	A == B :- \+ A ~= B.	% ~= fails iff A == B

The "funny quantifiers" of NU-Prolog can be used in an even more
horrible hack to implement subsumption efficiently:
        % gAll A A ~= B fails if and only if A and B can be unified
        % without binding any variables in B (otherwise it may succeed
        % or delay - we don't care which since its inside \+ anyway)
subsumes(A, B) :-
        \+ (gAll A A ~= B).


	lee

ok@quintus.uucp (Richard A. O'Keefe) (09/10/88)

In article <25985@ucbvax.BERKELEY.EDU> vanroy@ji.Berkeley.EDU (Peter Van Roy) writes:
>Consider the following predicate:
>	a(X,X).
>	a(Y,Z).
>	a(W,T).
>	a(a,b).
>The query
>	| ?- setof(a(X,Y), a(X,Y), S).
>gives the result (slightly edited for readability):
>	S = [a(A,B),a(C,D),a(E,E),a(a,b)]
>
>It would be better if one of a(A,B) and a(C,D) were deleted from this result.
>In order to do this, two terms such as a(X,X) and a(Y,Y) which have the
>same pattern of bound variables should be considered equal.
>This requires a new meaning for the equality of two terms.

[Shouldn't that be "same pattern of UNbound variables"?]

The basic problem here is not really a problem with term comparison.

There are two ways of looking at these variables (A,B,C,D,E).

(1) Since the variables are really "copies" of the original X,Y,
    we should have delayed until X and Y were sufficiently instantiated
    to ensure that duplicate elimination never had to compare a variable
    with a variable.  In this view, the mistake is returning any answer
    at all:  if we were to say
	| ?- all_solutions(a(X,Y), a(X,Y), S), X=foo, Y=baz.
    the answer should be S=[a(foo,baz)].

(2) The other view is that the variables which came back are METAVARIABLES,
    and the list S is really at the meta-level:
	is_a_solution(X) <-> (exists E) member(E, S) & instance_of(X, E)
    In this view, the set S *really* means
	[a($1,$1),a($1,$2),a(a,b)]
    But note that in order to reduce redundancy, we should not just drop
    an element E when it is *equivalent to* another element of S, but we
    should also drop E if it is *subsumed by* another element of S.
    That is to say, a($1,$1) should ALSO be dropped, and a non-redundant
    result is [a($1,$2),a(a,b)].

Note that in neither case do we have any reason to retain a(E,E).
Since a(p,p) is an instance of both a($1,$1) and a($1,$2), it is
implicitly present *twice* in [a(A,B),a(E,E),a(a,b)].  If we are
willing to tolerate that, we have lost our excuse for demanding
that a(C,D) be deleted.

Van Roy's proposal (which I heard a couple of days before from Herve'
Touati) is simply to compare terms by comparing "standardised"
alphabetic variants of them.

>Structural comparison is easy to implement.  One possibility is to instantiate
>all variables in each term temporarily to unique atoms in a sequence, do a
>term comparison between the resulting ground terms, and then uninstantiate the
>variables.

Note that you have to standardise the terms *separately*, because they
might share variables.  {Instead of van Roy's "inst_vars/2", you would
do well to use number_vars/3, which binds variables to '$VAR'(Int) terms.}

Note that the desired effect can be obtained for the special case of
setof/3 by doing

	touati_setof(Template, Generator, Set) :-
		quantify_and_number(Generator, Template, Goal),
		bagof(Template, Goal, Bag),
		sort(Bag, Temp),
		varnumbers_list(Temp, Set).

	quantify_and_number(Q^Generator, Template, Q^Goal) :- !,
		quantify_and_number(Generator, Template, Goal).
	quantify_and_number(Goal, Template, N^(Goal,numbervars(Template,0,N))).

	varnumbers_list([], []).
	varnumbers_list([T|Ts], [S|Ss]) :-
		varnumbers(T, S),
		varnumbers_list(Ts, Ss).

where varnumbers/2 is an inverse of numbervars(_, 0, _).  The query
	| ?- touati_setof(a(X,Y), a(X,Y), S).
gave	S = [a(a,b),a(A,A),a(B,C)]
as the answer, with X and Y still unbound.

>Some comments about this method:
>	2. Two terms that are equal will still be equal if they are unified
>	   together.
>	3. Two terms that are equal can become unequal if one of them becomes
>	   more instantiated.  This is never true for ==.

Note that unifying two terms which are alphabetic variants of each other
can change the relative order of two OTHER terms.  
Part of sort/2 looks like this:

sort(2, [X,Y|Rest], Answer, Rest) :- !,
	compare(R, X, Y),
	sort2(R, X, Y, Answer).

sort2(<, X, Y, [X,Y]).
sort2(>, X, Y, [Y,X]).
sort2(=, X, X, [X]).		<-----

The last clause may indifferently be written sort2(=, X, X, [X]),
sort(=, X, _, [X]), or sort(=, _, X, [X]), because with the standard
ordering, unifying two identical terms has no effect.  With the
"alphabetic variant" ordering, unifying term equivalent terms may
change the relative order of OTHER terms, so it MUST NOT be done by
sort/2.  So one of the other alternative versions of the last clause
must be used.  But there are two choices, and they yield distinguishable
results.

Note also that maintaining two binding environments during term
comparison (should there be any variables which have to be compared)
is intrinsically more costly than maintaining none.