CPSC 322 - Lecture 10 - September 29, 2004

CPSC 322 - Lecture 10

More Lists, More Recursion


I.  Simple recursive programming on lists

In the last lecture, we ended with writing the proof procedure for
"member":

member(E,[E|R]).
member(E,[F|R]) <- member(E,R).

You can use member in several ways.  You could ask if something is
an element of a list:

  ask member(b, [a,b,c]).

Or you could ask what things are elements of a list:

  ask member(X, [a,b,c]).

Or you could even ask what lists some element could be a member of:

  ask member(b, X).

So while our CILOG procedures share some similarities with true 
functions in that there's no state, no assignment, and therefore no
traditional iteration, these procedures have far more power than
ordinary functions written in Scheme.  Also, the programs we write
in CILOG are entirely declarative...there's no specification of 
flow of control because that information resides inside of the 
CILOG interpreter.  These significant differences take CILOG and 
Prolog beyond the functional programming paradigm and into their
own paradigm, usually called logic programming or sometimes 
relational programming.

After talking about member, I said, "Think about how you'd append 
two lists," thinking that's what I'd talk about today.  In the 
interim, I thought of a few other problems that would be useful 
practice.  The first of those is a variation on the member relation -- 
I'll call it "notmember", but your book calls it "notin".

Here's one way to think about the notmember proof procedure.  
Let's say you want to prove that element x is not a member of 
the list [a,b,c].  A little analysis tells us that the theorem

  notmember(x, [a,b,c])

is true if x doesn't match the first element of the list, which 
it doesn't, and x is not a member of the rest of the list.  That is:

  notmember(x, [a,b,c]) is true if notmember(x, [b,c]) and x \= a

Here's a more generalized and CILOGy way of saying the same thing, 
using variables instead of constants:

  notmember(X, [First|Rest]) <- notmember(X,Rest) & diff(X,First).
  diff(X,Y) <- X \= Y.

We still need a termination condition.  If we keep applying the 
rules above to our simple example, we'll eventually get to a point 
where we're looking at:

  notmember(x, [])

That seems like a good place to stop, since we know that nothing 
is a member of the empty list.  Our generalized version of the 
base case is:

  notmember(X, []).

The whole proof procedure looks like this:

  notmember(X, [First|Rest]) <- notmember(X,Rest) & diff(X,First).
  diff(X,Y) <- X \= Y.
  notmember(X, []).

Here it is in action:

cilog: ask notmember(x,[a,b,c]).
Answer: notmember(x, [a, b, c]).
 Runtime since last report: 0 secs.
  [ok,more,how,help]: ok.
cilog: ask notmember(b,[a,b,c]).
No. notmember(b, [a, b, c]) doesn't follow from the knowledge base.
 Runtime since last report: 0 secs.

Note that using the \= relation can sometimes lead to 
unexpected results.  As the warning message below (as well as
the CILOG manual) tells us, some parts of CILOG aren't fully
implemented.

cilog: ask notmember(X,[a,b,c]).
Warning: A\=c failing. Delaying not implemented.
No. notmember(A, [a, b, c]) doesn't follow from the knowledge base.
 Runtime since last report: 0 secs.


II.  Prefixes and suffixes

In the world of list manipulation, it often happens that we'd like
to know if some list or sequence appears within some other list.  
For example, we might want to know if the sequence [b,c] appears 
within the list [a,b,c,d].  Especially if that turns out to be a 
homework problem.  

Before we get there, though, let's look at two other procedures 
that help us out:  prefix and suffix.  First, we want to create a 
proof procedure that lets us prove a theorem such as this:

  prefix([a,b],[a,b,c]).

We've had so much practice in recursive CILOG programming now that 
this one should leap out at you.  We want our proof procedure to 
see if the first element of the first list is the same as the 
first element of the second list.  If that's true, then the proof 
procedure should tell us if

  prefix([b], [b,c])

is true.  In CILOG, it looks like this:

  prefix([X|Rest1],[X|Rest2]) <- prefix(Rest1, Rest2).

The base case or termination condition is when the first list is 
the empty list:

  prefix([], Anylist).

So the whole thing looks like this:

  prefix([X|Rest1],[X|Rest2]) <- prefix(Rest1, Rest2).
  prefix([], Anylist).

Similarly, we can write a proof procedure to show that

  suffix([b,c],[a,b,c])

is true.  How do we know if the smaller list on the left is a
suffix of the bigger list on the right?  If we "peel off" the
element at the front of the bigger list, then the list on the left
matches what remains of the list on the right.  So we know two 
things:  First, the proof procedure can terminate successfully 
when the list on the left is the same as the left on the right.

  suffix(List,List).

Second, in the case where the two lists aren't the same, we can
prove suffixness (?) by removing elements from the front of the 
list on the right until the two lists are the same.

  suffix(List,[Headbiglist|Restbiglist]) <-
  suffix(List|Restbiglist).


III.  How to do your homework

Well, not all of it...just one problem.  Having suffix and prefix
predicates can be quite useful.  For example, if you wanted to 
know if the sequence [b,c] occurs within the list [a,b,c,d], one 
way (of several) to go at this would be to prove that you can 
break the larger list into two parts such that:

  1.  The second part is a suffix of the larger list, and
  2.  The given sequence is a prefix of that second part.

In CILOG terms, those two requirements look like this:

  subsequence(Seq, List) <- suffix(M, List) & prefix(Seq, M).

While we're at it, notice that the member predicate can be 
redefined in terms of subsequence:

  member(X,Y) <- subsequence([X],Y).


IV.  Append:  Many tools in one

Although the "append" predicate was the one I really hoped to get 
to in class today, the repair work I had to do on the audio/video
interface in the classroom took up enough time to prevent our 
getting to append.  Here it is anyway; maybe we'll talk about 
on Friday.

The purpose of append is to join two lists together.  More 
accurately, the purpose is to prove that one particular list is
the result of joining two other specific lists together.
For example, we might want to prove:

  append([a,b],[c,d],[a,b,c,d]).

To find out the result of appending [a,b] and [c,d], we'd use
the more general form of the theorem:

  append([a,b],[c,d],X).

How do we look at this problem?  Don't be discouraged if the 
answer isn't obvious.  Defining append always seems to require
some breakthrough insight.  The insight here is that

  append([a,b],[c,d],[a,b,c,d])

is true if

  append([b],[c,d],[b,c,d]) 

is true (and a=a, which is true of course).  This is true if

  append([],[c,d],[c,d])

is true (and b=b, which is also true).  Is there any need to 
go further?  Nah.  So the generalized base case looks like this:

  append([],X,X).

Those reductions and tests of equality can be encoded like this:

  append([Head|Rest1], List2, [Head|Rest3]) <-
  append(Rest1, List2, Rest3).

The utility of append comes from its ability to split lists by
partially specifying the nature of the split lists.  For example,
you can find the possible front parts of a list by specifying just 
the back part of the list:

cilog: ask append(X,[c,d],Y).
Answer: append([], [c, d], [c, d]).
 Runtime since last report: 0 secs.
  [ok,more,how,help]: more.
Answer: append([A], [c, d], [A, c, d]).
 Runtime since last report: 0 secs.
  [ok,more,how,help]: more.
Answer: append([A, B], [c, d], [A, B, c, d]).
 Runtime since last report: 0 secs.
  [ok,more,how,help]: more.
Answer: append([A, B, C], [c, d], [A, B, C, c, d]).
 Runtime since last report: 0 secs.
  [ok,more,how,help]: 

Or you can find all the different ways to split one list into two:

cilog: ask append(X,Y,[a,b,c]).
Answer: append([], [a, b, c], [a, b, c]).
 Runtime since last report: 0 secs.
  [ok,more,how,help]: more.
Answer: append([a], [b, c], [a, b, c]).
 Runtime since last report: 0 secs.
  [ok,more,how,help]: more.
Answer: append([a, b], [c], [a, b, c]).
 Runtime since last report: 0 secs.
  [ok,more,how,help]: more.
Answer: append([a, b, c], [], [a, b, c]).
 Runtime since last report: 0 secs.
  [ok,more,how,help]: more.
No more answers.
 Runtime since last report: 0 secs.
cilog: 

Furthermore, lots of other problems can be viewed as instances of
list splitting, and the proof procedures associated with them can
be defined in terms of append:

  prefix(X,Y) <- append(X,M,Y)

Essentially, this says that X is a prefix of Y if there's some
mystery list M that can be appended to the back of X to produce Y.
The suffix procedure can be redefined in terms of append too:

  suffix(X,Y) <- append(M,X,Y)

In other words, X is a suffix of Y if there's some list M that can
be appended to the front of X to produce Y.

The member predicate can be viewed as list splitting:

  member(X,Y) <- append(M,[X|Rest],Y)

Which can be read as X is a member of Y if there's some list M
that can be appended to the front of another list whose first 
element is X, and the result of the appending is the list Y.

Again, that's all stuff that wasn't covered in class today, but
it may be useful to know when you're working on homework 
assignment 2.  Always remember that append is your friend.

Last revised: October 2, 2004