Prolog - Unification - Backtracking - Recursion - Lists - Cut
Prolog - Unification - Backtracking - Recursion - Lists - Cut
Prolog - Unification - Backtracking - Recursion - Lists - Cut
• Unification
• Backtracking
• Recursion
• Lists
• Cut
Unification
• When two terms match we say that they unify.
– Their structures and arguments are compatible.
• This can be checked using =/2
|?- loves(john,X) = loves(Y,mary).
X = mary, unification leads to instantiation
Y = john?
yes Terms that unify Outcome
fred = fred. yes.
Terms that don’t unify ‘Hey you’ = ‘Hey you’. yes
fred = jim. fred=X. X=fred.
‘Hey you’ = ‘Hey me’. X=Y. Y = X.
frou(frou) = f(frou). foo(X) = foo(bar). X=bar.
foo(bar) = foo(bar,bar). foo(N,N) = foo(bar,X). N=bar, X=bar.
foo(N,N) = foo(bar,rab). foo(foo(bar)) = foo(X) X = foo(bar)
Tests within clauses
• These operators can be used within the body of a
clause:
– To manipulate values,
sum(X,Y,Sum):-
Sum is X+Y.
bigger(N,M):-
N < M,
write(‘The bigger number is ‘), write(M).
bigger(N,M):-
N > M,
write(‘The bigger number is ‘), write(N).
bigger(N,M):-
N =:= M,
write(‘Numbers are the same‘).
Backtracking
|?- bigger(5,4).
bigger(5,4):- Backtrack
5 < 4, fails
write(‘The bigger number is ‘), write(M).
bigger(N,M):-
N > M,
write(‘The bigger number is ‘), write(N).
bigger(N,M):-
N =:= M,
write(‘Numbers are the same‘).
Backtracking
|?- bigger(5,4).
bigger(N,M):-
N < M,
write(‘The bigger number is ‘), write(M).
bigger(5,4):-
5 > 4,
write(‘The bigger number is ‘), write(N).
bigger(N,M):-
N =:= M,
write(‘Numbers are the same‘).
Backtracking
|?- bigger(5,4).
bigger(N,M):-
N < M,
write(‘The bigger number is ‘), write(M).
bigger(5,4):-
5 > 4, succeeds, go on with body.
write(‘The bigger number is ‘), write(5).
bigger(N,M):-
N < M,
write(‘The bigger number is ‘), write(M).
bigger(N,M):-
N > M,
write(‘The bigger number is ‘), write(N).
bigger(5,5):-
5 =:= 5, Is already known as the first two clauses failed.
write(‘Numbers are the same‘).
Backtracking
|?- bigger(5,5). If our query only matches the final clause
bigger(N,M):-
N < M,
write(‘The bigger number is ‘), write(M).
bigger(N,M):-
N > M,
write(‘The bigger number is ‘), write(N).
bigger(5,5):-
Satisfies the same conditions.
write(‘Numbers are the same‘).
• This is fine for checking what the code is doing but not for using
the proof.
1
| ?- bigger(6,4,Answer),bigger(Answer,5,New_answer).
3 2
bigger(X,Y,Answer):- X>Y.
, Answer = X.
bigger(X,Y,Answer):- X=<Y.
, Answer = Y.
bigger(X,Y,X):- X>Y.
bigger(X,Y,Y):- X=<Y.
• BACKTRACKING
– when the system fails during its search, it returns to previous
choices to see if making a different choice would allow
success.
Satisfying Subgoals
• Most rules contain calls to other predicates in their body. These are
known as Subgoals.
4) likes(Person,Drink):-
drinks(Person,Drink).
5) likes(Person,Somebody):-
likes(Person,Drink),
likes(Somebody,Drink).
Representing Proof using Trees
• To help us understand Prolog’s proof strategy we can
represent its behaviour using AND/OR trees.
1. Query is the top-most point (node) of the tree.
2. Tree grows downwards (looks more like roots!).
3. Each branch denotes a subgoal.
1. The branch is labelled with the number of the matching clause and
2. any variables instantiated when matching the clause head.
4. Each branch ends with either: |?- likes(ali,X).
1. A successful match ,
2. A failed match , or 2 X/coffee
3. Another subgoal. 1st solution
= “ali likes coffee.”
X = coffee
Representing Proof using Trees (2)
• Using the tree we can see what happens when we ask
for another match ( ; )
|?- likes(ali,X).
2 Backtracking
X/coffee 4
X = coffee
drinks(ali,X).
1st match is failed
and forgotten 1 X/tea
X = tea
2nd solution
= “Ali likes tea because Ali drinks tea.”
Recursion using Trees
• When a predicate calls itself within its body we say
the clause is recursing
|?- likes(ali,X).
Conjoined subgoals
2
5
X/coffee 4
X = coffee
drinks(ali,X). likes(ali,Drink)
likes(Somebody,Drink)
1 X/tea X/coffee 2
X = tea
X = coffee
Recursion using Trees (2)
• When a predicate calls itself within its body we say
the clause is recursing
|?- likes(ali,X).
2
5
X/coffee 4
X = coffee
drinks(ali,X). likes(ali,coffee)
likes(Somebody,coffee)
1 X/tea X/coffee 2
Somebody 2
X = tea /ali
X = coffee
Somebody = ali
likes(ali,coffee) likes(Somebody,coffee)
1 X/tea
Somebody
X/coffee 2 Somebody 3 / hasan
/ali
X = tea 2
X = coffee Somebody
4 solution =
th
Somebody
“ali likes hasan = hasan
= ali
because hasan likes coffee.”
Infitite Recursive Loop
• If a recursive clause is called with an incorrect goal it will loop
as it can neither prove it
nor disprove it.
likes(Someb,coffee)
Somebody 2 3 5
= ali likes(Someb,coffee)
Somebody 2 likes(coffee,coffee)
= hasan Someb = ali
likes(coffee,X3)
likes(X2,X3)
Why use recursion?
• It allows us to define very clear and elegant code.
– Why repeat code when you can reuse existing code.
Symbols Signs
a Strings <--->
bob ==>
l8r_2day ‘a’
…
‘Bob’
‘L8r 2day’
Structures
• To create a single data element from a collection of
related terms we use a structure.
• A structure is constructed from a functor (a constant
symbol) and one of more components.
functor
somerelationship(a,b,c,[1,2,3])
X = tom,
is_older(Old,Young):-
Y = jim ? ;
parent(Old,Young).
X = mary,
Y = tom ? ;
is_older(Ancestor,Young):-
X = mary,
is_older(Someone,Young),
Y = jim ? ;
is_older(Ancestor,Someone).
*loop*
X = tom,
is_older(Old,Young):-
Y = jim ? ;
parent(Old,Young).
X = mary,
Y = tom ? ;
is_older(Ancestor,Young):-
X = mary,
parent(Someone,Young),
Y = jim ? ;
is_older(Ancestor,Someone).
no
Fail(1): member(ringo,[john|_]).
(2): member(ringo,[_|paul,ringo,george]):-
Call: member(ringo,[paul,ringo,george]).
Fail(1): member(ringo,[paul|_]).
(2): member(ringo,[_|ringo,george]):-
Call: member(ringo,[ringo,george]).
Succeed(1): member(ringo,[ringo|_]]).
1) member(H,[H|_]).
2) member(H,[_|T]):-
member(H,T).
Quick Aside: Tracing Prolog
• To make Prolog show you its execution of a goal type
trace. at the command line.
– Prolog will show you:
• which goal is Called with which arguments,
• whether the goal succeeds (Exit),
• has to be Redone, or Fails.
| ?- member(stuart,[john,paul,ringo,george]).
1 1 Call: member(ringo,[john,paul,ringo,george]) ?
2 2 Call: member(ringo,[paul,ringo,george]) ?
3 3 Call: member(ringo,[ringo,george]) ?
4 4 Call: member(stuart,[george]) ?
5 5 Call: member(stuart,[]) ? [ ] does not match [H|T]
5 5 Fail: member(stuart,[]) ?
4 4 Fail: member(stuart,[george]) ?
3 3 Fail: member(ringo,[ringo,george]) ?
2 2 Fail: member(ringo,[paul,ringo,george]) ?
1 1 Fail: member(ringo,[john,paul,ringo,george]) ?
no
Collecting Results
• When processing data in Prolog there are three ways to
collect the results:
1. Compute result at base case first, then use this result as
you backtrack through the program.
2. Accumulate a result as you recurse into the program and
finalise it at the base case.
3. Recurse on an uninstantiated variable and accumulate
results on backtracking.
listlength([_|T],N1):-
listlength(T,N),
N1 is N+1.
listlength([],0).
Compute lower result first: trace.
listlength([],0).
listlength([_|T],N1):-
listlength(T,N),
N1 is N+1.
| ?- listlength([a,b,c],N).
1 1 Call: listlength([a,b,c],_489) ?
2 2 Call: listlength([b,c],_1079) ?
3 3 Call: listlength([c],_1668) ?
4 4 Call: listlength([],_2257) ?
4 4 Exit: listlength([],0) ?
5 4 Call: _1668 is 0+1 ?
5 4 Exit: 1 is 0+1 ?
3 3 Exit: listlength([c],1) ?
6 3 Call: _1079 is 1+1 ?
6 3 Exit: 2 is 1+1 ?
2 2 Exit: listlength([b,c],2) ?
7 2 Call: _489 is 2+1 ?
7 2 Exit: 3 is 2+1 ?
1 1 Exit: listlength([a,b,c],3) ?
N = 3 ? yes
Why compute lower result first?
listlength([],0). listlength([],_).
listlength([_|T],N1):- listlength([_|T],N):-
N1 is N+1, N1 is N+1,
listlength(T,N). listlength(T,N1).
|?-listlength([a,b,c],N). |?-listlength([a,b,c],0).
Instantiation error in 1 Call: listlength([a,b,c],0) ?
is/2 2 2 Call: _1055 is 0+1 ?
fail 2 2 Exit: 1 is 0+1 ?
3 2 Call: listlength([b,c],1) ?
4 3 Call: _2759 is 1+1 ?
4 3 Exit: 2 is 1+1 ?
5 3 Call: listlength([c],2) ?
6 4 Call: _4463 is 2+1 ?
6 4 Exit: 3 is 2+1 ?
7 4 Call: listlength([],3) ?
7 4 Exit: listlength([],3) ?
5 3 Exit: listlength([c],2) ?
3 2 Exit: listlength([b,c],1) ?
1 1 Exit: listlength([a,b,c],0)?
yes
Using an Accumulator
• You can also accumulate results as you recurse into the
program, finalising the result at the base.
• Once the result is finalised we need someway of getting
it back out of the program.
listlength([_|T],Acc,Out):-
Acc1 is Acc+1,
Increase Accumulator
as we recurse listlength(T,Acc1,Out).
Instantiate result to
Output variable in base case
and pass back.
Using an Accumulator (2)
listlength([a,b,c],0,N).
1 1 Call:
listlength([a,b,c],0,_501) ?
2 2 Call: _1096 is 0+1 ?
2 2 Exit: 1 is 0+1 ?
3 2 Call: listlength([b,c],1,_501) ?
listlength([],A,A).
4 3 Call: _2817 is 1+1 ?
listlength([_|T],A,O):-
4 3 Exit: 2 is 1+1 ? A1 is A+1,
5 3 Call: listlength([c],2,_501)? listlength(T,A1,O).
6 4 Call: _4538 is 2+1 ?
6 4 Exit: 3 is 2+1 ?
7 4 Call: listlength([],3,_501) ?
7 4 Exit: listlength([],3,3) ?
5 3 Exit: listlength([c],2,3) ?
3 2 Exit: listlength([b,c],1,3) ?
1 1 Exit: listlength([a,b,c],0,3) ?
N = 3 ?
yes
Using an auxiliary predicate
• When using an accumulator it needs to be initialised at
the right value (e.g. [] or 0).
• Make the predicate with the accumulator an auxiliary to
the predicate that the user will call.
listlength(List,Length):-
listlength2(List,0,Length).
Auxiliary
to main listlength2([],A,A). Initialise
predicate listlength2([_|T],A,O):- Accumulator
A1 is A+1,
listlength2(T,A1,O).
|?-append(X,[c,d],[a,b,c,d]).
X = [a,b] ? ;
no
Computing in reverse
• Both reverse/3 and append/3 can be used backwards to
make two lists out of one.
• This can be a useful way to strip lists apart and check their
contents.
| ?- reverse(X,Y,[a,b,c,d]).
X = [], Y = [a,b,c,d] ? ;
X = [a], Y = [b,c,d] ? ; reverse([],L2,L2).
X = [b,a], Y = [c,d] ? ; reverse([H|T],L2,Out):-
X = [c,b,a], Y = [d] ? ; reverse(T,[H|L2],Out).
X = [d,c,b,a], Y = [] ? ;
*loop*
|?-reverse([d,c,b,a],Y,[a,b,c,d]).
Y = [] ?
yes
Summary
• Base and recursive cases
• Using focused recursion to stop infinite loops.
• List processing through recursion: member/2
• Introduced the Prolog tracer.
• Showed three techniques for collecting results:
– Recursively find a result, then revise it at each level.
• listlength/3
– Use an accumulator to build up result during recursion.
• reverse/3
– Build result in the head of the clause during
backtracking.
• append/3
Controlling Backtracking:
The Cut
Clearing up equality
• There are various ways to test equality in Prolog.
X = Y succeeds if the terms X and Y unify.
To redo a goal:
1. discard bindings from previous success;
2. try clauses for this goal not so far tried;
3. if none, fail the goal.
Byrd Box model
• This is the model of execution used by the tracer.
• Originally suggested by Lawrence Byrd.
CALL EXIT
GOAL
FAIL REDO
Exception (error)
Redo-ing a Goal
fact(b,1).
fact(b,2).
a :- fact(b,N), fact(c,N).
|?- a.
CALL EXIT
N=1
fact(b,N)
Redo-ing a Goal (2)
fact(b,1).
fact(b,2).
a :- fact(b,N), fact(c,N).
|?- a.
|?- a.
EXIT
N=1
CALL EXIT
N=2
fact(b,N)
REDO
Redo-ing a Goal (4)
fact(b,1).
fact(b,2).
a :- fact(b,N), fact(c,N).
|?- a.
no.
EXIT
N=1
CALL EXIT CALL
N=2
fact(b,N) fact(c,2)
FAIL REDO FAIL
Prolog’s Persistence
• When a sub-goal fails, Prolog will backtrack to the most recent successful
goal and try to find another match.
• Once there are no more matches for this sub-goal it will backtrack again;
retrying every sub-goal before failing the parent goal.
• A call can match any clause head.
• A redo ignores old matches.
A new instantiation
a:- b, c, d, e, f, g, h, I, j .
a:- b, c, d, e, f, g, h, I, j .
Succeed
Fail
Redo a:- b, c, d, e, f, g, h, I, j .
Backtrack
Cut !
• If we want to restrict backtracking we can control which
sub-goals can be redone using the cut = !.
• We use it as a goal within the body of clause.
• It succeeds when called, but fails the parent goal (the
goal that matched the head of the clause containing the cut) when an
attempt is made to redo it on backtracking.
• It commits to the choices made so far in the predicate.
– unlimited backtracking can occur before and after the cut but
no backtracking can go through it.
immediate fail
a:- b, c, d, e, !, f, g, h, I, j . a:- b, c, d, e, !, f, g, h, I, j .
Failing the parent goal
a:- b, c, d, e, !, f, g, h, I, j . a:- b, c, d, e, !, f, g, h, I, j .
a:- k. Treated as if
a:- k. This clause and
these choices
a:- m . don’t exist a:- m . committed to
3 2 Call: 3=<2 ?
3 2 Fail: 3=<2 ?
4 2 Call: 6=<2 ?
4 2 Fail: 6=<2 ?
1 1 Fail: f(2,_487) ?
no
Green Cuts !
f(X,0):- X < 3, !. |?- trace, f(2,N).
f(X,1):- 3 =< X, X < 6, !. 1 1 Call: f(2,_487) ?
f(X,2):- 6 =< X.
2 2 Call: 2<3 ?
2 2 Exit: 2<3 ? ?
If you reach this point don’t
1 1 Exit: f(2,0) ?
bother trying any other clause.
N = 0 ? ;
• Notice that the answer is still thenosame, with or without the cut.
– This is because the cut does not alter the logical behaviour of the
program.
– It only alters the procedural behaviour: specifying which goals get
checked when.
• This is called a green cut. It is the correct usage of a cut.
• Be careful to ensure that your clauses are actually mutually
exclusive when using green cuts!
Red Cuts !
| ?- f(7,N).
f(X,0):- X < 3, !.
f(X,1):- 3 =< X, X < 6, !. 1 1 Call: f(7,_475) ?
f(X,2):- 6 =< X. 2 2 Call: 7<3
? 2 2 Fail:
7<3 ? 3 2
Call: 3=<7 ? 3
2 Exit: 3=<7 ? 4
2 Call: 7<6 ?
Redundant? 4 2 Fail: 7<6 ?
5 2 Call: 6=<7 ?
5 2 Exit: 6=<7 ?
1 1 Exit:
f(7,2) ?
N = 2 ?
• Because the clauses are mutually
yes
exclusive and ordered
we know that once the clause above fails certain
conditions must hold.
• We might want to make our code more efficient by
removing superfluous tests.
Red Cuts !
f(X,0):- X < 3, !. f(X,0):- X < 3.
f(X,1):- X < 6, !. f(X,1):- X < 6.
f(X,2). f(X,2).
| ?- f(7,N). | ?- f(1,Y).
1 1 Call: f(7,_475) ? 1 1 Call: f(1,_475) ?
2 2 Call: 7<3 ? 2 2 Call: 1<3
2 2 Fail: ? 2 2 Exit:
7<3 ? 3 2 Call: 1<3 ? ? 1 1
7<6 ? 3 2 Exit: f(1,0) ? Y = 0 ? ;
Fail: 7<6 ? 1 1 1 1
Exit: f(7,2) ? Redo: f(1,0) ? 3
N = 2 ? 2 Call: 1<6 ? 3
yes 2 Exit: 1<6 ? ?
1 1 Exit: f(1,1) ?
Y = 1 ? ;
1 1 Redo: f(1,1) ?
1 1 Exit: f(1,2)
? Y = 2 ?
yes
Using the cut
• Red cuts change the logical behaviour of a predicate.
• TRY NOT TO USE RED CUTS!
• Red cuts make your code hard to read and are dependent
on the specific ordering of clauses (which may change
once you start writing to the database).
• If you want to improve the efficiency of a program use
green cuts to control backtracking.
• Do not use cuts in place of tests.
To ensure a logic friendly cut either:
p(X):- test1(X), !, call1(X). p(1,X):- !, call1(X).
p(X):- test2(X), !, call2(X). p(2,X):- !, call2(X).
p(X):- testN(X), !, callN(X). p(3,X):- !, callN(X).
likes(mary,X):-
snake(X), !, fail. We need to combine a cut with
the fail to stop the redundant
likes(mary,X):- call to the second clause on
\+ snake(X), backtracking.
animal(X).
Cut – fail: why?
• However, using a cut-fail can make your code hard to
follow.
• It is generally clearer and easier to define the conditions
under which a fact is true rather than when it is false.
likes(mary,X):-
\+ snake(X), This is sufficient to represent the fact.
animal(X).