5

Suppose I have some expressions that look like a /\ b \/ c. I would like to generate the truth table for this, something like:

 a |  b |  c | a /\ b \/ c
---+----+----+-------------+-
 F |  F |  F | F
 F |  F |  T | T
 F |  T |  F | F
 F |  T |  T | T
 T |  F |  F | F
 T |  F |  T | T
 T |  T |  F | T
 T |  T |  T | T

A key idea here is to handle operators that are not already handled by is/2, such as logical implication ->. By the way, this question is derived from a post by reddit user u/emergenthoughts.

The code I have for this is as follows:

bool(0).
bool(1).

negate(1, 0).
negate(0, 1).

eval(Assignments, A, V) :- atom(A), memberchk(A=V, Assignments).
eval(Assignments, \+ E, V) :- eval(Assignments, E, NotV), negate(NotV, V).
eval(Assignments, E1 /\ E2, V) :-
    eval(Assignments, E1, V1),
    eval(Assignments, E2, V2),
    V is V1 /\ V2.
eval(Assignments, E1 \/ E2, V) :-
    eval(Assignments, E1, V1),
    eval(Assignments, E2, V2),
    V is V1 \/ V2.
eval(Assignments, E1 -> E2, V) :-
    eval(Assignments, E1, V1),
    V1 = 1 -> eval(Assignments, E2, V) ; V = 1.

generate_assignment(Variable, Variable=B) :- bool(B).
generate_assignments(Variables, Assignments) :-
    maplist(generate_assignment, Variables, Assignments).

atoms_of_expr(A, A) :- atom(A).
atoms_of_expr(\+ E, A) :- atoms_of_expr(E, A).
atoms_of_expr(E1 /\ E2, A) :- atoms_of_expr(E1, A) ; atoms_of_expr(E2, A).
atoms_of_expr(E1 \/ E2, A) :- atoms_of_expr(E1, A) ; atoms_of_expr(E2, A).
atoms_of_expr(E1 -> E2, A) :- atoms_of_expr(E1, A) ; atoms_of_expr(E2, A).

table_for(E) :-
    setof(A, atoms_of_expr(E, A), Variables),
    write_header(Variables, E),
    write_separator(Variables, E),
    table_rest(Variables, E).

table_rest(Variables, E) :-    
    generate_assignments(Variables, Assignments),
    eval(Assignments, E, Value),
    write_assignments(Assignments, Value),
    fail.
table_rest(_, _) :- true.

write_header([Var|Rest], E) :- 
    write(' '), write(Var), write(' | '), write_header(Rest, E).
write_header([], E) :- writeln(E).

write_separator([_|R], E) :- write('---+-'), write_separator(R, E).
write_separator([], _) :- write('-+-'), nl.

write_assignments([_=Var|Rest], Value) :-
    write(' '), write(Var), write(' | '), write_assignments(Rest, Value).
write_assignments([], Value) :- writeln(Value).

This code produces the slightly worse than desired output, but I didn't want to bore you with a lot of formatting:

?- table_for(a/\b\/c).
 a |  b |  c | a/\b\/c
---+----+----+--+-
 0 |  0 |  0 | 0
 0 |  0 |  1 | 1
 0 |  1 |  0 | 0
 0 |  1 |  1 | 1
 1 |  0 |  0 | 0
 1 |  0 |  1 | 1
 1 |  1 |  0 | 1
 1 |  1 |  1 | 1
true.    

I believe this solution is fairly simple and I like it, but I'm often surprised in Prolog by what the real wizards are able to do so I thought I'd ask if there are significant improvements to be made here. atoms_of_expr/2 feels a bit like boilerplate, since it duplicates the traversal in eval/3. I didn't see a way to use term_variables/2 instead because then I don't think I'd be able to actually supply the names the variables have or bind on them properly with memberchk/2. Am I mistaken?

Daniel Lyons
  • 22,421
  • 2
  • 50
  • 77
  • Interesting question. I was fooling with `term_variables/2` and I think it may be possible to achieve a simpler solution using `term_variables/2`, coming up with some scheme to get readable variable names output, but also assuming the operators are all evaluable by `is/2`. That second condition doesn't seem to be met here. If a mix of standard Prolog operators and custom operations are needed, then that will require a certain amount of complexity. I want to think about this more when I get a chance, though. My gut tells me there are other interesting options to consider. – lurker Oct 30 '18 at 17:32
  • @lurker thanks, I think the design space here is larger than my solution, I'm just not sure what else it contains, so I'm looking forward to seeing what you bring to it – Daniel Lyons Oct 30 '18 at 18:46
  • I did some evaluation based approach [some time ago](https://stackoverflow.com/a/41309449/1109583). You could get a truth table via `setof/3` - the main difference is that your implementation is a bottom-up grounding and my solution is a top down version. I guess there are formulas where each implementation beats the other easily. – lambda.xy.x Nov 05 '18 at 16:10

2 Answers2

3

The following is not entirely what is literally demanded in this task. Still, I want to show how it pays off to stay in the pure subset when reasoning about such tasks, and I therefore focus on this aspect.

The core idea is the following predicate, defining the relation between a Boolean formula and its truth value:

eval(v(V), V)       :- V in 0..1.
eval(\+ E0, V)      :- eval(E0, V0), V #= 1 - V0.
eval(E1 /\ E2, V)   :- eval(E1, V1), eval(E2, V2), V #<==> V1 #/\ V2.
eval(E1 \/ E2, V)   :- eval(E1, V1), eval(E2, V2), V #<==> V1 #\/ V2.
eval((E1 -> E2), V) :- eval(E1, V1), eval(E2, V2), V #<==> (V1 #==> V2).

This uses integer constraints to delegate as much as possible to the Prolog engine. See for more information. If your Prolog system supports them, you can also use constraints as an alternative. Note that we are working on actual Prolog variables instead of reifying the variables and their binding environment within Prolog.

Note also that I am using a so-called clean representation, by distinguishing variables by a unique (arbitrary) principal functor v/1. This makes the predicate amenable to argument indexing and at the same time retains its generality. It is not quite (but almost!) the intended input format. However, it is straight-forward to convert the intended input format to such a clean representation, and I leave this part as a challenge.

As a small additional point, please note the brackets in (E1->E2). The reason for them is:

6.3.3.1 Arguments

An argument (represented by arg in the syntax rules)
occurs as the argument of a compound term or element of
a list. It can be an atom which is an operator, or a term
with priority not greater than 999. ...

So, omitting the brackets is not conforming syntax, and will not work for example in GNU Prolog.

And now we are almost done already, because we can now use Prolog's built-in mechanisms to reason about such formulas and their included variables. Of particular help are term_variables/2, and the variable_names/1 read and write options.

The following is an impure part that does the reading and printing:

run :-
        read_term(Formula, [variable_names(VNs)]),
        term_variables(Formula, Vs),
        maplist(write_variable(VNs), Vs),
        write_term(Formula, [variable_names(VNs)]),
        nl,
        eval(Formula, Value),
        label(Vs),
        maplist(write_variable(VNs), Vs),
        format("~w\n", [Value]),
        false.

write_variable(VNs, V) :-
        write_term(V, [variable_names(VNs)]),
        format(" |  ", []).

Sample usage:

?- run.
|: v(A)/\v(B)\/v(C).
A |  B |  C |  v(A)/\v(B)\/v(C)
0 |  0 |  0 |  0
0 |  0 |  1 |  1
0 |  1 |  0 |  0
0 |  1 |  1 |  1
1 |  0 |  0 |  0
1 |  0 |  1 |  1
1 |  1 |  0 |  1
1 |  1 |  1 |  1
false.

It is good practice to separate the pure and impure parts in your code, because we can now still use the pure part in all directions.

For example:

?- eval(Formula, 0).
Formula = v(0) ;
Formula =  (\+v(1)) ;
Formula =  (\+ \+v(0)) ;
Formula =  (\+ \+ \+v(1)) ;
Formula =  (\+ \+ \+ \+v(0)) ;
etc.

It may not be especially helpful, but it is better than nothing at all. In particular, it illustrates that "eval" is not a good name for this relation, because "eval" is an imperative and suggests only one possible usage mode, whereas in fact the relation can be used in other directions too! I leave finding a better, more declarative name, as a challenge.

It is straight-forward to convert such clean formulas to other representations, and I strongly recommend to use them for all "internal" reasoning. Only for the actual input and output parts, it may be useful to translate between them and other representations.

mat
  • 40,498
  • 3
  • 51
  • 78
1

Hmm I am not certain that I understand the question and the comments, but maybe I have something to contribute. I am very very sorry: this is not code I have written but I remember I found it when I was first doing logic in school somewhere and I am really sorry but I do not remember where I found it. I also changed it a bit because it was not really good before I found it. But if someone recognizes this code please tell me and I take it down or attribute to real author of code.

So here is the code that I had. I named it cryptically lc.pl because 2 letters are only more than 1 letter but less than any other number. I do not know what "c" means. "l" means logic I hope.

:- module(lc, [
        bl/2,
        valid/1,
        contradiction/1,
        lequiv/2,
        truth_table/2,
        op(100, fy, ?),
        op(200, fy, ~),
        op(500, yfx, and),
        op(500, yfx, or),
        op(690, yfx, =>),
        op(700, yfx, <=>)]).

truth_table(F, T) :-
        term_variables(F, Vs),
        bagof(R-Vs, bl(F, R), T).

valid(F) :-
        forall(bl(F, R), R == t).

contradiction(F) :-
        forall(bl(F, R), R == f).

lequiv(F, G) :-
        forall(( bl(F, X), bl(G, Y) ), X == Y).

bl(?X, R) :-
        v(X, R).
bl(~X, R) :-
        bl(X, R0),
        not(R0, R).
bl(X and Y, R) :-
        bl(X, R0),
        and_bl(R0, Y, R).
bl(X or Y, R) :-
        bl(X, R0),
        or_bl(R0, Y, R).
bl(X => Y, R) :-
        bl(X, R0),
        impl_bl(R0, Y, R).
bl(X <=> Y, R) :-
        bl(X, R0),
        bl(Y, R1),
        eq(R0, R1, R).
bl(X xor Y, R) :-
        bl(X, R0),
        bl(Y, R1),
        xor(R0, R1, R).

v(f, f).
v(t, t).

not(t, f).
not(f, t).

and_bl(f, _, f).
and_bl(t, Y, R) :- bl(Y, R).

or_bl(f, Y, R) :- bl(Y, R).
or_bl(t, _, t).

impl_bl(t, Y, R) :- bl(Y, R).
impl_bl(f, _, t).

eq(f, Y, R) :- not(Y, R).
eq(t, Y, Y).

xor(f, Y, Y).
xor(t, Y, R) :- not(Y, R).

Sorry for long code :-(

You see term_variables/2 but you no see atoms_of_expr because it no use atoms because why use atoms when variables are easier to use? I don't know.

Anyway here is how I remember to use it for same example as your example but written in very different way:

?- truth_table(?A and ?B or ?C, Table).
Table = [f-[f, _3722, f], t-[f, _3692, t], f-[t, f, f], t-[t, f, t], t-[t, t, _3608]].

So apparently no README but if you want "boolean variable" and not "logical variable" you need to write a ? infront to make it "boolean". Why ? I don't know. After much trial and error I find I have to write ? before variable to make it boolean; a lot of code that does not terminate if you forget to write ? infront of variable :-(

But it has implication and equivalence in operator list. You see truth table in solution is very different but actually same.

  • @DanielLyons You are certainly very correct to think that there must be additional code. But maybe it is already defined in code I paste? Did you read carefully for example there is line that says `op(100, fy, ?)` in export list and maybe there? But read code slowly at your own pace if you need. Sorry if your comment was not question but statement. –  Nov 09 '18 at 22:27
  • Ah, I missed that. – Daniel Lyons Nov 09 '18 at 22:38