Hi all,
this post addresses 2 insightful comments that were raised in another thread. In short, the question that arose was how determinism and type checking can be combined with our desire to keep our code general and pure.
These questions are both very interesting, with the latter issue in my view being more subtle and insightful a question than the first, and I would like to address these two issues in this thread, independent of the also very interesting challenge that was the primary topic in the other thread.
We start from an obvious CLP(FD) definition of the factorial relation (I assume that you have ":- use_module(library(clpfd))." already in your .swiplrc by now, because using a Prolog system without integer constraints is like... well, there's really nothing else like that):
n_factorial(0, 1).
n_factorial(N, F) :-
N #> 0,
N1 #= N - 1,
F #= N * F1,
n_factorial(N1, F1).
This will serve as a useful, non-trivial though still quite simple relation to illustrate both topics.
I will first show a few tricks and strategies for finding out more about this program.
Obviously, a logic program defines a relation. What relation? Let us find out! Simply ask the
most general query, i.e., the query that has only variables as its arguments for all invoked predicates:
?- n_factorial(N, F).
N = 0,
F = 1 ;
N = F, F = 1 ;
N = F, F = 2 .
OK! So at this point we know that this logic program actually describes some solutions. How many solutions do we expect from the factorial relation? We know that the factorial function is defined for each integer, and seeing how these concrete integers arise in the answers, we certainly expect
no finite bound on the number of solutions! Let us put this into a Prolog query: We expect that the following query
never terminates:
?- n_factorial(N, F), false.
<wait>
What have I done here? I have simply added false/0 at the end of the query, so that the only thing that remains observable is termination, if it occurs. It is a good sign in this case that the query does not terminate! Its termination would mean that we have made a mistake in the implementation of this relation. Go check your definitions of n_factorial/2 with this query! Does this query terminate? If yes, then your definition of the relation is
incomplete!
Let's try a few other queries:
?- n_factorial(_, 5).
false.
?- n_factorial(N, 6).
N = 3 ;
false.
?- n_factorial(30, F).
F = 265252859812191058636308480000000 ;
false.
All very nice and true. But wouldn't it be nice if we could get rid of the dangling choice points in the last two queries?
This is the first issue I am addressing in this post:
Determinism.
Essentially, argument indexing cannot tell in this case that the two clauses are mutually exclusive, and therefore a choice point remains.
To help indexing, we are using the CLP(FD) constraint zcompare/3. This is like compare/3 on integers, except that it is a genuine constraint and remains sound and usable in all directions.
Side-note regarding monotonicity of compare/3:
?- compare(>, A, B).
false.
but adding (!) a constraint yields new solutions:
?- A = 3, compare(>, A, B).
A = 3.
This of course cannot happen with the CLP(FD) constraint zcompare/3:
?- zcompare(>, A, B).
B#=<A+ -1.
?- A = 3, zcompare(>, A, B).
A = 3,
B in inf..2.
Using zcompare/3, the example can be written as:
n_factorial(N, F) :-
zcompare(C, N, 0),
n_factorial_(C, N, F).
n_factorial_(=, _, 1).
n_factorial_(>, N, F) :-
N1 #= N - 1,
F #= N*F1,
n_factorial(N1, F1).
Here are the above queries and their results:
?- n_factorial(N, F).
N = 0,
F = 1 ;
N = F, F = 1 ;
N = F, F = 2 .
?- n_factorial(N, F), false.
<wait>
?- n_factorial(_, 5).
false.
?- n_factorial(N, 6).
N = 3.
?- n_factorial(30, F).
F = 265252859812191058636308480000000.
Aaaaaw yeah! It behaves like before, and the dangling choice points are gone!
But wait, there is more! Let us try two new examples with this definition:
?- n_factorial(3, a).
ERROR: Domain error: `clpfd_expression' expected, found `a'
?- n_factorial(0, a).
false.
Oh no! Why the discrepancy? Obviously, we would prefer some consistency regarding the
type errors, to help automated analyzers and declarative debugging techniques. Luckily, this is also easy to obtain, by changing only the first clause of the auxiliary predicate n_factorial_/3!
Instead of:
n_factorial_(=, _, 1).
we explicitly use the CLP(FD) constraint (#=)/2 to let it perform its type checking which normal unification obviously knows nothing about:
n_factorial_(=, _, F) :- F #= 1.
In total, we have:
n_factorial(N, F) :-
zcompare(C, N, 0),
n_factorial_(C, N, F).
n_factorial_(=, _, F) :- F #= 1.
n_factorial_(>, N, F) :-
N1 #= N - 1,
F #= N*F1,
n_factorial(N1, F1).
And now we get:
?- n_factorial(3, a).
ERROR: Domain error: `clpfd_expression' expected, found `a'
?- n_factorial(0, a).
ERROR: Domain error: `clpfd_expression' expected, found `a'
in addition to the favorable determinism properties discussed above and the intended generality of a true relation.
I regard finding constructs like zcompare/3 as one of the primary challenges in future development of CLP(FD)!
This is only one small step towards obtaining more declarative programs with additional favorable properties. The search space for more such constructs is vast and still almost entirely unexplored! I hope that this and similar constructs will help to eventually resolve the original challenge from which these issues arose.
Using constraints in actual programs is the necessary first step to gradually obtain such constructs after collecting more experience. We cannot solve all arising issues at once, because some are more complex than others and they need to be solved in a certain order. But, in my view, it is high time we at least begin trying!
All the best,
Markus