Prolog : Recursion problem with a calculated end condition - recursion

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.)

Related

Recursive addition in Prolog

Knowledge Base
add(0,Y,Y). // clause 1
add(succ(X),Y,succ(Z)) :- add(X,Y,Z). // clause 2
Query
add(succ(succ(succ(0))), succ(succ(0)), R)
Trace
Call: (6) add(succ(succ(succ(0))), succ(succ(0)), R)
Call: (7) add(succ(succ(0)), succ(succ(0)), _G648)
Call: (8) add(succ(0), succ(succ(0)), _G650)
Call: (9) add(0, succ(succ(0)), _G652)
Exit: (9) add(0, succ(succ(0)), succ(succ(0)))
Exit: (8) add(succ(0), succ(succ(0)), succ(succ(succ(0))))
Exit: (7) add(succ(succ(0)), succ(succ(0)), succ(succ(succ(succ(0)))))
Exit: (6) add(succ(succ(succ(0))), succ(succ(0)), succ(succ(succ(succ(succ(0))))))
My Question
I see how the recursive call in clause 2 strips the outermost succ()
at each call for argument 1.
I see how it adds an outer succ() to argument 3 at each call.
I see when the 1st argument as a result of these recursive calls
reaches 0. At that point, I see how the 1st clause copies the 2nd
argument to the 3rd argument.
This is where I get confused.
Once the 1st clause is executed, does the 2nd clause automatically
get executed as well, then adding succ() to the first argument?
Also, how does the program terminate, and why doesn't it just keep
adding succ() to the first and 3rd arguments infinitely?
Explanation from LearnPrologNow.com (which I don't understand)
Let’s go step by step through the way Prolog processes this query. The
trace and search tree for the query are given below.
The first argument is not 0 , which means that only the second clause
for add/3 can be used. This leads to a recursive call of add/3 . The
outermost succ functor is stripped off the first argument of the
original query, and the result becomes the first argument of the
recursive query. The second argument is passed on unchanged to the
recursive query, and the third argument of the recursive query is a
variable, the internal variable _G648 in the trace given below. Note
that _G648 is not instantiated yet. However it shares values with R
(the variable that we used as the third argument in the original
query) because R was instantiated to succ(_G648) when the query was
unified with the head of the second clause. But that means that R is
not a completely uninstantiated variable anymore. It is now a complex
term, that has a (uninstantiated) variable as its argument.
The next two steps are essentially the same. With every step the first
argument becomes one layer of succ smaller; both the trace and the
search tree given below show this nicely. At the same time, a succ
functor is added to R at every step, but always leaving the innermost
variable uninstantiated. After the first recursive call R is
succ(_G648) . After the second recursive call, _G648 is instantiated
with succ(_G650) , so that R is succ(succ(_G650) . After the third
recursive call, _G650 is instantiated with succ(_G652) and R therefore
becomes succ(succ(succ(_G652))) . The search tree shows this step by
step instantiation.
At this stage all succ functors have been stripped off the first
argument and we can apply the base clause. The third argument is
equated with the second argument, so the ‘hole’ (the uninstantiated
variable) in the complex term R is finally filled, and we are through.
Let us start by getting the terminology right.
These are the clauses, as you correctly indicate:
add(0, Y, Y).
add(succ(X), Y, succ(Z)) :- add(X, Y, Z).
Let us first read this program declaratively, just to make sure we understand its meaning correctly:
0 plus Y is Y. This makes sense.
If it is true that X plus Y is Z then it is true that the successor of X plus Y is the successor of Z.
This is a good way to read this definition, because it is sufficiently general to cover various modes of use. For example, let us start with the most general query, where all arguments are fresh variables:
?- add(X, Y, Z).
X = 0,
Y = Z ;
X = succ(0),
Z = succ(Y) ;
X = succ(succ(0)),
Z = succ(succ(Y)) .
In this case, there is nothing to "strip", since none of the arguments is instantiated. Yet, Prolog still reports very sensible answers that make clear for which terms the relation holds.
In your case, you are considering a different query (not a "predicate definition"!), namely the query:
?- add(succ(succ(succ(0))), succ(succ(0)), R).
R = succ(succ(succ(succ(succ(0))))).
This is simply a special case of the more general query shown above, and a natural consequence of your program.
We can also go in the other direction and generalize this query. For example, this is a generalization, because we replace one ground argument by a logical variable:
?- add(succ(succ(succ(0))), B, R).
R = succ(succ(succ(B))).
If you follow the explanation you posted, you will make your life very difficult, and arrive at a very limited view of logic programs: Realistically, you will only be able to trace a tiny fragment of modes in which you could use your predicates, and a procedural reading thus falls quite short of what you are actually describing.
If you really insist on a procedural reading, start with a simpler case first. For example, let us consider:
?- add(succ(0), succ(0), R).
To "step through" procedurally, we can proceed as follows:
Does the first clause match? (Note that "matching" is already limited reading: Prolog actually applies unification, and a procedural reading leads us away from this generality.)
Answer: No, because s(_) does not unify with 0. So only the second clause applies.
The second clause only holds if its body holds, and in this case if add(0, succ(0), Z) holds. And this holds (by applying the first clause) if Z is succ(0) and R is succ(Z).
Therefore, one answer is R = succ(succ(0)).. This answer is reported.
Are there other solutions? These are only reported on backtracking.
Answer: No, there are no other solutions, because no further clause matches.
I leave it as an exercise to apply this painstaking method to the more complex query shown in the book. It is straight-forward to do it, but will increasingly lead you away from the most valuable aspects of logic programs, found in their generality and elegant declarative expression.
Your question regarding termination is both subtle and insightful. Note that we must distinguish between existential and universal termination in Prolog.
For example, consider again the most general query shown above: It yields answers, but it does not terminate. For an answer to be reported, it is enough that an answer substitution is found that makes the query true. This is the case in your example. Alternatives, if any potentially remain, are tried and reported on backtracking.
You can always try the following to test termination of your query: Simply append false/0, for example:
?- add(X, Y, Z), false.
nontermination
This lets you focus on termination properties without caring about concrete answers.
Note also that add/3 is a terrible name for a relation: An imperative always implies a direction, but this is in fact much more general and usable also if none of the arguments are even instantiated! A good predicate name should reflect this generality.

Avoiding infinite recursion but still using unbound parameter passing only

I have the following working program: (It can be tested on this site: http://swish.swi-prolog.org, I've removed the direct link to a saved program, because I noticed that anybody can edit it.)
It searches for a path between two points in an undirected graph. The important part is that the result is returned in the scope of the "main" predicate. (In the Track variable)
edge(a, b).
edge(b, c).
edge(d, b).
edge(d, e).
edge(v, w).
connected(Y, X) :-
(
edge(X, Y);
edge(Y, X)
).
path(X, X, _, []) :-
connected(X, _).
path(X, Y, _, [X, Y]) :-
connected(Y, X).
path(X, Z, Visited, [X|Track]) :-
connected(X, Y),
not(member(X, Visited)),
path(Y, Z, [X|Visited], Track).
main(X, Y) :-
path(X, Y, [], Track),
print(Track),
!.
Results:
?- main(a, e).
[a, b, d, e]
true
?- main(c, c).
[]
true
?- main(b, w).
false
My questions:
The list of visited nodes is passed down to the predicates in 2 different ways. In the bound Visited variable and in the unbound Track variable. What are the names of these 2 different forms of parameter passing?
Normally I only wanted to use the unbound parameter passing (Track variable), to have the results in the scope of the main predicate. But I had to add the Visited variable too, because the member checking didn't work on the Track variable (I don't know why). Is it possible to make it work with only passing the Track in an unbound way? (without the Visited variable)
Many thanks!
The short answer: no, you cannot avoid the extra argument without making everything much messier. This is because this particular algorithm for finding a path needs to keep a state; basically, your extra argument is your state.
There might be other ways to keep a state, like using a global, mutable variable, or dynamically changing the Prolog data base, but both are more difficult to get right and will involve more code.
This extra argument is often called an accumulator, because it accumulates something as you go down the proof tree. The simplest example would be traversing a list:
foo([]).
foo([X|Xs]) :-
foo(Xs).
This is fine, unless you need to know what elements you have already seen before getting here:
bar(List) :-
bar_(List, []).
bar_([], _).
bar_([X|Xs], Acc) :-
/* Acc is a list of all elements so far */
bar_(Xs, [X|Acc]).
This is about the same as what you are doing in your code. And if you look at this in particular:
path(X, Z, Visited, /* here */[X|Track]) :-
connected(X, Y),
not(member(X, Visited)),
path(Y, Z, [X|Visited], /* and here */Track).
The last argument of path/4 has one element more at a depth of one less in the proof tree! And, of course, the third argument is one longer (it grows as you go down the proof tree).
For example, you can reverse a list by adding another argument to the silly bar predicate above:
list_reverse(L, R) :-
list_reverse_(L, [], R).
list_reverse_([], R, R).
list_reverse_([X|Xs], R0, R) :-
list_reverse_(Xs, [X|R0], R).
I am not aware of any special name for the last argument, the one that is free at the beginning and holds the solution at the end. In some cases it could be an output argument, because it is meant to capture the output, after transforming the input somehow. There are many cases where it is better to avoid thinking about arguments as strictly input or output arguments. For example, length/2:
?- length([a,b], N).
N = 2.
?- length(L, 3).
L = [_2092, _2098, _2104].
?- length(L, N).
L = [],
N = 0 ;
L = [_2122],
N = 1 ;
L = [_2122, _2128],
N = 2 . % and so on
Note: there are quite a few minor issues with your code that are not critical, and giving that much advice is not a good idea on Stackoverflow. If you want you could submit this as a question on Code Review.
Edit: you should definitely study this question.
I also provided a somewhat simpler solution here. Note the use of term_expansion/2 for making directed edges from undirected edges at compile time. More important: you don't need the main, just call the predicate you want from the top level. When you drop the cut, you will get all possible solutions when one or both of your From and To arguments are free variables.

Understanding Recursive Rule and Unification in Prolog

I'm a beginning Prolog student following the "LearnPrologNow!" set of tutorials. I'm doing my best to get a grip on the concepts and vocabulary. I've been able to understand everything up until Chapter 3 on Recursive Definitions when presented with this problem:
numeral(0).
numeral(succ(X)) :- numeral(X).
given the query
numeral(X).
Now, I understand that the idea of the program is that Prolog will begin counting numbers in this system in a sequence such as
X=0
X=succ(0)
X=succ(succ(0))
But I do not understand what causes it to "scale back" and ascend each time. I understand the principle of unification in that the program is trying to unify the query of X, but should it just follow the recursive rule once, and then return zero? What allows it to add a succ() around the query? Is that not traversing the recursive rule in the opposite direction?
Please think declaratively:
The rule
numeral(succ(X)) :- numeral(X).
means:
If X is a numeral, then succ(X) is a numeral.
:- is like an arrow used in logical implication (it looks similar to <==).
Seeing that you successfully derived that 0 is a numeral (first answer), it is thus no surprise that succ(0) is another solution.
I recommend you think in terms of such relations, instead of trying to trace the actual control flow.
Notice that succ/1 is not added "around the query", but is a part of the actual answer. The term succ(0) is just a normal Prolog term, with functor succ and argument 0.
Already given answer being good, i'll add some more:
Prolog uses denotational syntax (or declarative syntax) to define logical relations/"equations" between terms
A term is an object comprised of variables/functions/placeholders etc..
Unification is the process to check if two expressions (or two terms) can be equal with respect to the given relations/equations.
numeral(succ(X)) :- numeral(X)
Is such a relation/equation which says that the fact that variable-term X is of numeral type (or class), implies the successor functional succ is also of same type. So Prolog here can unify the expression (in other words solve the equation) and replace X with succ(X) and so on, untill the domain of X is covered. So this unification implies X replaced by succ(X) and then unification can be re-applied.
Just to add a proof tree to this answers, which may make things more clear for others:
Base call: numeral(succ(succ(0))).
: ^
rule1 : : {X/0}
: :
V :
numeral(succ(X)).
: ^
rule1 : : {X/0}
: :
V :
numeral(X).
: ^
fact1 : : {X/0}
: :
V :
Found proof [.]
You start with the downwards arrows and move then back up to the previous calls with the new found unificator in the last step. Please note that Prolog declares each variable in each step as a new variable, which I omitted in this scheme.

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.

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