Apologies on the length of this post but think I need to provide some motivation.
Background: Attributed variables are a core feature supporting many (most?) CLP dialects on Prolog. By permitting logic variables to be “annotated”, they effectively permit constraint networks to be constructed without requiring a separately managed constraint store, really a brilliant idea IMHO.
The constraint networks built using attributed variables are terms which are typically cyclic (they’re graphs) and can get quite large. Each externally visible variable is a “handle” on a different part of the graph. All this works pretty well until you want to print the graph, or walk it, e.g., to make a copy or count the number of nodes, etc. To address these kinds of issues, attribute implementations are expected to support the attribute_goals//1
hook which, given an attributed variable, produces a list of goals which, if evaluated, would reproduce the attributed variable. This list is acyclic so much more amenable to processing.
In practice, the two common uses for this are 1) to support copy_term
and friends, and 2) to produce terms that can be output in answers to a query (like the unification goals commonly seen).
My issue in clpBNR
: For use with copy_term
, you clearly need attribute_goals//1
to generate the full set of goals. However, for answer queries, this quickly results in information overload producing information related to variables internal to the constraint network (which is of little or no practical use to the average user), as well as constraint goals expressed as internal primitives rather than the original expression in source. A simple example:
?- X::real, {2*X**3-15*X**2+26*X+7==0}.
X::real(-4.820793239429888, 8.348881820487701),
(_A::real(-224.07092733268024, 1163.898039012228), {_A==2*_B, _A==_C+_D}),
(_B::real(-112.03546366634012, 581.949019506114), {_B==X**3}),
(_C::real(-224.07092733268024, 118.34062422517708), {-7==_C+_E}),
(_D::real(0, 1045.557414787051), {_D==15*_F}),
(_F::real(0, 69.70382765247005), {_F==X**2}),
(_E::real(-125.34062422517708, 217.07092733268024), {_E==26*X}).
?-
Now imagine what the output would be for the full specification of a system of many such equations. All this may be of some interest to those supporting clpBNR
, and is certainly required to support copy_term
, but the user is probably only interested in the value of X
(i.e., the variable in the query):
X::real(-4.820793239429888, 8.348881820487701)
Note that clpfd
has a similar issue:
?- X in -1000..1000, 2*X^3-15*X^2+26*X+7 #= 0.
X in -1000..1000,
26*X#=_A,
X^2#=_B,
X^3#=_C,
_C in -13003..7512996,
2*_C#=_D,
_D in -26006..15025992,
_E+_F#=_D,
_B in 0..1000000,
15*_B#=_F,
_F in 0..15000000,
_E in -26007..25993,
_E+_A#= -7,
_A in -26000..26000.
?-
clpfd
users probably don’t notice it as much since much of the time clpfd
domains narrow to a point, e.g., by labelling, so the attributes disappear. That essentially can’t happen with clpBNR
reals with floating point bounds due to outward rounding (unless something else in the query actually unifies the variable with a numeric value).
To alleviate the problem, clpBNR
uses a custom flag (clpBNR_verbose
) to optionally provide an abbreviated form of answer output from attribute_goals//1
. When set to false
:
?- X::real,{2*X**3-15*X**2+26*X+7==0}.
X::real(-4.820793239429888, 8.348881820487701).
?-
Using the expand_answer/2
hook, variables in the query can be further annotated such that attribute_goals
can remove all the constraint goals and any “invisible” variables. But this requires that attribute_goals
can detect whether it’s being used in “answer” mode or “copy” mode via some minimal state information. This is fairly easily “hacked” using a global variable when the query and the answer are processed in the same thread. It’s also not an issue when there is no answer generated, e.g., when used by the graphical debugger in the xpce thread. However, in the SWISH environment, the query execution and the answer generation are done in different threads. The only “hack” for this scenario is use the database, but that’s truly global, so not really useful in a server environment. (Global state affects every query.)
A solution: One possibility is to extend the existing attribute_goals
hook with an additional argument which can be used to distinguish between “answer” mode and “copy” mode, default is “copy”. A slight variation is to provide an additional attribute_answer_goals\\1
hook (name TBD) which is specifically used to generate answers. If it fails, try attribute_goals\\1
next.
Any other ideas on how to crack this nut?