I am taking a closer look at Constraint Handling Rules (CHR) to see if I can understand them (in the sense what's being computed here and how does classical logic and even linear logic fit into this) and possibly apply them.
Thom Frühwirth's book from 2009 discusses principles of CHR, but implementations may of course differ.
In this case, I'm using the SWI Prolog implementation of CHR.
If I understand well:
- An implementation of CHR would provide at least one "constraint store" to express "the state of the computation". The constraint store contains only ground atoms (i.e. positive literals).
- In a typical CHR session, one first set up the constraint store with an initial state. One writes the CHR program, containing CHR rules. One then runs the CHR program with the constraint store as parameter. Repeated application of the forward-chaining CHR rules until no rules are applicable any longer will iteratively (and destructively) transform the constraint store from its initial state to some final state. The constraint store can then be examined to find the desired answer.
- In this scenario, only don't care non-determinism ("committed choice non-determinism") is considered: when several rules apply at any intermediate state, any is taken.
- The "don't know" non-determinism with backtracking to the choice point in case of later failure is not considered - it is left to the implementation to provide this in one manner or the other, if so desired.
Just as an exercise, a most simple program to compute the GCD using Euclid's Algorithm and retain a log of the operations:
% Constraint `logg(Ti,Msg)` retains the log message `Msg` at step `Ti`
% (which increases monotonically)
% Constraint `gcdpool(X)` denotes a "GCD pool member". At each step, we want
% to compute the GCD of all the X for which there is a `gcdpool(X)` constraint
% in the constraint store. A CHR transformation of the store always reduces the
% sum of the X (variant) while keeping the GCD of the GCD pool members constant
% (invariant). We will thus find a solution eventually.
:- module(my_gcd, [ gcdpool/1, logg/2 ]).
:- use_module(library(chr)).
:- chr_constraint gcdpool/1, logg/2.
% pool contains duplicate: drop one!
gcdpool(N) \ gcdpool(N),logg(Ti,Msg) <=> To is Ti+1, logg(To,[[drop,[N,N],[N]]|Msg]).
% pool contains 1 and anything not 1: keep only 1
gcdpool(1) \ gcdpool(N),logg(Ti,Msg) <=> 1<N | To is Ti+1, logg(To,[[drop,[1,N],[N]]|Msg]).
% otherwise
gcdpool(N) \ gcdpool(M),logg(Ti,Msg) <=> 0<N, N<M | To is Ti+1,
V is M-N,
gcdpool(V),
logg(To,[[diff,[N,M],[N,V]]|Msg]).
This is all very straightforward. A test run in SWI Prolog
?- use_module(library(chr)).
?- [gcd].
?- chr_trace.
% now we enter a goal:
?- gcdpool(6),gcdpool(3),logg(0,[]).
CHR: (0) Insert: gcdpool(6) # <907>
CHR: (1) Call: gcdpool(6) # <907> ? [creep]
CHR: (1) Exit: gcdpool(6) # <907> ? [creep]
CHR: (0) Insert: gcdpool(3) # <908>
CHR: (1) Call: gcdpool(3) # <908> ? [creep]
CHR: (1) Exit: gcdpool(3) # <908> ? [creep]
CHR: (0) Insert: logg(0,[]) # <909>
CHR: (1) Call: logg(0,[]) # <909> ? [creep]
CHR: (1) Try: gcdpool(3) # <908> \ gcdpool(6) # <907>, logg(0,[]) # <909> <=> 0<3,3<6 | _71386 is 0+1,_71404 is 6-3,gcdpool(_71404),logg(_71386,[[diff,[3,6],[3,_71404]]]).
CHR: (1) Apply: gcdpool(3) # <908> \ gcdpool(6) # <907>, logg(0,[]) # <909> <=> 0<3,3<6 | _71386 is 0+1,_71404 is 6-3,gcdpool(_71404),logg(_71386,[[diff,[3,6],[3,_71404]]]). ? [creep]
CHR: (1) Remove: gcdpool(6) # <907>
CHR: (1) Remove: logg(0,[]) # <909>
CHR: (1) Insert: gcdpool(3) # <910>
CHR: (2) Call: gcdpool(3) # <910> ? [creep]
CHR: (2) Exit: gcdpool(3) # <910> ? [creep]
CHR: (1) Insert: logg(1,[[diff,[3,6],[3,3]]]) # <911>
CHR: (2) Call: logg(1,[[diff,[3,6],[3,3]]]) # <911> ? [creep]
CHR: (2) Try: gcdpool(3) # <908> \ gcdpool(3) # <910>, logg(1,[[diff,[3,6],[3,3]]]) # <911> <=> _78066 is 1+1,logg(_78066,[[drop,[3,3],[3]],[diff,[3,6],[3,3]]]).
CHR: (2) Apply: gcdpool(3) # <908> \ gcdpool(3) # <910>, logg(1,[[diff,[3,6],[3,3]]]) # <911> <=> _78066 is 1+1,logg(_78066,[[drop,[3,3],[3]],[diff,[3,6],[3,3]]]). ? [creep]
CHR: (2) Remove: gcdpool(3) # <910>
CHR: (2) Remove: logg(1,[[diff,[3,6],[3,3]]]) # <911>
CHR: (2) Insert: logg(2,[[drop,[3,3],[3]],[diff,[3,6],[3,3]]]) # <912>
CHR: (3) Call: logg(2,[[drop,[3,3],[3]],[diff,[3,6],[3,3]]]) # <912> ? [creep]
CHR: (3) Exit: logg(2,[[drop,[3,3],[3]],[diff,[3,6],[3,3]]]) # <912> ? [creep]
CHR: (2) Exit: logg(1,[[diff,[3,6],[3,3]]]) # <911> ? [creep]
CHR: (1) Exit: logg(0,[]) # <909> ? [creep]
gcdpool(3),
logg(2, [[drop, [3, 3], [3]], [diff, [3, 6], [3, 3]]]) .
The answer is given by the last two lines: The only constraint left in the constraint store is gcdpool(3)
, so 3 is the answer.
Implementation-wise the following the following seems to hold:
There is no dedicated "constraint store". The CHR program is (somehow) compiled into Prolog, and a "CHR constraint" becomes a "Prolog predicate". The "constraint store" as such is the stack of the called Prolog toplevel goal (it is not reified).
Thus the "constraint store" is initialized with the constraints listed in the "CHR goal", and disappears on final exit. You also cannot set up the constraint store in a stepwise or interactive manner, but must do in one line:
gcdpool(6),gcdpool(3),logg(0,[]).
After which the CHR program immediately starts its work.
Indeed, the predicate find_chr_constraint/1, which is supposed to get data out of a constraint store, returns nothing once the CHR program has run. Because there is no constraint store anymore.
Moreover, trying to set up a constraint store in the "CHR program" itself makes no sense. Thus putting logg(0,[])
into the GCD code has no effect. You have to put logg(0,[])
into the CHR goal.
Question
- Is that understanding correct?
- How do I get results of CHR computation back into Prolog?
- What's the deal with the possibility of backtracking provided by the Prolog implementation? How do I put it to use for CHR?
copy_term/3
in mind. In fact, it did not yet exist. – false