triska / clpz

Constraint Logic Programming over Integers

Home Page:https://www.metalevel.at/prolog/clpz

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Integration with `library(reif)`?

Qqwy opened this issue · comments

Hello there! After watching/reading part of 'the Power of Prolog' (amazing material by the way!), I wanted to get some experience with using clpz.

As challenge, I attempted to implement a variant of FizzBuzz that uses CLP(Z) rather than the builtin arithmetic predicates, as well as if_ (of library(reif)), both with the idea in mind to write as 'logicaly pure' code as possible.

The issue arose that I tried to use (#=)/2 inside if_, which did not work. After reading up on its definition in Scryer-Prolog's standard library as well as the 'indexing dif'-paper, I realized that if_ uses an arity-3 version of the predicate that is passed as first argument.

I was able to adapt the example of (=)/3 given in the paper to (#=) although I am not 100% if my implementation is correct.
To be precise, a call like ?- number_fizzbuzz(5, Res). still produces a choicepoint:

?- number_fizzbuzz(5, Res).
Res = 'Buzz' ;
false.

Am I using clpz and reif correctly together here? Or am I making a mistake?
Is providing a reif-compatible implementation of (#=)/3 (and maybe of the other similar predicates) something that would be worthwhile to add to clpz, or not?

Thank you for your consideration,

~Qqwy/Marten


:- module(fizz_buzz, [main/0, number_fizzbuzz_below_100/2, number_fizzbuzz/2]).
:- use_module(library(reif)).

% for Scryer-Prolog:
:- use_module(library(clpz)).

:- use_module(library(between)).
:- use_module(library(iso_ext)).

% for SWI-Prolog:
% :- use_module(library(clpfd)).

% Prints all solutions to `number_fizzbuzz_below_100` each on a separate line, in order.
% Logically-impure shell.
main :-
    forall(number_fizzbuzz_below_100(_, FizzBuzz), (write(FizzBuzz), write('\n'))).

% Constrains FizzBuzz results to the range 1 <= X <= 100,
% and (for the 'most general query' where neither X or FizzBuzz is concrete)
% ensures results are traversed in order low -> high X (with concrete X).
number_fizzbuzz_below_100(X, FizzBuzz) :-
    between(1, 100, X),
    number_fizzbuzz(X, FizzBuzz).

% States the relationship between a number
% and its FizzBuzz representation.
number_fizzbuzz(Num, FizzBuzz) :-
    if_((Num mod 15 #= 0), FizzBuzz = 'FizzBuzz',
        if_((Num mod 5 #= 0), FizzBuzz = 'Buzz',
            if_((Num mod 3 #= 0), FizzBuzz = 'Fizz',
                Num = FizzBuzz)
           )
       ).

% Reifiable `#=`.
% Is this implementation correct?
% Can this implementation be improved to reduce useless choicepoints?
#=(X, Y, T) :-
      (X #= Y,  T = true)
    ; (X #\= Y, T = false).

Thank you a lot for your interest, and your kind words!

As you note, the choice points arise in your definition of (#=)/3. The predicate is correct, that is quite clear from the definition. To make it deterministic, use for example zcompare/3 from library(clpz):

#=(X0, Y0, T) :-
        X #= X0,
        Y #= Y0,
        zcompare(C, X, Y),
        eq_t(C, T).

eq_t(=, true).
eq_t(<, false).
eq_t(>, false).

zcompare/3 reifies the outcome of the comparison, so that it is amenable to first argument indexing.

Currently, there is only one problem in Scryer Prolog: Many CLP(ℤ) constraints that ought to be deterministic currently themselves leave choice points. Please see the following issues for more information:

mthom/scryer-prolog#662 (comment)

mthom/scryer-prolog#192

Therefore, we currently get for example:

?- X #= 3.
   X = 3
;  false.  % redundant choice point

So, we depend on @mthom to improve this in the engine, then it will run deterministically.

Update: Indexing is now immensely improved via mthom/scryer-prolog#732, making the above example deterministic.


I have one other comment regarding this particular example: As I see it, a still larger improvement to the code can be made by making the output pure. By this, I mean that instead of just writing everything to the system terminal, use for example a DCG to declaratively describe the output! A key advantage of this change is that you then can easily write test cases for the output itself.

This will be especially useful once we have predicates like phrase_to_file/2 that let us use a DCG to directly write the described string to a file:

mthom/scryer-prolog#691

In this way, we can combine a declarative description of the output with good efficiency, since the list need not be fully manifested in memory, and characters are written as soon as they become known. Again, we depend on @mthom to implement this in Scryer Prolog as potentially the first Prolog system ever with this feature.

I hope this helps. Thank you again for your interest, and please file additional issues if you have any further questions. Enjoy!

Slowly but surely my understanding of monotonicity, determinism and logical purity advances 😄 . Thank you very much for your help. Very enlightening!

Would it be useful to have definitions like (#=)/3, (#/=)/3), (#/)/2, (#>)/3 as part of clpz? Or is it reasonable to expect that someone who attempts to use these two libraries together will know how to define them by themselves?


And I definitely look forward to being able to use pio not only for reading but for writing of i/o-streams as well :-) .

Very good point, thank you a lot!

I have filed mthom/scryer-prolog#710 to provide (#=)/3 and (#<)/3 in Scryer Prolog, please have a look and file an issue if you have any comments or suggestions, for example regarding the library location of these predicates.

One other thing I noticed: A few round brackets can be omitted from the snippet you posted, for example:

number_fizzbuzz(Num, FizzBuzz) :-
    if_(Num mod 15 #= 0, FizzBuzz = 'FizzBuzz',
        if_(Num mod 5 #= 0, FizzBuzz = 'Buzz',
            if_(Num mod 3 #= 0, FizzBuzz = 'Fizz',
                Num = FizzBuzz)
           )
       ).

It is always a challenge to find good descriptive names for predicates and variables, and there may be even better names in this case. Maybe integer_output(N, Output)?

Thank you again!

Thank you very much! I'll follow the PR with curious interest and will let you know if I have further comments.


Ah! Those brackets were there from when I was still figuring out if there was a way to make if_ play together nicely with (#=). I tried all kinds of awful things, 😅 .

I definitely agree that integer_* is a better prefix. I dislike the postfix *_output because I think it could be misunderstood for the relation only working in one direction. But *_fizzbuzz definitely is not the most descriptive either 🙈 .

Since the related PR is merged, I think it is a good idea to close this issue for now.
If I find something related later on, I'll open a new issue dedicated to that.

Thank you very much!