Prolog: Control, Negation and Cut • • • • Backtracking The cut operator ! Negation-as-Failure not 1 Backtracking and Nondeterminism member(X, [X|_]). % _ is an anonymous variable member(X, [_|T]) :- member(X, T). ?- member(fred, [john, fred, paul, fred]). yes Deterministic query ?- member(X, [john, fred, paul, fred]). X = john; X = fred; Nondeterministic query X = paul; X = fred; no 2 The Problem of Controlling Backtracking color(cherry, red). color(banana, yellow). color(apple, red). color(apple, green). color(orange, orange). color(X, unknown). ?- color(banana, X). X = yellow ?- color(physalis, X). X = unknown ?- color(cherry, X). X = red; X = unknown; no 3 The CUT • The cut is a built-in predicate written as ! • The cut always succeeds • When backtracking over a cut, the goal that caused the current procedure to be used fails • Not used for its logical properties, but to control backtracking. 4 How CUT works • Suppose goal H is called, and has two clauses: H1 :- B1,… Bi, !, Bk,… Bm. H2 :- Bn,… Bp. • If H1 matches goals B1...Bi are attempted and may backtrack among themselves • If B1 fails, H2 will be attempted • But as soon as ! is crossed, Prolog commits to the current choice. All other choices are discarded. 5 Commitment to the Clause H1 :- B1,… Bi, !, Bk,… Bm. H2 :- Bn,… Bp. • Goals Bk...Bm may backtrack amongst themselves, but • If goal Bk fails, then the predicate fails and the subsequent clauses are not matched 6 How to Remember Cut • Think of the ‘cut’ as a ‘fence’ – When crossed it always succeeds, – And asserts a commitment to the current solution. • When a failure tries to cross the fence, the entire goal is failed. commit H1 :- B1, B2, ..., Bi, !, Bk, ..., Bm. fail calling goal 7 Uses of Cut: Deterministic Predicates 1. To define a deterministic (functional) predicate. Example: a deterministic version of member, which is more efficient for doing ‘member checking’ because it doesn't need to give multiple solutions: membercheck(X, [X|_]) :- !. membercheck(X, [_|L]) :- membercheck(X, L). ?- membercheck(fred, [joe, john, fred, paul]). yes. ?- membercheck(X, [a, b, c]). X = a; no. 8 Uses of Cut: Specify Exclusion Cases 2. To specify exclusion of cases by ‘committing’ to the current choice. Example: max(X, Y, Z) makes Z maximum of X and Y max(X, Y, X) :- X >= Y. max(X, Y, Y) :- X < Y. Note: Both clauses are logically correct statement Using cut can get rid of the second test: max(X, Y, X) :- X >= Y, !. max(X, Y, Y). 9 Max with Cut max(X, Y, X) :- X >= Y, !. max(X, Y, Y). If max is called with X ≥ Y, the 1st clause succeeds, and because of ! the 2nd clause isn't called • The advantage – The test isn't made twice if X<Y • The disadvantage – Each rule isn't a logically correct statement about the predicate. – To see why this is a problem, try ?- max(10, 0, 0). 10 Max with Cut • It is better to use ! and both tests: max(X, Y, X) :- X >= Y, !. max(X, Y, Y) :- X < Y. • This backtracks correctly and avoids unnecessary tests • Or, if order of clause may change: max(X, Y, X) :- X >= Y, ! max(X, Y, Y) :- X < Y, !. 11 A Larger Example • Several versions of the disjoint partial map split: split(list of integers, non-negatives, negatives). 1. Without cut: split([], [], []). split([H|T], [H|Z], R) :- H >= 0, split(T, Z, R). split([H|T], R, [H|Z]) :- H < 0, split(T, R, Z). – Good code • • Each clause stands on its own as a logical statement Inefficient because choice points are retained. – Only the 1st solution is needed, but we don't ignore backtracked solutions 12 A Larger Example (cont.) 2. A version using cut. Most efficient, but not the best ‘defensively written’ code. The third clause does not stand on its own as a fact about the problem. As in normal programming languages, it needs to be read in context. This style is often seen in practice, but is discouraged. split([], [], []). split([H|T], [H|Z], R) :- H >= 0, !, split(T, Z, R). split([H|T], R, [H|Z]) :- split(T, R, Z). • Minor modifications may have unintended effects – E.g., adding more clauses • Backtracked solutions are invalid 13 A Larger Example (cont. 2) 3. A version using cut which is also ‘safe’. The only inefficiency is that the goal H < 0 will be executed unnecessarily whenever H < 0 split([], [], []). split([H|T], [H|Z], R) :- H >= 0, !, split(T, Z, R). split([H|T], R, [H|Z]) :- H < 0, split(T, R, Z). • Recommended for practical use. • Hidden problem: the third clause does not capture the idea that H < 0 is a committal. Here committal is the default because H < 0 is in the last clause. • Some new compilers detect that H < 0 redundant. 14 A Larger Example (cont. 3) 3. A version with unnecessary cuts split([], [], []) :- !. split([H|T], [H|Z], R) :- H >= 0, !, split(T, Z, R). split([H|T], R, [H|Z]) :- H < 0, !, split(T, R, Z). • First cut is unnecessary because anything matching first clause will not match anything else anyway – Most Prolog compilers can detect this • Why is the third cut unnecessary? Because H < 0 is in the last clause. Whether or not H < 0 fails, there are no choices left for the caller of split. However, the above will work for any order of clauses. 15 Mapping with Cut • Remember squint? Maps integers to their squares, map anything else to itself: squint([], []). squint([X|T], [Y|L]) :integer(X), !, Y is X * X, squint(T, L). squint([X|T], [X|L]) :- squint(T, L). 16 More Mapping with Cut Extract the even numbers... evens([], []). evens([X|T], [X|L]) :- 0 is X mod 2, !, evens(T, L). evens([X|T], L) :- evens(T, L). Extract the unique members... uniques([], []). uniques([X|T], L) :- member(X, T), !, uniques(T,L). uniques([X|T], [X|L]) :- uniques(T,L). 17 Negation-as-Failure • Using cut together with the built-in predicate fail defines a kind of negation. • Examples: • Mary likes any animals except reptiles: likes(mary,X) :- reptile(X), !, fail. likes(mary,X) :- animal(X). • A utility predicate meaning something like “not equals”: different(X, X) :- !, fail. different(_, _). 18 Negation-as-Failure: not • We can use the idea of “cut fail” to define the predicate not, which takes a term as an argument • not “calls” the term, evaluating as if it was a goal: – – not(G) fails if G succeeds not(G) succeeds if G does not succeed. • In Prolog, not(G) :- call(G), !, fail. not(_). • call is a built-in predicate. 19 Negation-as-Failure: not (cont.) • Most Prolog systems have a built-in predicate not. SICStus Prolog calls it \+. • not does not correspond to logical negation, because it is based on the success/failure of goals. • It can, however, be useful likes(mary, X) :- not(reptile(X)). different(X, Y) :- not(X = Y). 20 Misleading Negation-as-Failure • A student who missed some Prolog lectures was commissioned to write a Police Database system in Prolog • The database held the names of members of the public, marked by whether they are innocent or guilty of some offence • Suppose the database contains the following: innocent(peter_pan). innocent(X) :- occupation(X, nun). innocent(winnie_the_pooh). innocent(julie_andrews) guilty(X) :- occupation(X, thief). guilty(joe_bloggs). guilty(rolf_harris). • Consider the following dialogue: ?- innocent(st_francis). no. 21 Problem – No may not mean False • This can't be right – we know that St. Francis is innocent • Why does this happen? • Prolog produces no, because st_francis is not in the database • The user will believe it because the computer says so and the database is hidden from the user • How to solve this? 22 not Makes Things Worse • Using not doesn't help guilty(X) :- not(innocent(X)). • This makes matters even worse ?- guilty(st_francis). yes • It is one thing to show that st_francis cannot be demonstrated to be innocent, but it is very bad to incorrectly show that he is guilty 23 Negation-as-Failure can be Non-Logical • More subtle than the innocent/guilty problem, not can lead to some extremely obscure programming errors. • An example using a restaurant database good_standard(goedels). good_standard(hilberts). expensive(goedels). reasonable(R) :- not(expensive(R)). • Consider query: ?- good_standard(X), reasonable(X). X = hilberts • But let's ask the logically equivalent question: ?- reasonable(X), good_standard(X). no. 24 Why Different Answers? • Why different answers for logically equivalent queries? ?- good_standard(X), reasonable(X). ?- reasonable(X), good_standard(X). • In the 1st query, X is always instantiated when reasonable(X) is executed • In the 2nd query, X is not instantiated when reasonable(X) is executed • The semantics of reasonable(X) differ depending on whether its argument is instantiated! 25 Bad Programming Practice • It is bad to write programs that destroy the correspondence between the logical and procedural meaning of a program without any good reason • Negation-as-failure does not correspond to logical negation, and so requires special care 26 How to fix it? • One way is to specify that – Negation of a non-ground formula is undefined • A formula is ground if is has no unbound variables • Some Prolog systems issue a run-time exception when a non-ground goal is negated 27