unit-testingprologplunit

What is a "Test succeeded with choicepoint" warning in PL-Unit, and how do I fix it?


I'm writing a prolog program to check if a variable is an integer. The way I'm "returning" the result is strange, but I don't think it's important for answering my question.

The Tests

I've written passing unit tests for this behaviour; here they are...

foo_test.pl

:- begin_tests('foo').
:- consult('foo').

test('that_1_is_recognised_as_int') :-
    count_ints(1, 1).

test('that_atom_is_not_recognised_as_int') :-
    count_ints(arbitrary, 0).

:- end_tests('foo').
:- run_tests.

The Code

And here's the code that passes those tests...

foo.pl

count_ints(X, Answer) :-
  integer(X),
  Answer is 1.

count_ints(X, Answer) :-
  \+ integer(X),
  Answer is 0.

The Output

The tests are passing, which is good, but I'm receiving a warning when I run them. Here is the output when running the tests...

?- ['foo_test'].
%  foo compiled into plunit_foo 0.00 sec, 3 clauses
% PL-Unit: foo 
Warning: /home/brandon/projects/sillybin/prolog/foo_test.pl:11:
        /home/brandon/projects/sillybin/prolog/foo_test.pl:4:
        PL-Unit: Test that_1_is_recognised_as_int: Test succeeded with choicepoint
. done
% All 2 tests passed
% foo_test compiled 0.03 sec, 1,848 clauses
true.

The Question(s)


Solution

  • First, let us forget the whole testing framework and simply consider the query on the toplevel:

    ?- count_ints(1, 1).
    true ;
    false.
    

    This interaction tells you that after the first solution, a choice point is left. This means that alternatives are left to be tried, and they are tried on backtracking. In this case, there are no further solutions, but the system was not able to tell this before actually trying them.

    Using all/1 option for test cases

    There are several ways to fix the warning. A straight-forward one is to state the test case like this:

    test('that_1_is_recognised_as_int', all(Count = [1])) :-
        count_ints(1, Count).
    

    This implicitly collects all solutions, and then makes a statement about all of them at once.

    Using if-then-else

    A somewhat more intelligent solution is to make count_ints/2 itself deterministic!

    One way to do this is using if-then-else, like this:

    count_ints(X, Answer) :-
            (   integer(X) -> Answer = 1
            ;   Answer = 0
            ).
    

    We now have:

    ?- count_ints(1, 1).
    true.
    

    i.e., the query now succeeds deterministically.

    Pure solution: Clean data structures

    However, the most elegant solution is to use a clean representation, so that you and the Prolog engine can distinguish all cases by pattern matching.

    For example, we could represent integers as i(N), and everything else as other(T).

    In this case, I am using the wrappers i/1 and other/1 to distinguish the cases.

    Now we have:

    count_ints(i(_), 1).
    count_ints(other(_), 0).
    

    And the test cases could look like:

    test('that_1_is_recognised_as_int') :-
        count_ints(i(1), 1).
    
    test('that_atom_is_not_recognised_as_int') :-
        count_ints(other(arbitrary), 0).
    

    This also runs without warnings, and has the significant advantage that the code can actually be used for generating answers:

    ?- count_ints(Term, Count).
    Term = i(_1900),
    Count = 1 ;
    Term = other(_1900),
    Count = 0.
    

    In comparison, we have with the other versions:

    ?- count_ints(Term, Count).
    Count = 0.
    

    Which, unfortunately, can at best be considered covering only 50% of the possible cases...

    Tighter constraints

    As Boris correctly points out in the comments, we can make the code even stricter by constraining the argument of i/1 terms to integers. For example, we can write:

    count_ints(i(I), 1) :- I in inf..sup.
    count_ints(other(_), 0).
    

    Now, the argument must be an integer, which becomes clear by queries like:

    ?- count_ints(X, 1).
    X = i(_1820),
    _1820 in inf..sup.
    
    ?- count_ints(i(any), 1).
    ERROR: Type error: `integer' expected, found `any' (an atom)
    

    Note that the example Boris mentioned fails also without such stricter constraints:

    ?- count_ints(X, 1), X = anything.
    false.
    

    Still, it is often useful to add further constraints on arguments, and if you need to reason over integers, CLP(FD) constraints are often a good and general solution to explicitly state type constraints that are otherwise only implicit in your program.

    Note that integer/1 did not get the memo:

    ?- X in inf..sup, integer(X).
    false.
    

    This shows that, although X is without a shadow of a doubt constrained to integers in this example, integer(X) still does not succeed. Thus, you cannot use predicates like integer/1 etc. as a reliable detector of types. It is much better to rely on pattern matching and using constraints to increase the generality of your program.