Mixed info deduction

I can’t imagine a title that really describes what I’m after, so apologies for that…

I’m currently working with geometric objects (for no particular reason), and I’m wondering if there’s a good way to implement the use of two different, yet equivalent, definitions for something, such that no matter what you are given, prolog can fill in as many unknowns as possible.

Concrete example:

An equaliateral triangle can be defined as a triangle with 3 sides of equal length. It can also be defined as a triangle with 3 equal angles (which happen to be 60° if they’re planar, 90° if drawn on a sphere, etc).

On top of that, side lengths and angles can be calculated from each other given the right information.

So I’m wondering if there’s a simple way that I could write a predicate that could deduce if a triangle is equilateral no matter what combination of sides and angles I have, assuming enough info is given to prove one way or another?

My default would be to write two predicates:

equilateral( line(A), line(B), line(C) ) :- 
    length(line(A), L), 
    length(line(B), L), 
    length(line(C), L).
equilateral( angle(A), angle(B), angle(C) ) :- 
    degrees(angle(A), D),
    degrees(angle(B), D),
    degrees(angle(C), D).

But even doing things this way, I can’t think of a way to tell prolog to take what it knows (what I’ve given it elsewhere) about line-angle relationships to calculate for these. Seems like I’d need a million if-then clauses to handle the cases where it has mixed info.

Not an important project, but if anyone has ideas on simple ways to account for mixed-info scenarios, I’d love to hear your thoughts!

when/2 can be used to delay evaluation of a predicate until the arguments are sufficiently ground (e.g., two of the three angles).

An alternative is a constraint solver.

As this appears to be a learning exercise, I’ll stop here … however, the description of when/2 is rather terse, so if it’s not clear how to use it, please ask for more information.

A constraint solver is probably more along the lines of what I’m looking for, and I’m already implementing CLPR for other aspects of this project, so it wouldn’t affect anything to implement it here.

With the predicates I have, I can probably pass three points, test if they’re a triangle, and then test if the distances between them are equal, or if they form equal angles. I could also pass three lines and do the same. Each could be its own predicate, thanks to a kind of overloading Prolog can do. But I’m trying to find a universe where I don’t have to pass lines and points to every predicate, and could instead pass bigger shapes while being a bit agnostic to the details until I need them.

So maybe I’m really asking if Prolog can implement a kind of pseudo-OOP the way C can?

If you want, yes. Logtalk is an example. Depending on what you need, there are simple lightweight alternatives for some OO features, mostly based on modules or passing additional arguments. My first intuition would go to overloading, e.g.

distance(line(...), line(...), D) :- ...
distance(line(...), point(...), D) :- ...
...

That is different from OO. The relation is central rather than the object. Note that (SWI-)Prolog handles the above pretty efficient. Given enough clauses, calls with the first two arguments instantiated will create a hash table to find the candidate clause(s).

If you do not like this notation, invent whatever notation you do like and use program transformation (term_expansion/2) to turn your favourite notation into something efficient.

How about:

triangle_type_drawn(eq, sides(L, L, L), angles(A, A, A), Drawn) :-
    drawn_eq_triangle_angles(Drawn, A).
triangle_type_drawn(non_eq, sides(L1, L2, L3), angles(A1, A2, A3), _) :-
    not_all_equal([L1, L2, L3]),
    not_all_equal([A1, A2, A3]).

drawn_eq_triangle_angles(planar, 60).
drawn_eq_triangle_angles(sphere, 90).

not_all_equal([]).
not_all_equal([H|T]) :-
    % A simple method with weak inference
    % Would be stronger to use attributed variables
    when(ground([H|T]), once((
        select_forward(E1, [H|T], L1),
        member(E2, L1),
        E1 \= E2
    ))).
    
% A variation on select/3 which only goes forward through the list
select_forward(E, [H|T], F) :-
    select_forward_(T, H, E, F).

select_forward_(T, H, H, T).
select_forward_([H|T], _, E, F) :-
    select_forward_(T, H, E, F).

Simplest case:

?- triangle_type_drawn(T, S, A, D).
T = eq,
S = sides(_A, _A, _A),
A = angles(60, 60, 60),
D = planar ;
T = eq,
S = sides(_A, _A, _A),
A = angles(90, 90, 90),
D = sphere ;
T = non_eq,
S = sides(_A, _B, _C),
A = angles(_D, _E, _F),
when(ground([_A, _B, _C]), once((select_forward(_G, [_A, _B, _C], _H), member(_I, _H), _G\=_I))),
when(ground([_D, _E, _F]), once((select_forward(_J, [_D, _E, _F], _K), member(_L, _K), _J\=_L))).

not_all_equal/1 has weak but sound inference, e.g.:

?- triangle_type_drawn(T, S, A, D), S = sides(2, 2, 999).
T = non_eq,
S = sides(2, 2, 999),
A = angles(_A, _B, _C),
when(ground([_A, _B, _C]), once((select_forward(_D, [_A, _B, _C], _E), member(_F, _E), _D\=_F))).

It is weak because all the sides must be ground/1 before any of the sides are compared (even at the point where 2 are known to be different), which is non-optimal.

It is sound because it will always be correct, when the sides are eventually ground/1.

Same for sides and areas.

I’ll mark this as the solution I suppose.

It matches what I’ve already been doing, as a means of type checking (ensuring that points are being passed to the predicates), and it would work to take account of every variation.

I would obviously prefer if I could write a single predicate that could handle whatever it’s given, but odds are what I’m actually after will be a few rules and a few equivalency facts (such as below):

triangle( line(A), line(B), line(C) ) = triangle( angle(A), angle(B), angle(C) ).

Of course, I’m going to need to carefully define what an angle is, but in the end, the long way seems to be the right way.

I may still take a look at some Logtalk and see if I can’t get some inspiration on how to scale this up. But anyway, thanks for the help!

I think you just need a “canonical” triangle representation.

Then you implement:
shape_triangle(S, T)
which succeeds when S can be represented using T: the canonical triangle representation.

Then:
equilateral(S) :- shape_triangle(S, T) ... # test based on you canonical representation

Hello,
I agree with @z5h that you need to choose a canonical representation.
For example, instead of talking about lines or angles in the canonical representation, you should just reduce to points: triangle(A, B, C).

Then, your mixed information collapse to a single clause:

equilateral_triangle(triangle(A, B, C)) :-
  triangle(triangle(A, B, C)),
  equilateral(A, B, C).

triangle(triangle(A, B, C)) :-
  line(A, B),
  line(B, C),
  line(C, A).

equilateral(A, B, C) :-
  line(A, B, Length),
  line(B, C, Length),
  line(C, A, Length),
  angle(A, B, C, Angle),
  angle(B, C, A, Angle),
  angle(C, A, B, Angle).

Turns out I have talked about triangles before ^^: Triangle · Kwon-Young Choi
and Epsilon · Kwon-Young Choi for using clpBNR in the mix.