Decisions: guards, cuts and not
Choosing between clauses, branching inside one clause with if-then-else, the cut that commits to an answer, and negation as failure.
Choosing between clauses
The simplest decision is a second clause that matches whatever the first one refused: a clause for a passing score, and after it a clause that catches everything else, written with the anonymous variable because the score no longer matters. The first clause that matches is used, so the order of the clauses is the decision.
Inside a single clause you can branch with an arrow and a semicolon: ( Score >= 50 -> write(pass) ; write(retake) ). Read it as: if this holds, do the first thing, otherwise do the second.
The catch-all clause works because it comes second. grade(_, retake) matches any score, so placing it first would make the passing clause unreachable; every query would get retake. The order is the decision, and the underscore says the score is deliberately not inspected at that point.
The cut, and negation
The cut is an exclamation mark on its own, and it is a promise: do not look for another way to satisfy this rule. In the example it gives the grade rule one answer per score instead of two, because the second clause is never tried once the first has committed.
Negation is a backslash in front of a goal: \+ Goal succeeds when that goal cannot be proved. It is honest about its limits — it means not provable from what is known here and now, not false in the world.
A cut is a commitment, not a print statement. It succeeds immediately and removes the alternatives that were open when the clause was entered, including other clauses of the same predicate and any choices still pending in the goals to its left. It does not undo bindings, and it does not touch goals written after it.
What the cut actually throws away
The cut looks like a full stop for the search, but its reach is easy to overestimate. It discards the choice points created since the clause was entered — the other clauses of the same predicate, and any alternatives still open in the goals to its left. It does not undo bindings, and it does not affect goals to its right.
That is why the usual guard clause puts the test before the cut: grade(Score, pass) :- Score >= 50, !. The test has chosen this clause, and the cut says no other clause may be tried. If the cut came first, the rule would commit before it knew whether the score passed. If-then-else is the safer tool when the decision belongs inside one clause: ( Condition -> Then ; Else ) commits to Then as soon as the condition succeeds, and no cut is needed.
Put the cut after a failing goal and it never runs. With a score of 30, grade(Score, pass) :- Score >= 50, !. fails at the comparison, so the cut is not reached and the second clause still answers retake. That is why the guard comes first; the cut should commit only after the test has succeeded.
What goes wrong
A cut in a rule that is used as a test can remove answers a later caller still needs. If member/2 were written with a cut after the first match, asking for every member would return only the first. Keep cuts at the end of a guard clause, where the clause has already decided, and avoid them in a general-purpose helper.
A negation with a free variable asks whether nobody satisfies the goal. \+ likes(X, tea) with X free succeeds only when no one likes tea, which is almost never what a reader means. Bind X first, then negate; the question becomes about one person, and the answer is the one you expected.
A guard clause, a branch and a negation
% A guard clause, an if-then-else, and a negation.
kind(N, small) :-
N < 10, !.
kind(_, large).
main :-
kind(4, A),
write('4 is '), write(A), nl,
( kind(4, small) -> write('4 is small')
; write('4 is not small') ), nl,
\+ kind(40, small),
write('40 is not small'), nl.
:- initialization(main).One grade per score
Run it and read the three lines. Then delete the cut and run it again to watch the grade rule answer twice, and add a score just above the passing line.
Three mistakes to watch for. A cut inside a rule used as a test can make later queries fail, because it removes alternatives the caller still needed. A negation with a free variable asks whether nobody satisfies the goal, not whether one person does not, so bind the variable first. And an if-then-else with no else branch fails when the condition fails; write ; true if you mean “do nothing”.
Try these next
- Add a
sign/2rule with clauses for negative, zero and positive, and one cut. - Ask
\+ grade(10, pass)and then\+ grade(10, retake)and explain the difference. - Rewrite the grade rule with an if-then-else and no cut.
Who may enter
Run it and read the two answers. Then change bob’s age to 18 and see the second line change.
A negation with a free variable is easy to get wrong. Asking whether nobody likes tea is a very different question from asking whether one particular person does not, so bind the variable first.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does the cut do in the grade rule?
