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

As we saw in Scheme, we can reverse the 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 that L is equal to XS [H] YS; that is, L can be split into a prefix XS, an element H, and a suffix YS.

  • append(XS, YS, ZS) asserts that ZS is equal to XS ++ YS; importantly, ZS is comprised of all of the elements of L except for H.

  • permutation(ZS, T) asserts that T is a permutation of ZS; therefore, ZS is a permutation of all elements of L except for H.

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 remaing 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 lists L1 and L2 have at least one element in common. Investigate the behavior of overlap.

  • Using append, write (non-recursive) predicates prefix(L1,L2) and suffix(L1,L2) that succeed when L1 is a prefix of L2 and when L1 is a suffix of L2, respectively.

  • How does the behavior of append change if we replace the axiom append([], YS, YS). with the axiom append(XS, [], XS).?

  • How does the behavior of append change if we keep the axiom append([], YS, YS). and add the axiom append(XS, [], XS).?

  • Work carefully through the executions of reverseA(XS,[c,b,a]) and reverseB(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 rule ordered([A,B|L]) :- A =< B, ordered([B|L]). with the rule ordered([A,B|L]) :- A =< B, ordered(L)..

  • Implement an isort(L, SL) predicate that sorts a list using an insertion sort.

Logic Puzzle example

Baker, Cooper, Fletcher, Miller, and Smith live in a five-story building. Baker doesn’t live on the 5th floor and Cooper doesn’t live on the first. Fletcher doesn’t live on the top or bottom floor, and he is not on a floor adjacent to Smith or Cooper. Miller lives on some floor above Cooper.

Who lives on what floors?

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 writing empty_building(BLDG) in the goal and then simply using BLDG elsewhere.

  • location(P,F,B) establishes that person P is on floor F of building B.

  • floor_eq(F1, F2) establishes that floors F1 and F2 are equal.

  • floor_neq(F1, F2) establishes that floors F1 and F2 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 with not 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 floors F1 and F2 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 floors F1 and F2 are not adjacent.

  • floor_gt(F1, F2) establises that floor F1 is greater than floor F2.

  • 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 between X and Y, where L is an in-order enumeration of the elements from which X and Y are drawn. The rules work using member and append to find the relative positions of X and Y in L.

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