Programming in Prolog (continued)
List "functions"
See list.P
As in Scheme, lists are a workhorse data structure in Prolog. And, as in Scheme, lists have special supporting syntax:
-
The empty list can be written
nil
or[]
. -
A non-empty list can be written
cons(H,T)
or[H|T]
. -
More generally, a non-empty list (e.g., with at least two elements) can be written
cons(H1,cons(H2,T))
or[H1,H2|T]
. -
A fixed length list (e.g., of three elements) can be written
cons(H1,cons(H2,cons(H3,nil)))
or[H1,H2,H3|nil]
or[H1,H2,H3|[]]
or[H1,H2,H3]
.
Typically, a Prolog programmer uses the square-bracket syntax for lists.
Here are two simple predicates:
head([H|T], H). ;; equivalently: head(cons(H,T), H). tail([H|T], T). ;; equivalently: tail(cons(H,T), T).
The head(L,X)
predicate succeeds when X
is the head of the list L
and the
tail(L,X)
predicate succeeds when X
is the head of the list L
. Note that
these predicates are written with a cons
structure for the first argument;
this is similar to SML’s pattern matching. These predicates are similar to the
car
and cdr
functions of Scheme, but with some subtle differences:
?- head([a,b,c],a). yes ?- head([a,b,c],b). no ?- tail([a,b,c],[b,c]). yes ?- tail([a,b,c],[]). no ?- head([a,b,c],X). (1) X = a; no ?- tail([a,b,c],X). (1) X = [b, c]; no ?- head([],X). (2) no ?- tail([],X). (2) no ?- head(L,a). (3) L = [a|_T406]; no ?- tail(L,[b,c]). (4) L = [_H507, b, c]; no
1 | When the list is ground (i.e., known), then head and tail behave like
"functions", treating the first argument as an input and the second argument as
an output. |
2 | In Scheme, it is a runtime error to apply car or cdr to the empty list.
In Prolog, using the head or tail predicate does not cause a runtime
error. Rather, the goal is simply unsatisfiable. |
3 | We can run head "backwards": find a list L such that a is the head.
Prolog solves the goal with a substitution L = [a|_T406] , where _T406 is a
fresh variable generated during proof search. The term [a|_T406] (equivalent
to cons(a,_T406) ) simply means that the second component of the cons
structure is arbitrary --- could be instantiated with any term |
4 | Similarly, we can run tail "backwards": find a list L such that [b,c]
is the tail. Prolog solves the goal with a substitution L = [_H507, b, c] ,
where _H507 is a fresh variable generated during proof search. The term
[_H507, b, c] (equivalent to cons(_H507,cons(b,cons(c,nil))) simply means
that the first component of the outermost cons structure is arbitrary ---
could be instantiated with any tern. |
Here are two simple recursive predicates on lists:
last([X], X). last([H|T], X) :- last(T, X). list([]). list([H|T]) :- list(T).
The last(L,X)
predicate is satisfied when X
is the last element of the list
L
. Like head
and tail
, it can be run forward and "backward":
?- last([a,b,c],a). no ?- last([a,b,c],c). yes ?- last([a,b,c],X). X = c; no ?- last(L,c). (1) L = [c]; L = [_H1643, c]; L = [_H1643, _H1653, c]; L = [_H1643, _H1653, _H1663, c]; L = [_H1643, _H1653, _H1663, _H1673, c]; L = [_H1643, _H1653, _H1663, _H1673, _H1683, c] yes
1 | Note that there are an infinite number of lists that have c as the last
element. Prolog’s proof search effectively enumerates the "spine" of each such
list, leaving the elements (other than the last) arbitrary. |
The list(X)
predicate is satisfied when X
is a proper list (either the empty
list or a cons
whose second argument is a proper list). It isn’t very
interesting when the input is ground, but can be used to generate lists of
increasing length:
?- list([]). yes ?- list([a,b,c]). yes ?- list(a). no ?- list(1). no ?- list(cons(a,b)). no ?- list(pair(a,b)). no ?- list(X). X = []; X = [_H2198]; X = [_H2198, _H2200]; X = [_H2198, _H2200, _H2202]; X = [_H2198, _H2200, _H2202, _H2204]; X = [_H2198, _H2200, _H2202, _H2204, _H2206] yes
Calculating the length of a list is similar in spirit to the recursive fact
predicate from the previous lecture notes:
length([],0). length([H|T],N) :- length(T,M), N is M + 1.
The length
predicate works well as a "function", treating the first argument
as an input and the second argument as an output, but the first argument need
not be a ground term:
?- length([], N). N = 0; no ?- length([a,b,c], N). N = 3; no ?- length([A,B,C], N). A = _H32 B = _H36 C = _H40 N = 3; no ?- length([H|T],N). H = _H2220 T = [] N = 1; H = _H2220 T = [_H2234] N = 2; H = _H2220 T = [_H2234, _H2248] N = 3; H = _H2220 T = [_H2234, _H2248, _H2262] N = 4; H = _H2220 T = [_H2234, _H2248, _H2262, _H2276] N = 5 yes
However, our simple length
predicate doesn’t work well "backwards":
?- length(L, 3). L = [_H48, _H52, _H56]; ^C
This query enters an infinite loop. To see why, note that after unifying
length(L, 3)
with the second clause, the state of the proof search will be:
-
{L ↦ [H1|T1]}
;;length([H1|T1],3)
(at 2) ;;length(T1,N1)
,3 is N1 + 1
The next unsatisfied subgoal is length(T1,N1)
; from the above, we can see that
such a goal succeeds first with {T1 ↦ [], N1 ↦ 0}
(but
3 is 0 + 1
will fail), then with {T1 ↦ [_H20], N1 ↦ 1}
(but
3 is 1 + 1
will fail), then with {T1 ↦ [_H20,_H22], N1 ↦ 2}
for
which 3 is 2 + 1
also succeeds. But, if the user requests additional
solutions, then the length(T1,N1)
will next succeed with {T1 ↦
[_H20,_H22,_H24], N1 ↦ 3}
(but 3 is 3 + 1
will fail), then with
{T1 ↦ [_H20,_H22,_H24,_H26], N1 ↦ 4}
(but 3 is 4 + 1
will fail),
then with …. The length(T1,N1)
subgoal will generate an infinite number of
solutions, but the system cannot "know" that none of them will allow the
subsequent 3 is N1 + 1
goal to succeed.
One way to work around this is to give a different predicate that is specifically designed to "solve for the list when given the length":
ofLength(0, []). ofLength(N, [H|T]) :- N >= 1, M is N - 1, ofLength(M, T).
Now, the recursion is driven by the length (rather than the structure of the list):
?- ofLength(3,L). L = [_H20, _H38, _H56]; no
Using some features not available in uProlog (in particular, a primitive
predicate that succeeds when its argument is a ground term), it is possible in
full Prolog to implement a length
predicate that is able to properly "solve
for the list when given the length".
The member(X, L)
predicate succeeds when X
is a member of the list L
:
member(X,[X|T]). member(X,[H|T]) :- member(X,T).
Note in the axiom, the same variable X
is used in both the first argument and
the second argument. Such reuse of a variable is not allowed in SML pattern
matching, but it follows naturally from Prolog’s unification-based execution.
In this case, it ensures that the first argument and the head of the list are
the same term. Like last
and list
, it is able to enumerate lists with a
given element:
?- member(b,[a,b,c]). yes ?- member(d,[a,b,c]). no ?- member(X,[a,b,c]). X = a; X = b; X = c; no ?- member(b,L). L = [b|_T1328]; L = [_H1331, b|_T1356]; L = [_H1331, _H1359, b|_T1384]; L = [_H1331, _H1359, _H1387, b|_T1412]; L = [_H1331, _H1359, _H1387, _H1415, b|_T1440] yes
The append(XS,YS,ZS)
predicate succeeds when ZS
is the list formed by
appending XS
and YS
:
append([], YS, YS). append([X|XT], YS, [X|ZT]) :- append(XT, YS, ZT).
The base case is when the list XS
is empty, in which case the list ZS
is
equal to YS
; this is expressed by the axiom append([], YS, YS).
. The
recursive case is when the list XS
is non-empty (i.e., [X|XT]
), in which
case the list ZS
is equal to X
cons
-ed on the result of appending XT
and YS
. Compare this implementation of the append
predicate to the append
function in Scheme or SML.
No suprise, the append
predicate works well as a "function", treating the
first two arguments as inputs and the third argument as an output:
?- append([a,b,c],[d,e,f],ZS). ZS = [a, b, c, d, e, f]; no
But, the append
predicate also "works" to "solve for XS
(or YS
), given
ZS
and YS
(or XS
)":
?- append(XS,[d,e,f],[a,b,c,d,e,f]). XS = [a, b, c]; no ?- append([a,b,c],YS,[a,b,c,d,e,f]). YS = [d, e, f]; no
In each of the previous examples, when two of XS
, YS
, and ZS
are known and
one is unknown, there is at most one solution. However, when one of XS
, YS
,
and ZS
is known and two are unknown, there can be multiple solutions. In
particular, when ZS
is known but XS
and YS
are unknown, then the append
predicate can generate all "splits" of a list:
?- append(XS,YS,[a,b,c,d,e,f]). XS = [] YS = [a, b, c, d, e, f]; XS = [a] YS = [b, c, d, e, f]; XS = [a, b] YS = [c, d, e, f]; XS = [a, b, c] YS = [d, e, f]; XS = [a, b, c, d] YS = [e, f]; XS = [a, b, c, d, e] YS = [f]; XS = [a, b, c, d, e, f] YS = []; no ?- append([a,b,c],YS,ZS). YS = _YS3816 ZS = [a, b, c|_YS3816]; no ?- append(XS,[d,e,f],ZS). XS = [] ZS = [d, e, f]; XS = [_X4167] ZS = [_X4167, d, e, f]; XS = [_X4167, _X4190] ZS = [_X4167, _X4190, d, e, f]; XS = [_X4167, _X4190, _X4213] ZS = [_X4167, _X4190, _X4213, d, e, f] yes
Note that for the query append([a,b,c],YS,ZS).
one solution (essentially ZS =
[a,b,c|YS]
) represents an infinite number of possible instantiations.
This behavior of append
can be put to good use in defining other list
predicates. For example, the member
predicate can be defined in terms of
append
:
member_via_append(Y,ZS) :- append(XS, [Y|YT], ZS).
We can interpret this as asserting: “Y` is a member of ZS
if the list ZS
can be split into two lists XS
and YS
such that Y
is the head of `YS”.
The prefix(XS,YS)
predicate succeeds when YS
is a prefix of XS
. We can
define prefix
directly (using recursion) or indirectly (using append
):
prefix_rec(XS, []). prefix_rec([Z|XS], [Z|YS]) :- prefix_rec(XS, YS). prefix_via_append(XS, YS) :- append(YS, _, XS). prefix(XS, YS) :- prefix_rec(XS, YS).
The suffix(XS,YS)
predicate succeeds when YS
is a suffix of XS
. We can
define suffix
directly (using recursion) or indirectly (using append
):
suffix_rec(L, L). suffix_rec([_|XS], YS) :- suffix_rec(XS, YS). suffix_via_append(XS, YS) :- append(_, YS, XS). suffix(XS, YS) :- suffix_rec(XS, YS).
Finally, the sublist(XS,YS)
predicate succeeds when YS
is a sublist of XS
. We can define sublist
directly (using recursion and prefix
) or indirectly (using append
twice or using prefix
and suffix
):
sublist_rec(XS, YS) :- prefix(XS, YS). sublist_rec([_|XS], YS) :- sublist_rec(XS, YS). sublist_via_append(XS, YS) :- append(_, ZS, XS), append(ZS, _, YS). sublist_via_prefix_suffix(XS, YS) :- suffix(XS, ZS), prefix(ZS, YS). sublist(XS, YS) :- sublist_rec(XS, YS).
As we saw in Scheme, we can reverse a list in two ways:
-
One implementation uses append to copy the head of the input list to the end of the reverse of the tail of the input list.
-
Another implementation uses an accumulating parameter to copy each element of the input list in turn onto the front of the accumulating list.
We can express both of these algorithms in Prolog:
reverseA([], []). reverseA([H|T], LR) :- reverseA(T,TR), append(TR,[H],LR). revapp([], L, L). revapp([H|T], L2, L3) :- revapp(T, [H|L2], L3). reverseB(L, LR) :- revapp(L, [], LR). reverse(L, LR) :- reverseB(L, LR).
As in Scheme, the second implementation is more efficient. (The
revapp(L1,L2,L3)
predicate succeeds when ZS
is the list formed by appending
the reverse of XS
and YS
.)
Both implementations work well as a "function", treating the first argument as an input and the second argument as an output:
?- reverseA([a,b,c], L). L = [c, b, a]; no ?- reverseB([a,b,c], L). L = [c, b, a]; no
And when both arguments are variables, the implementations can generate pairs of a list and its reversal:
?- reverseA(XS,YS). XS = [] YS = []; XS = [_H36] YS = [_H36]; XS = [_H36, _X253] YS = [_X253, _H36]; XS = [_H36, _X654, _X626] YS = [_X626, _X654, _H36]; XS = [_H36, _X1368, _X1340, _X1312] YS = [_X1312, _X1340, _X1368, _H36] yes ?- reverseB(XS,YS). XS = [] YS = []; XS = [_H1474] YS = [_H1474]; XS = [_H1474, _H1515] YS = [_H1515, _H1474]; XS = [_H1474, _H1515, _H1556] YS = [_H1556, _H1515, _H1474]; XS = [_H1474, _H1515, _H1556, _H1597] YS = [_H1597, _H1556, _H1515, _H1474] yes
Unfortunately, neither implementation works well "backwards":
?- reverseA(L,[c,b,a]). L = [a, b, c]; ^C ?- reverseB(L,[c,b,a]). L = [a, b, c]; ^C
Both implementations enter an infinite loop when backtracking after the first (and only) solution.
Using reverse
, we can give a trivial definition of palindrome
:
palindrome(L) :- reverse(L, L).
This predicate concisely captures what a palindrome is, rather than specifying how to check if something is a palindrome.
The zip(XS,YS,ZS)
predicate succeeds when the list ZS
is formed by pairing
corresponding elements of the lists XS
and YS
. Here is one definition:
zip([], YS, []). zip(XS, [], XS). zip([X|XT], [Y|YT], [pair(X,Y)|ZT]) :- zip(XT,YT,ZT).
Note the use of the structure pair(X,Y)
for a data structure that pairs the
elements X
and Y
. The following transcript demonstrates that zip
can be
executed both "forwards" and "backwards":
?- zip([a,b,c,d],[1,2,3],ZS). ZS = [pair(a, 1), pair(b, 2), pair(c, 3)]; no ?- zip(XS,YS,[pair(a, 1), pair(b, 2), pair(c, 3)]). XS = [a, b, c] YS = [1, 2, 3|_YS588]; XS = [a, b, c|_XS589] YS = [1, 2, 3]; no
Thus, the predicate zip
captures the behavior of both the function zip
and
the function unzip
from SML!
But, the following is curious:
?- zip([a,b,c],[1,2,3],ZS). ZS = [pair(a, 1), pair(b, 2), pair(c, 3)]; ZS = [pair(a, 1), pair(b, 2), pair(c, 3)]; no
Why are there two (identical) solutions? Essentially, because the two base
cases are not mutually exclusive. In particular, zip([],[],[])
can be proven
two ways: either by using the axiom zip([], YS, []).
or by using the axiom
zip(XS, [], XS).
. Each successful proof search yields a solution
(corresponding to a "different" proof), although different proofs may not yield
different substitutions. We can emphasize this point by changing the predicate
to include a token indicating which base case was used:
zip([], YS, [], fst_base_case). zip(XS, [], XS, snd_base_case). zip([X|XT], [Y|YT], [pair(X,Y)|ZT], BC) :- zip(XT,YT,ZT,BC).
Now, we can observe the difference in the proofs:
?- zip([a,b,c],[1,2,3],ZS, BC). ZS = [pair(a, 1), pair(b, 2), pair(c, 3)] BC = fst_base_case; ZS = [pair(a, 1), pair(b, 2), pair(c, 3)]; BC = snd_base_case; no
We can solve the problem by making the two base cases mutually exclusive:
zip([], YS, []). zip([X|XT], [], []). zip([X|XT], [Y|YT], [pair(X,Y)|ZT]) :- zip(XT,YT,ZT).
?- zip([a,b,c],[1,2,3],ZS). ZS = [pair(a, 1), pair(b, 2), pair(c, 3)]; no
In Programming03: Scheme Programming, a (bonus) problem was to implement a
function permutation?
that takes two lists and returns a boolean indicating
whether the first argument list is a permutation of the second argument list.
This can be a challenging problem. Also challenging is to write a function that
generates all the permutations of an argument list. However, a permutation
predicate can be defined concisely in Prolog:
permutation([], []). permutation(L, [H|T]) :- append(XS, [H|YS], L), append (XS, YS, ZS), permutation(ZS, T).
The second clause takes a little unpacking.
-
append(XS, [H|YS], L)
asserts thatL
is equal toXS [H] YS
; that is,L
can be split into a prefixXS
, an elementH
, and a suffixYS
. -
append(XS, YS, ZS)
asserts thatZS
is equal toXS ++ YS
; importantly,ZS
is comprised of all of the elements ofL
except forH
. -
permutation(ZS, T)
asserts thatT
is a permutation ofZS
; therefore,ZS
is a permutation of all elements ofL
except forH
.
When all of these body terms are satisfied, the head permutation(L,[H|T])
asserts that [H|T]
is a permutation of L
. This rule is correct, because T
is a permutation of all of the elements of L
except for H
; therefore [X|T]
is a permutation of all of the elements of L
.
To put it another way, permutation
works by moving an arbitrary element of L
to the front and then permuting the remaining elements.
In addition to checking whether two lists are permutations of each other, the
permutation
predicate can be used to generate permutations:
?- permutation([a,b,c], [b,a,c]). yes ?- permutation([a,b,c], [a,c]). no ?- permutation([a,b,c], L). L = [a, b, c]; L = [a, c, b]; L = [b, a, c]; L = [b, c, a]; L = [c, a, b]; L = [c, b, a]; no
Unfortunately, like reverse
(and for much the same reason), permutation
cannot be run "backwards".
With permutation
in hand, we can give a direct encoding of the specification
of the problem of sorting a list (of numbers):
ordered([]). ordered([A]). ordered([A,B|L]) :- A =< B, ordered([B|L]). naive_sort(L,SL) :- permutation(L,SL), ordered(SL).
The ordered(L)
predicate succeeds when the elements of L
are ordered from
least to greatest. When there are less than two elements in the list, it is
trivially ordered. When there are two or more elements in the list, then the
first two elements must be in the correct order and the tail of the list must be
in order.
naive_sort(L,SL)
asserts that a list SL
is the sorting of a list L
when
SL
is a permutation of L
that is ordered. This is precisely the
specification of the problem of sorting a list. Unfortunately, while it
precisely captures the what of sorting, the induced how via Prolog’s proof
search is wildely inefficient: naive_sort
is \(O(n!)\). Essentially, this
implementation generates all \(n!\) permutations of the list L
and checks
whether or not each one is ordered.
A more efficient sort is a bubble sort.
bsort(L,L) :- ordered(L). bsort(L1,SL) :- append(XS,[A,B|YS],L1), A > B, append(XS,[B,A|YS],L2), bsort(L2,SL).
Our bubble sort in Prolog has two rules. If the list is already ordered, then
the sorting succeeds (with the list itself). If the list is not already
ordered, then there must be a pair of adjacent elements that are out of order;
swap those pairs and check again. (This implementation is a little inefficient,
because the failure of the ordered(L)
sub-goal in the first rule will have
found the out of order elements, but that information is lost when backtracking
into the second rule, which will consider all splits of the list and check the
ordering of the first two elements of the suffix list.)
A yet more efficient sort (on average) is a quick sort.
partition(Pivot, [A|XS], [A|YS], ZS) :- A =< Pivot, partition(Pivot, XS, YS, ZS). partition(Pivot, [A|XS], YS, [A|ZS]) :- Pivot < A, partition(Pivot, XS, YS, ZS). partition(Pivot, [], [], []). qsort([], []). qsort([X|XS], SL) :- partition(X, XS, LEs, GTs), qsort(LEs, SLT), qsort(GTs, GTs), append(SLEs, [X|SGTs], SL).
The partition(Pivot,XS,LEs,GTs)
predicate succeeds when LEs
is the list of
elements from XS
that are less than or equal to Pivot
and GTs
is the list
of elements from XS
that are greater than Pivot
.
Exercises
-
Use the
member
predicate to define a (non-recursive)overlap(L1,L2)
predicate that succeeds when the listsL1
andL2
have at least one element in common. Investigate the behavior ofoverlap
. -
How does the behavior of
append
change if we replace the axiomappend([], YS, YS).
with the axiomappend(XS, [], XS).
? -
How does the behavior of
append
change if we keep the axiomappend([], YS, YS).
and add the axiomappend(XS, [], XS).
? -
Work carefully through the executions of
reverseA(XS,[c,b,a])
andreverseB(XS,[c,b,a])
to understand why the proof search enters an infinite loop when backtracking after the first solution. -
How does the behavior of
ordered
change if we replace the ruleordered([A,B|L]) :- A =< B, ordered([B|L]).
with the ruleordered([A,B|L]) :- A =< B, ordered(L).
. -
Implement an
isort(L, SL)
predicate that sorts a list using an insertion sort.
Logic Puzzle example
puzzleA.P
is a direct approach:
puzzle_soln(BLDG) :- empty_building(BLDG), location(baker,BF,BLDG), location(cooper,CF,BLDG), location(fletcher,FF,BLDG), location(miller,MF,BLDG), location(smith,SF,BLDG), floor_neq(BF,fifth), floor_neq(CF,first), floor_neq(FF,fifth), floor_neq(FF,first), floor_nadj(FF,SF), floor_nadj(FF,CF), floor_gt(MF,CF). empty_building(building(_,_,_,_,_)). location(P,first,building(P,_,_,_,_)). location(P,second,building(_,P,_,_,_)). location(P,third,building(_,_,P,_,_)). location(P,fourth,building(_,_,_,P,_)). location(P,fifth,building(_,_,_,_,P)). floor_eq(F, F). floor_neq(first, second). floor_neq(first, third). floor_neq(first, fourth). floor_neq(first, fifth). floor_neq(second, first). floor_neq(second, third). floor_neq(second, fourth). floor_neq(second, fifth). floor_neq(third, first). floor_neq(third, second). floor_neq(third, fourth). floor_neq(third, fifth). floor_neq(fourth, first). floor_neq(fourth, second). floor_neq(fourth, third). floor_neq(fourth, fifth). floor_neq(fifth, first). floor_neq(fifth, second). floor_neq(fifth, third). floor_neq(fifth, fourth). floor_adj(first, second). floor_adj(second, first). floor_adj(second, third). floor_adj(third, second). floor_adj(third, fourth). floor_adj(fourth, third). floor_adj(fourth, fifth). floor_adj(fifth, fourth). floor_nadj(first, third). floor_nadj(first, fourth). floor_nadj(first, fifth).
-
empty_building(B)
establishes the structure of a building, essentially a 5-tuple. Useful for writingempty_building(BLDG)
in the goal and then simply usingBLDG
elsewhere. -
location(P,F,B)
establishes that personP
is on floorF
of buildingB
. -
floor_eq(F1, F2)
establishes that floorsF1
andF2
are equal. -
floor_neq(F1, F2)
establishes that floorsF1
andF2
are not equal. It is a long, tedious enumeration because, in Prolog, must give positive conditions; in general, cannot define not-equal by failure to be equal. (Advanced note: Even withnot
predicate,floor_neq(F1, F2) :- not(floor_eq(F1, F2))
cannot be used to enumerate pairs of unequal floors or to enumerate the floors not equal to a given (ground) floor.) -
floor_adj(F1, F2)
establishes that floorsF1
andF2
are adjacent. Must be explicitly symmetric;floor_adj(F1, F2) :- floor_adj(F2, F1)
would lead to infinite loops. -
floor_nadj(F1, F2)
establishes that floorsF1
andF2
are not adjacent. -
floor_gt(F1, F2)
establises that floorF1
is greater than floorF2
. -
puzzle_soln(BLDG)
establishes the building structure, that each person lives on some floor, and then encodes each of the conditions.
puzzleB.P
uses an encoding to simplify:
puzzle_soln(BLDG) :- empty_building(BLDG), location(baker,BN,BLDG), location(cooper,CN,BLDG), location(fletcher,FN,BLDG), location(miller,MN,BLDG), location(smith,SN,BLDG), floor_neq(BN, fifth), floor_neq(CN, first), floor_neq(FN, fifth), floor_neq(FN, first), floor_nadj(FN, SN), floor_nadj(FN, CN), floor_gt(MN, CN). empty_building(building(_,_,_,_,_)). location(P,first,building(P,_,_,_,_)). location(P,second,building(_,P,_,_,_)). location(P,third,building(_,_,P,_,_)). location(P,fourth,building(_,_,_,P,_)). location(P,fifth,building(_,_,_,_,P)). append([], L2, L2). append([H1|T1], L2, [H1|L3]) :- append(T1,L2,L3). member(X, [X|T]). member(X, [H|T]) :- member(X, T). eqInList(X, X, L) :- member(X, L). neqInList(X, Y, L) :- append(L1, [X|L2], L), member(Y, L1). neqInList(X, Y, L) :- append(L1, [X|L2], L), member(Y, L2). adjInList(X, Y, L) :- append(L1, [X,Y|L2], L). adjInList(X, Y, L) :- append(L1, [Y,X|L2], L). nadjInList(X, Y, L) :- append(L1, [Z,X|L2], L), member(Y, L1). nadjInList(X, Y, L) :- append(L1, [X,Z|L2], L), member(Y, L2). ltInList(X, Y, L) :- append(L1, [X|L2], L), member(Y, L2). gtInList(X, Y, L) :- append(L1, [X|L2], L), member(Y, L1). floors([first,second,third,fourth,fifth]). floor_eq(F1, F2) :- floors(FS), eqInList(F1, F2, FS). floor_neq(F1, F2) :- floors(FS), neqInList(F1, F2, FS). floor_adj(F1, F2) :- floors(FS), adjInList(F1, F2, FS). floor_nadj(F1, F2) :- floors(FS), nadjInList(F1, F2, FS).
-
{eq,neq,adj,nadj,lt,gt}InList(X, Y, L)
establishes a relationship betweenX
andY
, whereL
is an in-order enumeration of the elements from whichX
andY
are drawn. The rules work usingmember
andappend
to find the relative positions ofX
andY
inL
. -
floors(FS)
establishes the list of floors for use with the*InList
predicates. -
floor_{eq,neq,adj,nadj,gt}
use floors and the*InList
predicates.
This puzzle is so "small" that the order of the sub-goals and the amount of backtracking mostly does not matter, but that isn’t always true for larger puzzles.
puzzleB.P
demonstrates a faster solution:
;; The above version is rather slow, because it generates each of the 5! ;; candidate buildings and checks each one for the conditions. ;; The below version is faster, because it interleaves predicates that assert the ;; location of a person in the building with predicates that assert the ;; additional conditions. ;; For example, by placing `floor_neq(BN, fifth)` immediately after ;; `location(baker,BN,BLDG)`, Prolog immediately backtracks anytime it places ;; `baker` on the `fifth` floor, without going on to place the remaining persons ;; in the house; this eliminates a full fifth of the search space. Similarly, ;; by placing `floor_neq(CN, first)` immediately after ;; `location(cooper,CN,BLDG)`, Prolog immediately backtracks anytime it places ;; `cooper` on the `first` floor, without going on to place the remaining ;; persons in the building; this eliminates a full quarter of the (remaining) ;; search space. puzzle_soln_fast(BLDG) :- empty_building(BLDG), location(baker,BN,BLDG), floor_neq(BN, fifth), ;; location(cooper,CN,BLDG), floor_neq(CN, first), location(fletcher,FN,BLDG), floor_neq(FN, fifth), floor_neq(FN, first), location(smith,SN,BLDG), floor_nadj(FN, SN), floor_nadj(FN, CN), location(miller,MN,BLDG), floor_gt(MN, CN).
Family Trees
parent(M,X) :- mother(M,X). parent(F,X) :- father(F,X). grandparent(GP,X) :- parent(GP,P), parent(P,X). sibling(X,Y) :- mother(M,X), mother(M,Y), father(F,X), father(F,Y). aunt_or_uncle(AU,X) :- parent(P,X), sibling(AU,P).
See family.P
Are you your own sibling (according to Prolog)?
"Negative information" is more difficult to express in Prolog than is "positive information".
Backtracking and Cuts
Backtracking sometimes generates incorrect or extraneous answers when searching for alternative solutions.
The dedup(L1,L2)
predicate is meant to succeed when L2
is the list obtained
by removing all duplicate elements of L1
. We might try to implement dedup
as follows:
dedup_bad([],[]). dedup_bad([H|T], L) :- member(H,T), dedup_bad(T, L). dedup_bad([H|T], [H|L]) :- dedup_bad(T,L).
The idea is that H
is not added to the output list if member(H,T)
, but H
is added to the output list otherwise.
However, consider the following transcript:
?- dedup_bad([a,b,b,c], L). L = [a, b, c]; L = [a, b, b, c]; no
When additional solutions are requested, then proof search resumes (as though
member(H,T)
had failed) and uses the last rule to add H
to the the output
list.
Introduce a predicate to prevent backtracking
-
Avoid incorrect solutions (correctness)
-
Avoid extraneous solutions (efficiency)
The "cut" predicate, written !
, is a predicate that is always satisfied, but
with a side effect: it commits the interpreter to whatever choices have been
made since unifying the parent goal with the left-hand side of the current rule,
including the choice of that unification itself.
By placing a cut after the member(H,T)
predicate, we ensure that after
member(H,T)
succeeds, we cannot backtrack and fall through to the next rule.
dedup_good([],[]). dedup_good([H|T], L) :- member(H,T), !, dedup_good(T, L). dedup_good([H|T], [H|L]) :- dedup_good(T,L).
?- dedup_good([a,b,b,c], L). L = [a, b, c]; no
The cut is a non-logical operator:
* Deviates from “pure” logic programming (the logical interpretation of Prolog)
* The goto
of logic programming
Sometimes useful, but can be abused
More difficult to reason about programs
-
Idiomatic cuts
-
Confining cuts to other, well-defined, predicates.
-
-
Good/Green cuts – preserve logical meaning of program
-
Bad/Red cuts – destroy logical meaning of program
Idiomatic Cuts
Use cuts to implement other, well-defined, predicates.
The following is an almost, but not quite, not-equal predicate.
eq(X, X). neq(X, Y) :- eq(X, Y), !, fail. neq(X, Y).
Note: The neq
predicate does not correspond to logical negation of eq
. In
particular, cannot be used to enumerate non-equal terms.
?- eq(a, a). yes ?- eq(a, b). no ?- neq(a, a). no ?- neq(a, b). yes ?- eq(a, X). X = a; no ?- neq(a, X). no
For every predicate, we can apply this idiom. In fact, this idiom is so common
that full Prolog (and uProlog with Exercises 44 and 45 of Chapter 11 from
Programming Languages: Build, Prove, and Compare) provides the not
predicate
to provide this idiom.
Good/Green Cuts
Introduced to make program more efficient by eliminating “known” useless computations.
max(X,Y,Y) :- X =< Y, !. max(X,Y,X) :- X > Y.
-
Without the cuts, the program produces the same solutions (perhaps requiring more time or space).
-
Logical meaning of the program remains the same.
-
Bad/Red Cuts
Introduced to make program more efficient by eliminating some meaningful solutions.
-
Without the cuts, the program might produce different solutions.
-
Changes the logical meaning of the program.
Acknowledgments
Portions of these notes based upon material by Hossein Hojjat.