Prolog - Unification - Backtracking - Recursion - Lists - Cut

Download as ppt, pdf, or txt
Download as ppt, pdf, or txt
You are on page 1of 78

PROLOG

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

– To distinguish between clauses of a predicate definition


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

The bigger number is 5 Reaches full-stop


yes = clause succeeds
|?-
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):-
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‘).

Numbers are the same


yes
Clauses should be ordered according to specificity
Most specific at top Universally applicable at bottom
Reporting Answers
|?- bigger(5,4).  Question is asked
The bigger number is 5  Answer is written to terminal
yes  Succeeds but answer is lost

• This is fine for checking what the code is doing but not for using
the proof.

|?- bigger(6,4), bigger(Answer,5).


Instantiation error!

• To report back answers we need to


– put an uninstantiated variable in the query,
– instantiate the answer to that variable when the query succeeds,
– pass the variable all the way back to the query.
Passing Back Answers
• To report back answers we need to
1. put an uninstantiated variable in the query,

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.

2. instantiate the answer to that variable when the query


succeeds,
3. pass the variable all the way back to the query.
Head Unification
• To report back answers we need to
1. put an uninstantiated variable in the query,
1
| ?- bigger(6,4,Answer),bigger(Answer,5,New_answer).
2 3

bigger(X,Y,X):- X>Y.
bigger(X,Y,Y):- X=<Y.

Or, do steps 2 and 3 in one step by naming the variable in


the head of the clause the same as the correct answer.
= head unification
The central ideas of Prolog
• SUCCESS/FAILURE
– any computation can “succeed'' or “fail'', and this is used as a
‘test‘ mechanism.
• MATCHING
– any two data items can be compared for similarity, and values
can be bound to variables in order to allow a match to succeed .
• SEARCHING
– the whole activity of the Prolog system is to search through
various options to find a combination that succeeds.
• Main search tools are backtracking and recursion

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

• These subgoals can match:


– facts,
– other rules, or
– the same rule = a recursive call
Likes program
1) drinks(ali,tea).
2) likes(ali,coffee).
3) likes(hasan,coffee).

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

3rd solution = “Ali likes Ali because Ali likes coffee.”


Recursion using Trees (3)
• 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
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/2 is a likes(coffee,X) likes(coffee,X)


left recursive clause.
likes(coffee,X2)
likes(X,X2)

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.

• Relationships may be recursive


e.g. “X is my ancestor if X is my Ancestor’s ancestor.”

• Data is represented recursively and best processed


iteratively.
– Grammatical structures can contain themselves
– E.g. NP  (Det) N (PP), PP  P (NP)
– Ordered data: each element requires the same processing

• Allows Prolog to perform complex search of a


problem space without any dedicated algorithms.
Prolog Data Objects (Terms)
Simple objects Structured Objects

Constants Variables Structures Lists


X date(4,10,04) []
A_var person(bob,48) [a,b,g]
Atoms Integers _Var [[a],[b]]
-6 [bit(a,d),a,’Bob’]
987

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

• The components can be of any type: atoms,


integers, variables, or structures.
• As functors are treated as data objects just like
constants they can be unified with variables
|?- X = date(04,10,04).
X = date(04,10,04)?
yes
Structure unification
• 2 structures will unify if
– the functors are the same,
– they have the same number of components,
– and all the components unify.
| ?- person(Nm,london,Age) = person(bob,london,48).
Nm = bob,
Age = 48?
yes
| ?- person(Someone,_,45) = person(harry,dundee,45).
Someone = harry ?
yes

• (A plain underscore ‘_’ is not bound to any value. By using it you


are telling Prolog to ignore this argument and not report it.)
Structure unification (2)
• A structure may also have another structure as a
component.
|?-addr(flat(4),street(‘Home Str.’),postcode(eh8_9lw))
= addr(flat(Z),Yy,postcode(Xxx)).
Z = 4,
Remember to
Yy = street(‘Home Str.’),
close brackets!
Xxx = eh8_9lw ?
yes Reported variables are
ordered according to
• Unification of nested structures number of characters
works
recursively: in the variable name.

– first it unifies the entire structure,


– then it tries to unify the nested structures.
Structures = facts?
• The syntax of structures and facts is identical but:
– Structures are not facts as they are not stored in the
database as being true (followed by a period ‘.’);
– Structures are generally just used to group data;
– Functors do not have to match predicate names.

• However predicates can be stored as structures


command(X):-
X. By instantiating a variable with a structure which
is also a predicate you can pass commands.
| ?- X = write(‘Passing a command’), command(X).
Passing a command
X = write('Passing a command') ?
yes
Lists
• A collection of ordered data.
• Has zero or more elements enclosed by square
brackets (‘[ ]’) and separated by commas (‘,’).
[a]  a list with one element
[]  an empty list
1 2 3
1 2

[34,tom,[2,3]]  a list with 3 elements where the


3rd element is a list of 2 elements.
• Like any object, a list can be unified with a variable
|?- [Any, list, ‘of elements’] = X.
X = [Any, list, ‘of elements’]?
yes
List Unification
• Two lists unify if they are the same length and all their
elements unify.
|?-[a,B,c,D]=[A,b,C,d]. |?-[(a+X),(Y+b)]=[(W+c),(d+b)].
A = a, W = a,
B = b, X = c,
C = c, Y = d?
D = d ? yes
yes

|?- [[X,a]]=[b,Y]. |?-[[a],[B,c],[]]=[X,[b,c],Y].


no B = b,
X = [a],
Y = [] ?
Length 1 Length 2 yes
Definition of a List
• Lists are recursively defined structures.
“An empty list, [], is a list.
A structure of the form [X, …] is a list if X is a term and
[…] is a list, possibly empty.”
• This recursiveness is made explicit by the bar notation
– [Head|Tail] (‘|’ = bottom left PC keyboard character)

• Head must unify with a single term.


• Tail unifies with a list of any length, including an empty
list, [].
– the bar notation turns everything after the Head into a
list and unifies it with Tail.
Head and Tail
|?-[a,b,c,d]=[Head|Tail]. |?-[a,b,c,d]=[X|[Y|Z]].
Head = a, X = a,
Tail = [b,c,d]? Y = b,
yes Z = [c,d];
yes

|?-[a] = [H|T]. |?-[a,b,c]=[W|[X|[Y|Z]]].


H = a, W = a,
T = []; X = b,
yes Y = c,
Z = []? yes

|?-[] = [H|T]. |?-[a|[b|[c|[]]]]= List.


no List = [a,b,c]?
yes
Summary
• Prolog’s proof strategy can be represented using
AND/OR trees.
• Tree representations allow us trace Prolog’s search
for multiple matches to a query.
• They also highlight the strengths and weaknesses of
recursion (e.g. economical code vs. infinite looping).
• Recursive data structures can be represented as
structures or lists.
• Structures can be unified with variables then used
as commands.
• Lists can store ordered data and allow its sequential
processing through recursion.
Lists - Identifying a list
• Last lecture we introduced lists: [a,[],green(bob)]
• We said that lists are recursively defined structures:
“An empty list, [ ], is a list.
A structure of the form [X, …] is a list if X is a term and
[…] is a list, possibly empty.”

• This can be tested using the Head and Tail notation,


[H|T], in a recursive rule.
is_a_list([]).  A term is a list if it is an empty list.
is_a_list([_|T]):-  A term is a list if it has two
is_a_list(T). elements and the second is a list.
Base and Recursive Cases
• A recursive definition, whether in prolog or some other
language (including English!) needs two things.

• A definition of when the recursion terminates.


– Without this the recursion would never stop!
– This is called the base case: is_a_list([]).
– Almost always comes before recursive clause

• A definition of how we can define the problem in terms of


a similar, smaller problem.
– This is called the recursive case: is_a_list([_|T]):-
is_a_list(T).

• There might be more than one base or recursive case.


Focussed Recursion
• To ensure that the predicate terminates, the recursive
case must move the problem closer to a solution.
– If it doesn’t it will loop infinitely.
• With list processing this means stripping away the Head
of a list and recursing on the Tail.
Head is replaced with
is_a_list([_|T]):- an underscore as we
is_a_list(T). don’t want to use it.

• The same focussing has to occur when recursing to find


a property or fact.
is_older(Ancestor,Person):-
Doesn’t focus is_older(Someone,Person),
is_older(Ancestor,Someone).
Focussed Recursion (2)
Given this program: • A query looking for all
solutions will loop.
parent(tom,jim). |?-is_older(X,Y).
parent(mary,tom).

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*

It loops because the recursive clause does not focus


the search it just splits it. If the recursive is_older/2
doesn’t find a parent it just keeps recursing on itself
Focussed Recursion (3)
The correct program: • Can generate all valid
matches without looping.
parent(tom,jim). |?-is_older(X,Y).
parent(mary,tom).

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

To make the problem space smaller we need to check


that Young has a parent before recursion. This way we
are not looking for something that isn’t there.
List Processing Predicates: Member/2
• Member/2 is possibly the most used user-defined
predicate (i.e. you have to define it every time you want to
use it!)
• It checks to see if a term is an element of a list.
– it returns yes if it is
– and fails if it isn’t.
| ?- member(c,[a,b,c,d]).
yes
• It 1st checks if the Head of the list
unifies with the first argument.
member(H,[H|_]). • If yes then succeed.
• If no then fail first clause.
member(H,[_|T]):-
member(H,T). • The 2nd clause ignores the head of
the list (which we know doesn’t
match) and recurses on the Tail.
List Processing Predicates: Member/2
|?- member(ringo,[john,paul,ringo,george]).

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.

– The tracer also indicates the level in the search tree


from which a goal is being called.
• The number next to the goal indicates the level in the tree (top
level being 0).
• The leftmost number is the number assigned to the goal
(every new goal is given a new number).

• To turn off the tracer type notrace.


Tracing Member/2
| ?- trace.
| ?- member(ringo,[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]) ?
3 3 Exit: member(ringo,[ringo,george]) ?
2 2 Exit: member(ringo,[paul,ringo,george]) ?
1 1 Exit: member(ringo,[john,paul,ringo,george]) ?
yes

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

• These all have different uses, effect the order of the


accumulated data differently, and require different
degrees of processing.
Compute lower result first.
• We want to define a predicate, length/2, which takes a list
as its first argument and returns a number as the second
argument that is equal to the length of the list.
• We can use recursion to move through the list element by
element and is/2 to count as it goes.

listlength([_|T],N1):-
listlength(T,N),
N1 is N+1.

• To make a counter we need to initialise it at a value i.e. zero.


• As the counter increases during backtracking it needs to be
initialised in the base case.

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([],Acc,Acc). Finalise result.

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

• This ensures that the accumulator is initialise correctly


and that the user doesn’t have to understand the
workings of your code to use it.
Combining lists
• A common use of an accumulator is to construct lists.
• If we want to make a new list out of the combined
elements of two lists we can’t just make one list the Head
of a new list and the other the tail as:
| ?- L1=[a,b], L2=[c,d], Z=[L1|L2].
L1 = [a,b], L2 = [c,d], Z = [[a,b],c,d] ?

• We need to take each element from L1 and add them to


L2 one at a time.
• There are two ways we can do this
– during recursion, or
– backtracking.
Constructing a list during Recursion
|?- pred([a,b],[c,d],Out). Desired behaviour
Out = [a,b,c,d].

• To add L1 to L2 during recursion we can use the bar


notation to decompose L1 and add the Head to L2.
pred([H|T],L2,Out):- pred(T,[H|
L2],Out).
Accumulator
• We need to have an extra variable (Out) which can be
used to pass back the new list once L1 is empty.
pred([],L2,L2).  base case: when L1 is empty make
the new list equal to the Output list.
• The base case must go before the recursive case.
Constructing a list during Recursion (2)
Always the
| ?- pred([a,b],[c,d],Out). same variable
1 1
Call: pred([a,b],[c,d],_515) ? 2
2 Call: pred([b],[a,c,d],_515) ? 3
3 Call: pred([],[b,a,c,d],_515) ? 3
3 Exit: pred([],[b,a,c,d],[b,a,c,d]) ?
2 2 Exit: pred([b],[a,c,d],
[b,a,c,d]) ? 1 1 Exit:
pred([a,b],[c,d],[b,a,c,d]) ? Out =
[b,a,c,d] ?
yes If you construct a list through
recursion (on the way down)
and then pass the answer
pred([],L2,L2). back the elements will be in
pred([H|T],L2,Out):- reverse order.
pred(T,[H|L2],Out).
reverse/3
| ?- pred([a,b],[c,d],Out).
1 1
Call: pred([a,b],[c,d],_515) ? 2
2 Call: pred([b],[a,c,d],_515) ? 3
3 Call: pred([],[b,a,c,d],_515) ? 3
3 Exit: pred([],[b,a,c,d],[b,a,c,d]) ?
2 2 Exit: pred([b],[a,c,d],
[b,a,c,d]) ? 1 1 Exit:
pred([a,b],[c,d],[b,a,c,d]) ? Out =
[b,a,c,d] ?
yes If you construct a list through
recursion (on the way down)
and then pass the answer
reverse([],L2,L2). back the elements will be in
reverse([H|T],L2,Out):- reverse order.
reverse(T,[H|L2],Out).
Constructing a list in backtracking

• To maintain the order of list elements we need to


construct the list on the way out of the program, i.e.
through backtracking.
• Use the same bar deconstruction as before but add the
head element of L1 to Out in the Head of the clause.
pred([H|T],L2,[H|Out]):-  Head is not added
pred(T,L2,Out). until backtracking.

• Now when we reach the base case we make L2 the


foundation for the new Out list and add our L1 elements
to it during backtracking.
pred([],L2,L2).  base case: when L1 is empty make
the new list equal to the Output list.
append/3 Variable changes
at every Call.
| ?- pred2([a,b],[c,d],Out).
1 1
Call: pred2([a,b],[c,d],_515) ? 2 2
Call: pred2([b],[c,d],_1131) ? 3 3
Call: pred2([],[c,d],_1702) ? 3 3
Exit: pred2([],[c,d],[c,d]) ? 2 2
Exit: pred2([b],[c,d],[b,c,d]) ? 1
1 Exit: pred2([a,b],[c,d],[a,b,c,d]) ? Out =
[a,b,c,d] ?
yes
append([],L2,L2).
append([H|T],L2,[H|Rest]):-
append(T,L2,Rest).

* append/3 is another very common user-defined list


processing predicate.
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.
| ?- append(X,Y,[a,b,c,d]).
X = [], Y = [a,b,c,d] ? ;
append([],L2,L2).
X = [a], Y = [b,c,d] ? ; append([H|T],L2,[H|Rest]):-
X = [a,b], Y = [c,d] ? ; append(T,L2,Rest).
X = [a,b,c], Y = [d] ? ;
X = [a,b,c,d], Y = [] ? ;
no

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

X is Y succeeds if the arithmetic value of expression Y


matches the value of term X.
X =:= Y succeeds if the arithmetic value of two expressions
X and Y match.
X =\= Y succeeds if the arithmetic value of two expressions
X and Y DO NOT match.

X == Y succeeds if the two terms have literal equality = are


structurally identical and all their components have
the same name.
X \== Y succeeds if the two terms are NOT literally identical.

\+ Goal succeeds if Goal does not true


Clearing up equality (2)
| ?- 3+4 = 4+3. | ?- 3+4 =:= 4+3.
no % treats them as terms yes % calculates both values
| ?- 3+4 =\= 4+3.
| ?- 3+4 = 3+4.
no
yes
| ?- 3+4 == 4+3.
| ?- X = 4+3.
no
X = 4+3 ?
| ?- 3+4 \== 4+3.
yes
yes
| ?- X is 4+3. | ?- 3+X = 3+4.
X = 7 ? X = 4 ? yes
yes | ?- 3+X == 3+4.
| ?- 3+4 is 4+3. no
no % left arg. has to be a term | ?- \+ 3+4 == 4+3.
yes
Processing in Prolog
To call the goal G:
1. Find first clause head that matches G:
1. bind all variables accordingly,
2. call goals in body in order;
3. if all succeed, G succeeds (and exits).
2. else try next clause down;
3. if no next clause, fail the goal G.

When a goal fails:


redo the most recent successful goal

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.

CALL EXIT CALL


N=1
fact(b,N) fact(c,1)
REDO FAIL
Redo-ing a Goal (3)
fact(b,1).
fact(b,2).
a :- fact(b,N), fact(c,N).

|?- 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

• The cut succeeds when it is called and commits the


system to all choices made between the time the parent
goal was invoked and the cut.
• This includes committing to the clause containing the cut.
= the goal can only succeed if this clause succeeds.
• When an attempt is made to backtrack through the cut
– the clause is immediately failed, and
– no alternative clauses are tried.
Mutually Exclusive Clauses
• We should only use a cut if the clauses are mutually
exclusive (if one succeeds the others won’t).
• If the clauses are mutually exclusive then we don’t want
Prolog to try the other clauses when the first fails
= redundant processing.

• By including a cut in the body of a clause we are


committing to that clause.
– Placing a cut at the start of the body commits to the clause
as soon as head unification succeeds.
a(1,X):- !, b(X), c(X).
– Placing a cut somewhere within the body (even at the end)
states that we cannot commit to the clause until certain
sub-goals have been satisfied.
a(_,X):- b(X), c(X), !.
Mutually Exclusive Clauses (2)
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 ? ?
1 1 Exit: f(2,0) ?
N = 0 ? ;
1 1 Redo: f(2,0) ?

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

testI predicates are mutually exclusive. The mutually exclusive tests


are in the head of the clause.
Cut - fail
• As well as specifying conditions under which a goal can
succeed sometimes we also want to specify when it
should fail.
• We can use the built-in predicate fail in combination
with a cut to achieve this: “ !, fail. “
= if you reach this point, fail regardless of other clauses.
• e.g. If we want to represent the fact that ‘Mary likes all
animals except snakes’.

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

• However, sometimes it can be much simpler to specify


when something is false rather than true so cut-fail can
make your code more efficient.
• As with all cuts; be careful how you use it.
Summary
• Clearing up equality: =, is, =:=, =\=, ==, \==, \+
• REDO vs. CALL
• Controlling backtracking: the cut !
– Efficiency: avoids needless REDO-ing which cannot
succeed.
– Simpler programs: conditions for choosing clauses can be
simpler.
– Robust predicates: definitions behave properly when forced
to REDO.
• Green cut = cut doesn’t change the predicate logic = good
• Red cut = without the cut the logic is different = bad
• Cut – fail: when it is easier to prove something is false than true.

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy