Reif cookbook
A few examples on using reif.
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"):
- There are two truth values, corresponding to the atoms
trueandfalse. This is not trivial - the SICStus manual defines a reified constraint as having truth value0or1. - The predicates in
reifare all reified to their last argument.
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
reifthe further conventions hold (and I think this is what you mean by the phrase "reif predicates"):
- There are two truth values, corresponding to the atoms
trueandfalse. This is not trivial - the SICStus manual defines a reified constraint as having truth value0or1.- The predicates in
reifare all reified to their last argument.
I think I have made this more clear.
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.
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.
Is there any need to mention
(->)/2in this text, especially so close to the beginning? Or(is)/2, which is mentioned even earlier? These constructs are very hard to understand, and(->)/2easily 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.
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.
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 ; ... .
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).
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.
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).
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?
Is there any need to mention
(->)/2in this text, especially so close to the beginning? Or(is)/2, which is mentioned even earlier? These constructs are very hard to understand, and(->)/2easily leads to elementary mistakes in programs.
removed the offending (->) and is.
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_.
Can it be merged now, at least as the first version? Who is making the decision about which PRs are merged? I presume @mthom?