CPS 343/543, 430/542 Lecture notes:
Introduction to PROLOG
Coverage:
(CPS 343/543) [COPL] §§16.4-16.8
(pp. 624-650), [PLPP] §§12.4-12.5 (pp. 552-568), and
(CPS 430/542) [FCDB] §§10.1-10.3 (pp. 463-492)
Logic programming
- tries to make programming
a specification (of solution) activity
(called declarative
programming), but it falls shorts
- PROLOG (PROgramming LOGic) is a declarative programming language
Formalism gone awry
- what is the problem with implementing resolution in a computer
system?
- how should be search the database: top-down, bottom-up, or neither?
- the goal {} ⊂ legs(horse, 4) led to 2 sub-goals:
{} ⊂ mammal(x) ∧ arms(x,0)
- in which order should the system try to prove the
sub-goals? left-to-right or right-to-left or neither?
- in this case, the end result (true) is the same, but in
other cases in might make a difference (e.g. (see proof tree on
p. 559 of [PLPP]),
ancestor(x,y) ⊂ parent(x,z) ∧
ancestor(z,y).
ancestor(x,x).
parent(amy,bob).
{} ⊂ ancestor(x,bob).
)
- therefore: order in which
- the database, and
- sub-goals are searched
is significant
- an
implementation must use a fixed search strategy, and the programmer must be
aware of these (unsettling)
- questions
- how to search the database? top-down or bottom-up or neither?
- how to search the sub-goals? left-to-right or right-to-left or neither?
- PROLOG search its database top-down
and searches sub-goals left-to-right
(i.e., using depth-first-search)
- do not confuse backward chaining with bottom-up search
- this violates the defining principle of a declarative language, that is
that the programmer need worry only about the logic and leave the control
(inference methods used to prove an answer) up to the system (resolution comes
for free with PROLOG, programmer need not program it!)
- we have a pure functional programming language (Haskell),
can we have a pure logic programming language?
- the answer is yes, but it would be far too inefficient to be practical
- holy grail of logic programming: original goal of logic programming was to
make programming a specification activity (i.e., declarative programming!)
- pure logic programming is non-deterministic;
programmers should not have to impart control flow
- therefore, PROLOG falls short
Summary
If we have a goal Q, to prove Q, PROLOG must either
- find Q as a fact in the database, or
- find Q as a sequence of propositions such that
P2 ⊂ P1.
P3 ⊂ P2.
...
Q ⊂ Pn.
Applications of logic programming
- logical reasoning
- problem solving
- cryparithmetic problems (e.g.,
| SEND |
| +MORE |
| ------------ |
| MONEY |
)
- GRE analytical problems
- puzzles
- artificial intelligence
- specification verification (or rapid prototyping)
- equational logic programming languages (e.g., OBJ3 or
Equation Interpreter Project)
Essential PROLOG programming
use make. to re-consult a file (in SWI-PROLOG)
use n or ; character to get next solution
comments
- % introduces a comment until the end of a line
- C style comments /* ... */ are also permitted, but
unlike in C, in PROLOG these comments can be nested
halt. or EOF character (e.g., ctrl-D on UNIX,
ends session with PROLOG)
use predicate protocol/1 to log your session with PROLOG
(e.g., protocol('diary').)
backtracking
tracing and trace/0: allows user to trace the resolutions process,
including instantiations, as PROLOG tries to satisfy a goal
outputting text
- write
- writeln
- nl (newline)
call/1 is the PROLOG analog of eval in Scheme
add the following goal
set_prolog_flag(toplevel_print_options,[quoted(true), portray(true),
max_depth(0)]).
to a program to prevent PROLOG from abbreviating results with
ellipses.
Here the value of max depth indicates
how deep into the list it will print.
By default it is 10;
if it is set to 0, then the printing depth limit is turned off.
Kowalski's classic formulation of PROLOG strategy:
algorithm = logic + control
contrast with Wirth's formulation of imperative programming:
programs = algorithms + data structures
Unification examples
me = me.
me = you.
me = X.
f(a,X) = g(Y,b).
f(a,X) = f(Y,b).
f(X) = g(X).
gcd(U,0,U).
gcd(U,V,W) :- V \=0, R is U mod V, gcd(V,R,W).
Lists in PROLOG
- uses brackets (like ML and Haskell, but not LISP) to specify lists
- list construction/decomposition in PROLOG vs. Haskell
-----------------------
PROLOG Haskell
-----------------------
[X|Y] X:Y
.(X,Y) X:Y
[X,Y] X:Y:nil
[X] X:nil
[X,Y|Z] X:Y:Z
[X,Y,Z|W] X:Y:Z:W
-----------------------
- the following expressions are nonsense
X|Y
[X|Y,Z]
[X|Y|Z]
Simple PROLOG database
animal(cow).
animal(dog).
animal(ramsay).
animal(rogerrabbit).
animal(yak).
animal('Emu').
likes(larry,lucy).
likes(lucy,apples).
likes(larry,larry).
cpscourse(cps343).
cpscourse(cps543).
cpscourse(cps430).
wtcourse(wt150).
wtcourse(wt151).
challenging(_x) :- cpscourse(_x).
easy(X) :- wtcourse(X).
easy(cps343).
ihave([pencil,pen,watch]).
ihave([cps343,cps430,cps444,cps350]).
ihave([itall]).
ihave([[toyota,2006,corolla],[2008,honda,civic]]).
ihave([book,[pen1, pen2],[newdollabill]]).
ihave(itall).
Simple session with PROLOG
?- consult(first).
% animal compiled 0.00 sec, 936 bytes
Yes
?- animal(X).
X = cow
Yes
?- animal(X).
X = cow ;
X = dog ;
X = ramsay ;
X = rogerrabbit ;
X = yak ;
X = emu ;
No
?- animal(WhatIwant).
WhatIwant = cow ;
WhatIwant = dog ;
WhatIwant = ramsay ;
WhatIwant = rogerrabbit ;
WhatIwant = yak ;
WhatIwant = emu ;
No
?- animal(whatiwant).
No
?- animal(whatIwant).
No
?- animal(X), animal(Y).
X = cow
Y = cow ;
X = cow
Y = dog ;
X = cow
Y = ramsay ;
X = cow
Y = rogerrabbit ;
X = cow
Y = yak ;
X = cow
Y = emu ;
X = dog
Y = cow ;
X = dog
Y = dog ;
X = dog
Y = ramsay ;
X = dog
Y = rogerrabbit ;
X = dog
Y = yak ;
X = dog
Y = emu ;
X = ramsay
Y = cow ;
X = ramsay
Y = dog ;
X = ramsay
Y = ramsay ;
X = ramsay
Y = rogerrabbit ;
X = ramsay
Y = yak ;
X = ramsay
Y = emu ;
X = rogerrabbit
Y = cow ;
X = rogerrabbit
Y = dog ;
X = rogerrabbit
Y = ramsay ;
X = rogerrabbit
Y = rogerrabbit ;
X = rogerrabbit
Y = yak ;
X = rogerrabbit
Y = emu ;
X = yak
Y = cow ;
X = yak
Y = dog ;
X = yak
Y = ramsay ;
X = yak
Y = rogerrabbit ;
X = yak
Y = yak ;
X = yak
Y = emu ;
X = emu
Y = cow ;
X = emu
Y = dog ;
X = emu
Y = ramsay ;
X = emu
Y = rogerrabbit ;
X = emu
Y = yak ;
X = emu
Y = emu ;
No
?- animal(X), animal(Y), X\=Y.
X = cow
Y = dog ;
X = cow
Y = rabbit ;
X = cow
Y = rogerrabbit
?- likes(larry,X), likes(X,apples).
X = lucy ;
?- cpscourse(X).
X = cps534 ;
X = cps430 ;
X = cps343 ;
No
?- easy(X).
X = cps343 ;
X = wt150 ;
X = wt151 ;
No
?- ihave(X).
X = [pencil,pen,watch] ;
X = [cps343,cps430,cps444,cps350] ;
X = [itall] ;
X = [[toyota,1998,corolla],[1997,honda,civic]] ;
X = [book,[pen1,pen2],[newdollabill]] ;
X = itall ;
No
?- ihave([X|Y]).
X = pencil
Y = [pen,watch] ;
X = cps343
Y = [cps430,cps444,cps350] ;
X = itall
Y = [] ;
X = [toyota,1998,corolla]
Y = [[1997,honda,civic]] ;
X = book
Y = [[pen1,pen2],[newdollabill]] ;
No
?- ihave([X,Y|Z]).
X = pencil
Y = pen
Z = [watch] ;
X = cps343
Y = cps430
Z = [cps444,cps350] ;
X = [toyota,1998,corolla]
Y = [1997,honda,civic]
Z = [] ;
X = book
Y = [pen1,pen2]
Z = [[newdollabill]] ;
No
?- ihave([X,Y]).
X = [toyota,1998,corolla]
Y = [1997,honda,civic] ;
No
?- ihave([X]).
X = itall ;
No
?- ihave([X,Y,Z]).
X = pencil
Y = pen
Z = watch ;
X = book
Y = [pen1,pen2]
Z = [newdollabill] ;
No
?- halt.
Simple control
- ancestor
- order of clauses matter
- left recursion is bad, since PROLOG uses DFS!
- mutual recursion is bad: doesn't
cause stack overflow, but infinite loop nevertheless
- backtracking to find alternate solutions
% ref. [PLPP] pp. 558-559
ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y). % clause 1
ancestor(X,X). % clause 2
parent(amy,bob).
analyze search tree:
(ref. [PLPP] Fig. 12.2, p. 559)
/* in ancestor(X,Y), Y is meant to be the ancestor of X */
/* order of predicate definitions matters
beware of left recursion, as written below
because PROLOG uses depth-first search
*/
parent(sallie,amy).
parent(amy,marc).
ancestor(X,Y) :- parent(X,Y).
/*
ancestor(X,Y) :- ancestor(X,Z), parent(Z,Y).
*/
/* left recursion is bad */
/* why doesn't left recursion cause a
problem in reverse?
*/
ancestor(X,Y) :- parent(Z,Y), ancestor(X,Z).
/* the fix
*/
/* have not exhausted stack,
rather an infinite transfer of control
*/
niceday(X) :- classletsoffearly(X).
classletsoffearly(X) :- niceday(X).
List codes written in class
isempty([]).
islist([]).
/* only one of the following is required, the first is preferred */
islist([_|_]).
islist([_|T]) :- islist(T).
/* only one of the following is required, the first is preferred */
cons(H,T,[H,T]).
cons(H,T,L) :- L = [H|T].
/* member is built-in */
member(E,[E|_]).
member(E,[_|T]) :- member(E,T).
Using append as a primitive to
construct some simple list predicates
/* predicate to append two lists; */
/* first two are the inputs, last is the appended list */
/* this predicate has a minor bug; */
/* it does not prune duplicates (see below) */
/* fix it */
append([],L,L).
append(L,[],L).
append([H|T],Y,[H|W]) :- append(T,Y,W).
/* predicate to check if X is a member of list List */
member(E,L) :- append(_,[E|_],L).
/* predicate to check if X is a sublist of Y */
sublist(X,Y) :- append(_,X,W), append(W,_,Y).
/* predicate to triple a list
given [3] produce [3,3,3] as answer
L when tripled gives list LLL */
triple(L,LLL) :- append(L,L,LL), append(LL,L,LLL).
/* predicate to reverse a list
give X as input, Y is the reversed X */
reverse([],[]).
/* why isn't left recursion a problem here? */
reverse([H|T],RL) :- reverse(T,RT), append(RT,[H],RL).
/* predicate to do bubblesort
X when bubblesorted gives Y */
bubblesort(L,SL) :- append(M,[A,B|N],L),
A > B,
append(M,[B,A|N],S),
%bubblesort(S,SL).
bubblesort(S,SL), !.
bubblesort(L,L).
/* fix this code yourself, so as to
not give more answers after the correct one */
Paths
/* edge(X,Y) means there is a directed edge from X to Y */
edge(a,b).
edge(b,c).
edge(c,a).
/* path(X,Y) is true when there is a
directed path from X to Y */
/* have a third argument that keeps a running tally of nodes visited so far */
path(X,X,_).
path(X,Y,T) :- edge(X,Z),
notamember(Z,T),
append([Z,T,T2),
path(Z,Y,T2).
/* we can go from X to Y through Z only
if Z was not already visited in T */
notamember(E,[]).
notamember(E,[H|T]) :- E \= H, notamember(E,T).
Analogs to relational databases
(primarily for CPS 430/542)
% relation, called predicate in PROLOG
movies1(roger_rabbit,1990,123,paramount).
movies1(get_shorty,1966,122,fox).
movies2(get_shorty,1966,122,fox).
% union
moviesU(X,Y,Z,W) :- movies1(X,Y,Z,W).
moviesU(X,Y,Z,W) :- movies2(X,Y,Z,W).
% intersection
moviesI(X,Y,Z,W) :- movies1(X,Y,Z,W), movies2(X,Y,Z,W).
% difference
moviesD(X,Y,Z,W) :- movies1(X,Y,Z,W), not(movies2(X,Y,Z,W)).
% projection
title(T) :- movies1(T,_,_,_).
% selection
new_movies(T,Y,L,fox) :- movies1(T,Y,L,fox), L >= 121.
% selection followed by a projection
new_movies2(T) :- movies1(T,_,L,fox), L >= 121.
% or
new_movies2(T) :- movies1(T,_,L,S), S = 'fox', L >= 121.
% theta-join
fox_stars(T,Y,L,fox,N) :- movies(T,Y,L,fox), actors(N,T,fox).
% natural join
movies_aug(T,Y,L,S,N) :- movies(T,Y,L,S), stars(N,T,S).
Imparting more control in PROLOG:
the cut operator (!)
- ! is called cut; it is the goto of PROLOG;
use it with great precaution
- cut (!) always evaluates to true (e.g.,
?- !.
Yes
)
- fail always evaluates to false (e.g.,
?- false.
No
)
- uses of cut
- preventing consideration of alternate solutions
- preventing multiple solutions from being produced
- reduce number of branches in search tree to pursue
- 'freezing' parts of solutions
all really one in the same
- example
/* cut prevents consideration of alternate
solutions by freezing parts of current solution. */
juice(apple).
juice(grape).
juice(orange).
/* execute the above code with the following goals:
?- juice(X).
?- juice(X), juice(Y).
?- juice(X), !, juice(Y) */
juice(apple).
juice(grape) :- !.
juice(orange).
More cut examples
- example (ref. [PLPP] pp. 561-562):
ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).
ancestor(X,X).
parent(amy,bob).
/* goal is ancestor(X,bob) */
% try these various combos:
ancestor(X,Y) :- parent(X,Z), !, ancestor(Z,Y).
ancestor(X,X).
parent(amy,bob).
(ref. [PLPP] Fig. 12.4, p. 561)
ancestor(X,Y) :- !, parent(X,Z), ancestor(Z,Y).
ancestor(X,X).
parent(amy,bob).
(ref. [PLPP] Fig. 12.4, p. 561)
ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).
ancestor(X,X) :- !.
parent(amy,bob).
- member example:
/* cut here prevent member from finding all occurrences */
member(E,[E|_]) :- !.
member(E,[_|T]) :- member(E,T).
member(E,[1,2,1,7]).
E = 1 ;
No
- squarelist example:
/* squarelist takes a list and squares every integer element of the list;
if an element is not an integer,
it is simply inserts the element as is into the result list, the second argument
here, cut is serving two purposes:
- fix rule 2 when you know A is an integer
- skip all subsequent rules (in this case, rule 3) */
squarelist([],[]).
squarelist([A|B],[C|D]) :- integer(A), !,
C is A*A,
squarelist(B,D).
squarelist([A|B],[A|D]) :- squarelist(B,D).
- practice with cuts from [PPFC]
/* more practice with cuts;
determine what each of back1, back2, and back3 will print */
back1 :- example(X),
d(Y),
write(X), write(Y), nl,
fail.
back2 :- example(X), !,
d(Y),
write(X), write(Y), nl,
fail.
back3 :- example(X),
e(Y),
write(X), write(Y), nl,
fail.
example(1).
example(2).
d(1).
d(2).
d(3).
e(1) :- !.
e(2).
Towers of Hanoi in PROLOG
/* move n disks from peg A to peg B using peg C as intermediary;
requires (2^n)-1 moves, where n is the number of discs */
/* does this cut serve any useful purpose? */
move(0,_,_,_) :- !.
move(N,A,B,C) :- M is N-1,
move(M,A,C,B),
step(A,B),
move(M,C,B,A).
step(A,B) :- write('Move one disc from peg '),
write(A),
write(' to peg '),
write(B),
writeln('.'),
/* Towers of Hanoi is an exponential-time algorithm.
Orders of Growth: if we ran an exponential-time algorithm with a
data set of size n=100 on a computer which performed
1 billion operations per second, we would have to
wait for approximately 4 x 10^11 centuries for the
code to finish running! */
Math in PROLOG
gcd(U,0,U).
gcd(U,V,W) :- V \=0, R is U mod V, gcd(V,R,W).
factorial(0,1).
factorial(X,Y) :- X > 0,
Z is X-1,
factorial(Z,M),
Y is M*X.
sum(0,0).
sum(X,Y) :- X > 0,
Z is X-1,
sum(Z,M),
Y is M+X.
Negation in PROLOG
- is the not/1 predicate in PROLOG a logical NOT?
- exercise care when using it
- its use can produce counter-intuitive results
mother(mary).
?- mother(X).
X = mary
true.
?- not(mother(X)).
false % this is not saying 'there are no mothers'
?- not(not(mother(X))).
X = _G157
starts with the innermost clause and succeeds with X = mary,
the moment the clause becomes false, the instantiation is released
?- not(not(mother(mary))).
true.
?- not(X=0) % returns no, without binding X
false.
/* in logic the order should not matter */
?- X=0, not(X=1). % instantiates X to 0 */
?- not(X=1), X=0. % fail at first sub-goal without binding X to 0. why?
?- not(not(X=1)), X=0.
X=0.
?- not(X=1), not(X=0).
false.
moral of the story: not in PROLOG is not a logical NOT
Reflective predicates
Recall, PROLOG programs are built from terms. A term is either a constant,
variable, or structure.
- assert/1 or assertz/1:
adds a fact to the end of the database
- asserta/1: adds a fact to the beginning of the database
- retract/1: removes a fact from the database
% indicates that car is a dynamic predicate
:- dynamic car/1.
car(honda).
car(toyota).
?- car(CAR).
CAR = honda ;
CAR = toyota ;
No
?- asserta(car(ford)).
Yes
?- assertz(car(bmw)).
Yes
?- retract(car(honda)).
Yes
?- car(CAR).
CAR = ford ;
CAR = toyota ;
CAR = bmw ;
No
?-
assert and retract can be used to build
a tic-tac-toe program
clause/2: matches the head and body of an existing
clause in the database; can be used to implement a metacircular
interpreter (i.e., an implementation of call/1 [PLP] p. 578)
var(Term):
succeeds if Term is currently a free variable
nonvar(Term):
succeeds if Term is currently not a free variable
ground(Term):
succeeds if Term holds no free variable
Impurities:
mismatch between LOGIC and PROLOG
| FOPL | LP (PROLOG) |
| any form of proposition possible |
restricted to Horn clauses |
| order in which sub-goals are searched insignificant |
order in which sub-goals are searched significant (l-to-r) |
| order in which clauses are searched insignificant |
order in which clauses are searched significant (top-down) |
| logical NOT |
NOT as failure (due to unification) |
in summary, `there are aspects of predicate calculus that PROLOG
cannot capture and there are aspects of PROLOG (e.g., its imperative
and database manipulating features) that have no analogues in predicate
calculus' [PLP1] p. 642
another impure feature of PROLOG:
closed world assumption; PROLOG is a true/fail system,
like our legal system, rather than a true/false system [COPL]
Problems with PROLOG
(these are all somewhat interrelated)
- to be purely declarative, programmer should not be required to
affect control flow for program success
- consequent of DFS search strategy
- left recursion often leads to incorrect results
- closed world assumption (and nonmonotonic reasoning);
PROLOG is a true-fail system, not a true-false system [COPL] p. 643.
There is no mechanism in PROLOG by which to assert facts assumed to
be false. PROLOG relies on pure positive logic. This
is another reason why
not in PROLOG (see below) is not a logical not.
As a result not(P) can succeed simply because PROLOG cannot
prove P true.
- Horn clauses are not expressive enough to capture all knowledge in
first-order predicate calculus (e.g., propositions in clausal
form with with a disjunction of more than one nonnegated term such as
every `every positive natural number is either even or odd'
even(N) ∨ odd(N) ⊂ natural(N).
)
- negation as failure (a
reflection of PROLOG's limitation to Horn clauses)
odd(N) :- natural(N), not(even(N)).
even(N) :- natural(N), not(odd(N)).
- more negation issues: not(transmission(X,manual))
means ¬ ∃X (transmission(X,manual)) or `there are no
cars with manual transmissions' rather than ∃X
(¬transmission(X,manual)) or `not all cars have a manual transmission'
(example inspired by [PLP] p. 582).
As a result, the goal not(transmission(X,manual)) fails
even if we have transmission(accord,manual) in our database.
- the occurs-check problem
Semantic part of PROLOG interpreter
(an implementation of the built-in call/1)
(ref. [EBG=PE])
prolog(Leaf) :- clause(Leaf,true).
prolog((Goal1, Goal2)) :- prolog(Goal1), prolog(Goal2).
prolog(Goal) :- clause(Goal,Clause), prolog(Clause).
References
| [COPL] |
R.W. Sebesta.
Concepts of Programming Languages.
Addison-Wesley, Boston, MA, Sixth edition, 2003. |
| [EBG=PE] |
F. van Harmelen and A. Bundy.
Explanation-Based Generalisation = Partial Evaluation.
Artificial Intelligence, 36(3), 401-412, 1988. |
| [FCDB] |
J.D. Ullman and J. Widom. A First Course in Database Systems.
Prentice Hall, Upper Saddle River, NJ, Second edition, 2002.
|
| [PLP1] |
M.L. Scott.
Programming Language Pragmatics.
Morgan Kaufmann, San Francisco, First edition, 2000.
|
| [PLP] |
M.L. Scott.
Programming Language Pragmatics.
Morgan Kaufmann, Amsterdam, Second edition, 2006.
|
| [PLPP] |
K.C. Louden.
Programming Languages: Principles and Practice.
Brooks/Cole, Pacific Grove, CA, Second edition, 2002.
|
| [PPFC] |
P. Burna.
Prolog Programming A First Course.
|
|