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