scryer-prolog icon indicating copy to clipboard operation
scryer-prolog copied to clipboard

Reif cookbook

Open dougransom opened this issue 3 months ago • 15 comments

A few examples on using reif.

dougransom avatar Sep 24 '25 23:09 dougransom

You keep using the term "reify" in many different senses, which complicates understanding. Are we reifying a predicate? A goal? A condition? A truth value? A definition? A concept?

I would stick to only talking about "reified predicates" or predicates "reified by a variable".

In reif the further conventions hold (and I think this is what you mean by the phrase "reif predicates"):

  1. There are two truth values, corresponding to the atoms true and false. This is not trivial - the SICStus manual defines a reified constraint as having truth value 0 or 1.
  2. The predicates in reif are all reified to their last argument.

rotu avatar Oct 01 '25 00:10 rotu

You keep using the term "reify" in many different senses, which complicates understanding. Are we reifying a predicate? A goal? A condition? A truth value? A definition? A concept?

I would stick to only talking about "reified predicates" or predicates "reified by a variable".

In reif the further conventions hold (and I think this is what you mean by the phrase "reif predicates"):

  1. There are two truth values, corresponding to the atoms true and false. This is not trivial - the SICStus manual defines a reified constraint as having truth value 0 or 1.
  2. The predicates in reif are all reified to their last argument.

I think I have made this more clear.

dougransom avatar Oct 06 '25 00:10 dougransom

Ouptput from doclog, so you don't have to run it yourself:

reif_examples.html

dougransom avatar Oct 06 '25 00:10 dougransom

Is this ok now for a first cut? I plan to add to it in another version one i understand how to use (,) and (;). I’d like to get this published first.

dougransom avatar Oct 08 '25 19:10 dougransom

Is there any need to mention (->)/2 in this text, especially so close to the beginning? Or (is)/2, which is mentioned even earlier? These constructs are very hard to understand, and (->)/2 easily leads to elementary mistakes in programs.

triska avatar Oct 08 '25 20:10 triska

Is there any need to mention (->)/2 in this text, especially so close to the beginning? Or (is)/2, which is mentioned even earlier? These constructs are very hard to understand, and (->)/2 easily leads to elementary mistakes in programs.

Probably not. Honestly i never tried (->) before writing this tutorial. I’ll address that. I may reintroduce-> and (is) in a later version to illustrate the problems with them.

dougransom avatar Oct 09 '25 18:10 dougransom

Would FizzBuzz be a good example of how to use reification meaningfully?

e.g.

?- use_module(library(reif)).
?- use_module(library(clpz)).
?- N=3,
(0 is N mod 3 -> Fizz = true; Fizz = false),
(0 is N mod 5 -> Buzz = true; Buzz = false),
( Fizz,Buzz -> Answer = "FizzBuzz"
; Fizz -> Answer = "Fizz"
; Buzz -> Answer = "Buzz"
; number_chars(N, Answer)).

Versus a version using reif, which has the advantage that the clause order doesn't matter and you can backtrack through different solutions of N with the semicolon key:

?- use_module(library(reif)).
?- use_module(library(clpz)).
?- (N=2;N=3;N=6;N=15),
#=(0, N mod 3, Fizz),
#=(0, N mod 5, Buzz),
(
 Fizz=true,Buzz=true,Answer="FizzBuzz"
;Fizz=true,Buzz=false,Answer="Fizz"
;Fizz=false,Buzz=true,Answer="Buzz"
;Fizz=false,Buzz=false,number_chars(N, Answer)
).

Can the above be improved maybe? I don't think that 4-way fork can be simplified with use of if_ but maybe I'm wrong.

rotu avatar Oct 30 '25 05:10 rotu

Personally, I think a good program makes clear that it follows the specification. Therefore, if we have for example the following specification of FizzBuzz I found online:

Given an integer n, for every positive integer i <= n, the task is to print,

    "FizzBuzz" if i is divisible by 3 and 5,
    "Fizz" if i is divisible by 3,
    "Buzz" if i is divisible by 5
                   "i" as a string, if none of the conditions are true.

Then what about this program:

i_output(I, Os) :-
    if_((I mod 3 #= 0, I mod 5 #= 0),
        Os = "FizzBuzz",
        if_(I mod 3 #= 0,
            Os = "Fizz",
            if_(I mod 5 #= 0,
                Os = "Buzz",
                number_chars(I, Os)))).

Yielding for example:

?- N = 15, between(1, N, I), i_output(I, Os).
   N = 15, I = 1, Os = "1"
;  N = 15, I = 2, Os = "2"
;  N = 15, I = 3, Os = "Fizz"
;  N = 15, I = 4, Os = "4"
;  N = 15, I = 5, Os = "Buzz"
;  N = 15, I = 6, Os = "Fizz"
;  N = 15, I = 7, Os = "7"
;  N = 15, I = 8, Os = "8"
;  N = 15, I = 9, Os = "Fizz"
;  N = 15, I = 10, Os = "Buzz"
;  N = 15, I = 11, Os = "11"
;  N = 15, I = 12, Os = "Fizz"
;  N = 15, I = 13, Os = "13"
;  N = 15, I = 14, Os = "14"
;  N = 15, I = 15, Os = "FizzBuzz".

We can test it also in other ways, for instance: Which integers yield "Fizz"?

?- length(_, I), catch(i_output(I, "Fizz"), error(syntax_error(_),_), false).
   I = 3
;  I = 6
;  I = 9
;  I = 12
;  I = 18
;  I = 21
;  I = 24
;  I = 27
;  I = 33
;  I = 36
;  ... .

triska avatar Oct 30 '25 05:10 triska

I think that's reasonable but I still don't love the if_s nested 3 deep. Is there a reasonable alternative?

The more legible but less logically pure alternative is the following, and I imagine there must be a better pure option:

i_output_bad(I, Os) :-
  (I mod 3 #= 0, I mod 5 #= 0) -> Os = "FizzBuzz"
; (I mod 3 #= 0) -> Os = "Fizz"
; (I mod 5 #= 0) -> Os = "Buzz"
; number_chars(N, Os).

rotu avatar Oct 30 '25 06:10 rotu

match/2 from my reified pattern matching library Qupak helps flatten this thing while remaining pure. It was primarily made to do patterns, but you can abuse guards to do cond style things like this. This code is basically equivalent to the 3 deep if_/3 ladder that @triska showed earlier:

number_canonchars(N, Cs) :-
    number_chars(N, Cs),
    number_chars(N, Cs1),
    Cs1 = Cs.

fizzbuzz(N, Fizzbuzz) :-
    match(N, [
        ((*) | #N mod 3 #= 0, #N mod 5 #= 0) ~> Fizzbuzz = "FizzBuzz",
        ((*) | #N mod 3 #= 0) ~> Fizzbuzz = "Fizz",
        ((*) | #N mod 5 #= 0) ~> Fizzbuzz = "Buzz",
        (*) ~> number_canonchars(N, Fizzbuzz)
    ]).

?- length(_,N), fizzbuzz(N, Fb).
   N = 0, Fb = "FizzBuzz"
;  N = 1, Fb = "1"
;  N = 2, Fb = "2"
;  N = 3, Fb = "Fizz"
;  N = 4, Fb = "4"
;  N = 5, Fb = "Buzz"
;  N = 6, Fb = "Fizz"
;  N = 7, Fb = "7"
;  N = 8, Fb = "8"
;  N = 9, Fb = "Fizz"
;  N = 10, Fb = "Buzz"
;  N = 11, Fb = "11"
;  N = 12, Fb = "Fizz"
;  N = 13, Fb = "13"
;  N = 14, Fb = "14"
;  N = 15, Fb = "FizzBuzz"
;  N = 16, Fb = "16"
;  ... .
?- length(_, I), catch(fizzbuzz(I, "Fizz"), error(syntax_error(_),_), false).
   I = 3
;  I = 6
;  I = 9
;  I = 12
;  I = 18
;  I = 21
;  I = 24
;  ... .

This also made me realize a bug on my handling of modules, which was kinda expected given the cursed things I'm doing with them here.

bakaq avatar Oct 30 '25 13:10 bakaq

That Qupak example is cool, but it's bringing in a lot!

I wonder if it makes sense to think of reif as something like a monad. Is this cursed?

:- meta_predicate(lift_t(0, ?)).
lift_t(G, true) :- G.

:- meta_predicate(eval_reif(1)).
eval_reif(G_1) :- call(G_1, true).

:- meta_predicate('->'(1, 0, ?)).
'->'(If_1, Then_0, T) :- cond_t(If_1, Then_0, T).

fizzbuzz(I, Os) :- eval_reif((
    (I mod 3 #= 0, I mod 5 #= 0) -> Os = "FizzBuzz"
  ; (I mod 3 #= 0) -> Os = "Fizz"
  ; (I mod 5 #= 0) -> Os = "Buzz"
  ; lift_t(number_chars(I,Os))
)).

% e.g.:
% N = 15, between(1, N, I), fizzbuzz(I, Os).
% length(_, I), catch(fizzbuzz(I, "Fizz"), error(syntax_error(_),_), false).

rotu avatar Oct 30 '25 21:10 rotu

I think that's reasonable but I still don't love the if_s nested 3 deep. Is there a reasonable alternative?

The more legible but less logically pure alternative is the following, and I imagine there must be a better pure option:

i_output_bad(I, Os) :-
  (I mod 3 #= 0, I mod 5 #= 0) -> Os = "FizzBuzz"
; (I mod 3 #= 0) -> Os = "Fizz"
; (I mod 5 #= 0) -> Os = "Buzz"
; number_chars(N, Os).

Is that what cond_t is for? if without the else?

dougransom avatar Nov 15 '25 19:11 dougransom

Is there any need to mention (->)/2 in this text, especially so close to the beginning? Or (is)/2, which is mentioned even earlier? These constructs are very hard to understand, and (->)/2 easily leads to elementary mistakes in programs.

removed the offending (->) and is.

dougransom avatar Nov 15 '25 20:11 dougransom

Personally, I think a good program makes clear that it follows the specification. Therefore, if we have for example the following specification of FizzBuzz I found online:

Given an integer n, for every positive integer i <= n, the task is to print,

"FizzBuzz" if i is divisible by 3 and 5,
"Fizz" if i is divisible by 3,
"Buzz" if i is divisible by 5
               "i" as a string, if none of the conditions are true.

Then what about this program:

i_output(I, Os) :- if_((I mod 3 #= 0, I mod 5 #= 0), Os = "FizzBuzz", if_(I mod 3 #= 0, Os = "Fizz", if_(I mod 5 #= 0, Os = "Buzz", number_chars(I, Os)))). Yielding for example:

?- N = 15, between(1, N, I), i_output(I, Os). N = 15, I = 1, Os = "1" ; N = 15, I = 2, Os = "2" ; N = 15, I = 3, Os = "Fizz" ; N = 15, I = 4, Os = "4" ; N = 15, I = 5, Os = "Buzz" ; N = 15, I = 6, Os = "Fizz" ; N = 15, I = 7, Os = "7" ; N = 15, I = 8, Os = "8" ; N = 15, I = 9, Os = "Fizz" ; N = 15, I = 10, Os = "Buzz" ; N = 15, I = 11, Os = "11" ; N = 15, I = 12, Os = "Fizz" ; N = 15, I = 13, Os = "13" ; N = 15, I = 14, Os = "14" ; N = 15, I = 15, Os = "FizzBuzz". We can test it also in other ways, for instance: Which integers yield "Fizz"?

?- length(, I), catch(i_output(I, "Fizz"), error(syntax_error(),_), false). I = 3 ; I = 6 ; I = 9 ; I = 12 ; I = 18 ; I = 21 ; I = 24 ; I = 27 ; I = 33 ; I = 36 ; ... .

Added this example since it shows pretty well how to use if_.

dougransom avatar Nov 15 '25 20:11 dougransom

Can it be merged now, at least as the first version? Who is making the decision about which PRs are merged? I presume @mthom?

dougransom avatar Nov 15 '25 20:11 dougransom