Solving mutually recursive constraints in Prolog - recursion

I am attempting to solve some mutually recursive constraints with SWI-Prolog. These constraints are relatively simple, but querying any of these predicates leads to infinite recursion:
%If X is an animal, then X is a bird or a mammal, and vice-versa.
animal(X) :-
(mammal(X);bird(X)),
(male(X);female(X)).
male(X) :- animal(X).
female(X) :- animal(X).
bird(X) :- (X='parrot';X='pigeon'),animal(X).
mammal(X) :- (X='cat';X='dog'),animal(X).
Would it be possible to solve these constraints in Prolog without making them non-recursive?
I wrote a similar program with several base cases, but the query mammal(X),bird(X) still leads to infinite recursion instead of returning false:
%If X is an animal, then X is a bird or a mammal, and vice-versa.
animal(X) :-
(mammal(X);bird(X)).
bird('parrot').
bird('pigeon').
bird(X) :- (X='parrot';X='pigeon'),animal(X).
mammal('cat').
mammal('dog').
mammal(X) :- (X='cat';X='dog'),animal(X).

Solving a recursive constraint requires one or more base cases; you have not provided any. The problem isn't with Prolog; its with the problem definition.

It is possible to find solutions for mutually recursive constraints using constraint handling rules.
This is a set of mutually recursive constraints:
%If X is an animal, then X is a bird or a mammal, and vice-versa.
:- use_module(library(chr)).
:- chr_constraint mammal/2,bird/2,animal/1,male/1,female/1,species/2.
animal(X) <=>
(mammal(X,Species);bird(X,Species)),
(male(X);female(X)).
male(X),female(X) ==> false.
bird(X,Species) <=> member(Species,[parrot,pigeon,crow]),species(X,Species).
bird(X,Species) ==> animal(X).
mammal(X,Species) <=> member(Species,[cat,dog,bull]),species(X,Species).
mammal(X,Species) ==> animal(X).
species(X,bull) ==> male(X).
...and this is the output of a query for this program:
?- male(X),mammal(X,Species).
male(_G67406)
species(_G67406,cat)
Species = cat

I think what you're trying to get at is that you have birds and you have mammals. And you are further trying to establish that a creature is an animal if it is either a bird or a mammal.
The code currently over-specifies, and has circular logic.
Walking through the code...
animal(X) :-
(mammal(X); bird(X)).
This says that X is an animal if X is a mammal or X is a bird. So far, so good.
The description of bird reads:
bird('parrot').
bird('pigeon').
These are facts that indicate that a parrot is a bird and a pigeon is a bird. But then there's this rule:
bird(X) :- (X='parrot';X='pigeon'),animal(X).
Which says that X is a bird if X is either a parrot or pigeon, AND if X is an animal. The prior two facts already establish that parrot and pigeon are birds. Why is this rule necessary? And it further adds the condition that X is an animal, which is, in turn, defined in terms of bird and mammal, so is circular.
Similar holds true for the mammal definition. It has the needed facts for mammals:
mammal('cat').
mammal('dog').
And then overspecifies with circular logic:
mammal(X) :- (X='cat';X='dog'), animal(X).
The essence of what you need is simply:
bird('parrot').
bird('pigeon').
mammal('cat').
mammal('dog').
animal(X) :-
mammal(X); bird(X).
This logic defines what creatures are birds or mammals using facts, then provides a rule that says if a creature is known to be a bird or a mammal, then it's an animal.

One solution to such problems is to simply enable your Prolog system's tabling mechanism.
For example, in SWI-Prolog (latest development version), if I simply add the following directives at the start of your program:
:- use_module(library(tabling)).
:- table animal/1.
Then I get for example:
?- animal(X).
false.
?- male(X).
false.
?- bird(X).
false.
So, in these cases, we still do not find any solution, but at least we get answers.

Related

What determines the order in which predicates are run?

I am working with some code that was in part written by my professor that takes a tableau as input, expands it, and then outputs the complete tableau.
I am having trouble understanding the logic behind the order in which the predicates are being run. What is the deciding factor that would, say, make the program address conjunction before negation if both were present in the original formula? And how does recursion play into this?
The code is as follows:
%negation
expand([[not(not(X))|B]|T], T1) :-
expand([[X|B]|T], T1).
%conjunction
expand([[(X)+(Y)|B]|T], T1):-
expand([[X, Y|B]|T], T1).
expand([[not((X)+(Y))|B]|T], T1):-
expand([[not(X)|B], [not(Y)|B]|T], T1).
%disjunction
expand([[(X)/(Y)|B]|T], T1):-
expand([[X|B], [Y|B]|T], T1).
expand([[not((X)/(Y))|B]|T], T1):-
expand([[not(X), not(Y)|B]|T], T1).
%not sure what the rest is or how it works
expand([[X|B]|T1], T5) :-
expand([B], T2), distribute(X,T2,T3), expand(T1,T4), append(T3,T4,T5).
expand([[]|T1], [[]|T2]) :-
expand(T1, T2).
expand([],[]).
distribute(X,[B|T],[[X|B]|T1]) :-
distribute(X,T,T1).
distribute(_,[],[]).
Apologies for the vague post, I am unfamiliar with this language
A Prolog program consists of
Facts, a simple statement of truth. Something like this
mother( jane , alice ) .
mother( jane , john ) .
is two facts that state that Jane is the mother of both Alice and John.
Predicates, more complex assertions of truth. Something like this:
sibling(X,Y) :- same_mother(X,Y), same_father(X,Y) .
half_sibling(X,Y) :- same_mother(X,Y) , \+ same_father(X,Y) .
half_sibling(X,Y) :- \+ same_mother(X,Y) , same_father(X,Y) .
same_mother(X,Y) :- mother(M,X), mother(M,Y) .
same_father(X,Y) :- father(F,X), father(F,Y) .
states that
two people, X and Y, are siblings if they have both the same mother and the same father, and
two people, X and Y, are half-siblings if they have either
the same mother and different fathers, or
different mothers and the same father
Each predicate is a logic proposition written in a restricted form of the predicate calculus, and forms what amounts to a search tree (more of a search graph, really). When you query/execute a predicate, say
sibling(stephen,alice).
Prolog's inference engine essentially traverses the search tree until either succeeds or fails. If it succeeded, one can backtrack into it, and it will continue the traversal until it either succeeds again, or fails.
So, the order in which predicates are "executed" is entirely dependent on the predicate with which evaluation began, and it's structure.
Note that, depending on what arguments/parameters are instantiated or not instantiated, when a predicate is evaluated, allows one to ask questions with more than one answer:
sibling( john, X ). asks the question "Who are John's siblings?"
sibling( X , Y ). asks the question "Who are all the pairs of siblings?"
A good text to start learning Prolog are
Clocksin+Mellish's Programming in Prolog — it's a pretty good introduction to the language.
Clocksin's Clause and Effect — I've heard good things about this as a beginner's text, but I've not read it.
Once you've gotten the basics down...
Sterling+Shapiro's The Art of Prolog is a most excellent book to take you deeper
And O'Keefe's The Craft of Prolog is yet a deeper dive.

Seeing if there is a possible train route through Prolog

A problem I'm working on with Prolog is to see if a train can travel from one destination to the next. There are two rules.
A train can travel through on or more intermediary from one destination to the next.
Ex: San Francisco to Los Angeles
Los Angeles to Irvine
Irvine to San Diego
This gives a route from San Francisco to San Diego.
A train can travel to and from a destination. So if a train can travel from San Francisco to Los Angeles, it can travel from Los Angeles to San Francisco.
This is the code I currently have.
nonStopTrain(sandiego,oceanside).
nonStopTrain(lasvegas,sandiego).
nonStopTrain(sanfrancisco,bakersfield).
nonStopTrain(bakersfield,sandiego).
nonStopTrain(oceanside,losangeles).
nonStopTrain(portland,sanfrancisco).
nonStopTrain(seattle,portland).
canTravel(From, To) :- nonStopTrain(From, To); nonStopTrain(To, From).
canTravel(From, To) :-
canTravel(From, Through), canTravel(Through, To).
The problem is the ability to travel bidirectionally. When I run this program, I keep running back and fourth between the same places, and I'm not exactly sure why.
The problem with a naive solution is that there are an infinite number of ways to get from point A to point B if you don't eliminate cycles. Suppose I want to go from Seattle to San Francisco. Without handling cycles, we're going to get each of these as a unique solution:
seattle -> portland -> seattle -> portland -> sanfrancisco
seattle -> portland -> seattle -> portland -> seattle -> portland -> sanfrancisco
seattle -> (portland -> seattle) x N -> sanfrancisco
There's no limit to the number of times you can double back on yourself, so there's effectively an infinite number of solutions once you have as little as three nodes connected. In practice you don't want any solutions where you double back on yourself, but Prolog doesn't know that and there's no intuitive and naive way to prevent it.
One of the better ways forward is to simply keep track of where you've been. To do that we're going to need to make the predicate take an extra argument. First I've also introduced a helper predicate.
connectedDirectly(From, To) :- nonStopTrain(From, To) ; nonStopTrain(To, From).
Having this separated out will reduce the desire to call canTravel recursively when we really just want to attach one more leg to the journey. Now for canTravel:
canTravel(From, To) :- canTravel(From, To, []).
This is an arity 2 rule that maps onto our new arity 3 rule. The list of places we've been is always empty initially. Now we need a base case:
canTravel(From, To, _) :- connectedDirectly(From, To).
This should be obvious. Now the inductive case is a little different, because we need to do two things: find a new leg to attach to the journey, make sure we haven't been through that leg before, and then recur, adding the new leg to the list of places we've been. Finally, we want to ensure we don't get large cycles where we end up where we started, so we add a rule to the end to make sure we don't.
canTravel(From, To, Visited) :-
connectedDirectly(From, Through),
\+ memberchk(Through, Visited),
canTravel(Through, To, [Through|Visited]),
From \= To.
Now if you run it you'll find you get 98 solutions and all the solutions are symmetric:
?- forall(canTravel(X, Y), (write(X-Y), nl)).
sandiego-oceanside
lasvegas-sandiego
sanfrancisco-bakersfield
... etc.
So, happily, we were able to avoid going for a breadth-first search solution.
Edit
I have apparently confused the situation by overloading the name canTravel for two separate predicates. In Prolog, a predicate is uniquely defined by the name and the arity, much like overloading in C++ or Java where the "effective method" is determined by the number of arguments and the name, not just the name.
Your intuition is correct—the empty list in canTravel(From, To) :- canTravel(From, To, []) is establishing an initial binding for the list of visited places. It's not exactly allocating storage so much as establishing a base case.
There are really two uses of canTravel inside itself. One of them is calling canTravel/3 from canTravel/2. In this case, canTravel/3 is really sort of like a helper, doing the actual work of canTravel/2, but with an internal variable that we are initializing to the empty list. The other use is canTravel/3 from within canTravel/3, and for that we're both using it to achieve the same goal: recursion, Prolog's primary "looping" construction.
The third argument in canTravel(From, To, _) :- connectedDirectly(From, To) is what makes this clause part of canTravel/3. This is the base case of the recursion, so it doesn't need to consider the places we've visited so far (although the inductive case will prevent a circular journey). We could also check it here, but it turns out to be more expensive and have no effect on the resultset:
canTravel(From, To, Visited) :- connectedDirectly(From, To), \+ memberchk(To, Visited).
I concluded that if it was adding expense and complexity without changing the answers we could omit the check, which reduces the base case to the original one with the anonymous third variable.
It may make more sense to see this without the overloading, in which case it looks like this:
canTravel(From, To) :- canTravel_loop(From, To, []).
canTravel_loop(From, To, _) :- connectedDirectly(From, To).
canTravel_loop(From, To, Visited) :-
connectedDirectly(From, Through),
\+ memberchk(Through, Visited),
canTravel_loop(Through, To, [Through|Visited]),
From \= To.
Edit 2
Regarding the "bar operator," your intuition is once again correct. :) I'm using it here to prepend an item to a list. What's confusing you is that in Prolog, with unification, most expressions express relationships rather than procedures. So depending on the context, [X|Xs] might be used to construct a new list (if you have X and XS in hand) or it might be used to break an implicit list into a head X and tail Xs. Look at all the ways I can use it just from the repl:
?- X = hello, Xs = [world, new, user], Y = [X|Xs].
Y = [hello, world, new, user].
This is basically how we're using it in canTravel: we have Through and we have Visited, so we're making a new list with Through first and Visited as the tail, and that's the third parameter to the recursive invocation. In procedural terms, we're just adding Through to a variable we're using in our loop.
But because this is Prolog, we're not limited to using things in one direction:
?- Y = [hello, world, new, user], Y = [X|Xs].
X = hello,
Xs = [world, new, user].
?- Y = [hello, world, new, user], [X|Xs] = Y.
X = hello,
Xs = [world, new, user].
Notice that Prolog didn't care which direction the assignment happened in, but it managed to "work backwards" to figure out what X and Xs should be using Y. This is one of the magical things about Prolog. (Note that in the examples in this session I'm omitting the variables which are echoed back because they obscure the point.)
In general, you want predicates that can solve for different parameters. For instance, member/2 can be used to test membership or to enumerate items. append/3 can build a new list from two old lists, or it can enumerate all the ways to split a list into two segments, or it can find a prefix or suffix given a list and a suffix or prefix.
As you get more used to this functionality you'll stop thinking of Prolog rules as being like functions in other languages and start to see them as relations: logical "truths" that exist between certain constructions. member/2 isn't written by trying to enumerate items or by seeking through a list looking for a particular value. It's implemented by saying: the relation member(Item, List) is true when the Item is the first thing in List:
member(Item, [Item|_]).
or else when Item is in the remainder of the list:
member(Item, [_|Tail]) :- member(Item, Tail).
This definition is sufficient for all the possible uses. If Item is not instantiated, it will be instantiated to the first item in the list, then the first item in the tail of that list, and so on. If Item is instantiated, it will be true if Item is the first item in the list or if it is the first item in the tail. Surprisingly, member/2 can even be used to generate lists that contain a value:
?- member(1, X).
X = [1|_G274] ;
X = [_G8, 1|_G12] ;
X = [_G8, _G11, 1|_G15] .
You can see what happened there: the _ in the second clause is being made into anonymous variables, so it's generating lists with the 1 in the first position, then the second, then the third, etc.
A lot of Prolog works like this. This one is also pretty surprising:
?- length(X, 3).
X = [_G273, _G276, _G279].
Hope this helps clarify things a bit more! :)
Do you have use some specific Prolog system?
Your program will work as intended without modifications (well, you have to add :- auto_table. as a first line of your program) in a system with tabling support, like B-Prolog.
I think adding a cut will stop your infinite recursion issue because once it finds an answer it won't keep backtracking forever:
canTravel(From, To) :- nonStopTrain(From, To); nonStopTrain(To, From).
canTravel(From, To) :-
canTravel(From, Through), canTravel(Through, To), !.
I have no doubt that there is a more correct solution than this though.

Prolog Recursion - Satisfying Both Directions (Simple)

I am very new to Prolog and I was given this assignment.
My code is as follows:
relatives(cindy,tanya).
relatives(tanya,alan).
relatives(alan,mike).
relatives(kerry,jay).
relatives(jay,alan).
isRelated(X,Y):-
relatives(X,Y).
isRelated(X,Y):-
relatives(X,Z),
isRelated(Z,Y).
Simple enough. This shows that if:
?- isRelated(cindy,mike).
Prolog will return true. Now, I'm stuck on how to make it return true if:
?- isRelated(mike,cindy).
I've been trying to come up with ideas like if isRelated(Z,Y) returns false, then switch X and Y, and run isRelated again. But I'm not sure if Prolog even allows such an idea. Any hints or advice would be greatly appreciated. Thanks!
UPDATE:************************************
So I added:
isRelated(X,Y):-
relatives(X,Y);
relatives(Y,X).
That will satisfy "direct" relationships, but simply enough I found out that it doesn't satisfy indirect relationships.
I really want to do something like, if the initial query:
isRelated(mike,cindy)
fails, then try and see if the reverse is true by switching X and Y:
isRelated(cindy,mike)
That will definitely return true. I just don't know how to do this syntactically in Prolog.
Further hint to those in the comments, as I can't leave comments yet: With your original set of rules and facts,
isRelated(cindy,tanya) is true, but isRelated(tanya,cindy) is not, so you need to make isRelated(X,Y) symmetric; what simple addition to isRelated would achieve that?
Also, you could try drawing a graph of the relation relatives(X,Y), with an arrow from X to Y for all your base facts, and see if that helps you think about how the Prolog interpreter is going to attempt to satisfy a query.
So to answer your last question, you don't switch the values of X and Y in Prolog, like you would call swap(x,y) in C, say. The value held by a logic variable can not be changed explicitly, only back-tracked over. But you can easily use Y where you would use X, and vice versa:
somePred(X,Y):- is_it(X,Y).
somePred(X,Y):- is_it(Y,X).
This defines somePred predicate as a logical disjunction, an "OR". It can be written explicitly too, like
somePred(X,Y):- is_it(X,Y) ; is_it(Y,X).
Note the semicolon there. A comma , between predicates OTOH defines a conjunction, an "AND" (a comma inside a compound term just serves to delimit the term's "arguments").
YOu're almost there, you're just trying, I think, to cram too much stuff into one predicate.
Write the problem statement in English and work from that:
A relationship exists between two people, X and Y
if X and Y are directly related, or
if any direct relative of X, P, is related to Y.
Then it gets easy. I'd approach it like this:
First, you have your set of facts about relatives.
related( cindy, tanya ).
...
related( james, alan ).
Then, a predicate describing a direct relationship is terms of those facts:
directly_related( X , Y ) :- % a direct relationship exists
related(X,Y) % if X is related to Y
. % ... OR ...
directly_related( X , Y ) :- % a direct relationship exists
related(Y,X) % if Y is related to X
. %
Finally, a predicate describing any relationship:
is_related(X,Y) :- % a relationship exists between X and Y
directly_related(X,Y) % if a direct relationship exists between them
. % ... OR ...
is_related(X,Y) :- % a relationship exists between X and Y
directly_related(X,P) , % if a direct relationship exists between X and some other person P
is_related(P,Y) % and [recursively] a relationship exists between P and Y.
. %
The solution is actually more complicated than this:
The facts about relationships describe one or more graphs. More on graphs at http://web.cecs.pdx.edu/~sheard/course/Cs163/Doc/Graphs.html. What you're doing is finding a path from node X to Node Y in the graph.
If the graphs described by the facts about relationships have one or more paths between X and Y, the above solution can (and will) succeed multiple times (on backtracking), once for every such path. The solution needs to be deterministic. Normallly, having established that two people are related, we're done: just because I have two cousins doesn't mean I'm related to my aunt twice.
If the graph of relationships contains cycles (almost certainly true) such that a "circular" path exists: A → B → C → A …, the solution is susceptible to unlimited recursion. That means the solution needs to detect and deal with cycles. How might that be accomplished?

Prolog : Recursion problem with a calculated end condition

I'm trying to model a set of cog wheels in Prolog. And to query to find if two wheels are connected by a chain of others.
I have a simple recursion :
connected(X,Y) :-
engages(X,Y).
connected(X,Y) :-
engages(X,Z),
connected(Z,Y).
If I define engages in terms of a hard-wired predicate :
engages(X,Y) :- touches(X,Y).
and
touches(z1,z2).
touches(z2,z3).
touches(z3,z4).
Then this works.
A test of
connected(z1,z4).
produces true and
connected(z1,z5).
produces false.
However, if I replace my touches predicate with a calculation (that the sum of the two radii is roughly the distance between the two centres), then this search goes into what looks like an infinite recursion (stack overflow) when the answer should be false.
Why should this be, given that my "touches" calculation doesn't itself call the "connected" functor? Is it because a touches functor is going to try to match more atoms than the explicit predicates?
This, roughly, is my calculated touches (A and B are the wheels, ARAD and BRAD are their radii, D is distance and approx is an "approximately equals" function.
touches(A,B) :-
wheel(A,_,_,ARAD),
wheel(B,_,_,BRAD),
distance(A,B,D),
approx(D,(ARAD+BRAD),2),
A \== B.
The infinite recursion occurs because the program doesn't check for cycles. Here's what might happen:
The program finds wheels A and B, that touch.
Given B, it looks for a wheel that touches. It finds A.
Goto 1.
You can prevent this from maintaining a "closed set" of wheels already considered in an extra argument to connected:
connected(X,Y) :-
connected(X,Y,[]).
connected(X,Y,Closed) :-
touches(X,Y),
\+ memberchk(Y,Closed).
connected(X,Z,Closed) :-
touches(X,Y),
\+ memberchk(Y,Closed),
connected(Y,Z,[Y|Closed]).
(Exercise for the reader: shorten this.)

GNU Prolog - Recursion issue (easy?)

Ok, so i have this
edu_less(hs,college).
edu_less(college,masters).
edu_less(masters,phd).
I need to write a function to tell if something is less than the other. The predicate is
edu_le.
So if i put edu_le(hs,phd). it should return yes.
I came up with this.
edu_le(A,B) :- A = B.
edu_le(A,B) :- edu_less(A,B).
edu_le(A,B) :- edu_less(A,C), edu_le(C,B).
I really don't want it to go through everything and return multiple answers.
Is it possible to only return yes or no if it finds that it is in fact less than or equal to the 2nd argument?
So basically if i use the example edu_le(hs,phd) again, then because hs is less than college, and college is than masters, and masters is less than phd, then hs must be less than phd and it would say yes.
Sorry, really new to prolog, still trying to get the hang of this.
In the predicate definition
edu_le(A,B) :- A = B.
edu_le(A,B) :- edu_less(A,B).
edu_le(A,B) :- edu_less(A,C), edu_le(C,B).
the second clause is superfluous and causes repeated generation of answers. Use
edu_le(A,B) :- A = B.
edu_le(A,B) :- edu_less(A,C), edu_le(C,B).
This gives you one true answer, then no more answers (false) on backtracking. You can use a cut in the first clause, but then generating won't work anymore.
?- edu_le(hs,X).
X = hs ;
X = college ;
X = masters ;
X = phd ;
false.
becomes incomplete:
?- edu_le(hs,X).
X = hs.
As mat suggested, use once/1 instead. In a good Prolog implementation, this predicate works as if your program had cuts in strategic places, speeding up your program without disturbing its logical semantics.
The most practical way to write predicates like that is to use the cut (!). The cut causes further clauses not to be considered when backtracking. You would write your predicate as following:
edu_le(A,B) :- A = B, !.
edu_le(A,B) :- edu_less(A,B), !.
edu_le(A,B) :- edu_less(A,C), edu_le(C,B).
The last clause does not need a cut because there are no further clauses to consider anyway. The cut is placed after any tests to determine whether the clause should succeed.
Logic programming purists disapprove of the cut, because it makes the meaning of a predicate depend on the ordering of the clauses, which is unlike logic in mathematics.
!/0 also makes this program incomplete, consider for example the most general query with both versions:
?- edu_le(X, Y).
It is often better to use once/1 if you only want a single proof of a particular goal:
?- once(edu_le(hs, phd)).
I would suggest you NOT to follow the path proposed by Juho Östman and keep purity - otherwise, why should you use Prolog in first instance? If you are too lenient with sticking to the logical paradigm you obtain some unpleasing results. In this case, Juho's predicate is definitely different from yours, and I'll show you why.
First, just drop the useless edu_le(A,B) :- edu_less(A,B). rule, as larsmans suggests. You will obtain a less redundant version of your original predicate:
edu_le1(A, A).
edu_le1(A, B) :- edu_less(A, C), edu_le1(C, B).
It just behaves as edu_le, meaning: given an arbitrary query, it produces exactly the same answer, except for duplicates (edu_le1 has less). You may just be happy with it, but it still has some redundant answers that you may not like; e.g, under SWI:
?- edu_le1(hs, hs)
true ;
false.
Now you may say you do not like it because it still has the redundant false, but if you use Juho's predicate instead (without the useless rule):
edu_le2(A, A) :- !.
edu_le2(A, B) :- edu_less(A, C), edu_le2(C, B).
it's true that you eliminate the useless final false:
?- edu_le2(hs, hs)
true.
?-
but you lose more than that: You lose, as mat remarks, the possibility of generating all the solutions when one variable is not instantiated:
?- edu_le1(hs, B) %same, with more copies, for edu_le
B = hs ;
B = college ;
B = masters ;
B = phd ;
false.
?- edu_le2(hs, B)
B = hs. %bad!
?-
In other words, the latter predicate is NOT equivalent to the former: edu_le and edu_le1 have type edu_le(?A, ?B), while instead edu_le2 has type edu_le2(+A, +B) (see [1] for the meaning). Be sure: edu_le2 is less useful because it does less things, and thus can be reused in less contexts. This because the cut in edu_le2 is a red cut, i.e., a cut that changes the meaning of the predicate where it is introduced. You may nevertheless be content with it, given that you understand the difference between the two. It all depends on what you want to do with it.
If you want to get the best of the two worlds, you need to introduce in edu_le1 a proper green cut that lowers the redundancy when A and B are completely instantiated to terms. At the purpose, you must check that A and B are instantiated to the same term before cutting. You cannot do it with =, because = does not check, but unifies. The right operator is ==:
edu_le3(A, B) :- (A == B -> ! ; true), A = B.
edu_le3(A, B) :- edu_less(A, C), edu_le3(C, B).
Note that the additional cut in the first rule is active only when A and B happen to be the same term. Now that the cut is a proper green cut, the predicate works also in the most general cases as your original one:
?- edu_le3(A, A).
true.
?- edu_le3(A, B). %note that A and B are not the same term
A = B ;
A = hs,
B = college ;
A = hs,
B = masters ;
A = hs,
B = phd ;
A = college,
B = masters ;
A = college,
B = phd ;
A = masters,
B = phd ;
false.
?-
with Prolog backtracking through all the solutions.
I don't think there is some way to eliminate the last false without introducing too strong dependency on edu_lt. This because we must keep open the possibility that there is another edu_lt to explore, in the case you decide later to enrich it with more ground facts. So, in my opinion, this is the best you can have.
[1] SWI Prolog reference manual, section 4.1.

Resources