answer to challenge problem 4 for the new user of ACL2

This answer is in the form of a script sufficient to lead ACL2 to a proof, with a brief prologue.

We wish to collect one copy of each element in x. We'll actually define the method two ways, primitive recursively and tail-recursively, the latter method being analogous to the program:

a = nil;
while (x not empty) {
  a = if (member (car x) a) then a else (cons (car x) a);
  x = (cdr x);
return a;

We'll prove the two ``equivalent'' and we'll prove that they return a subset of x that contains no duplications.

This page is organized into four sections. (A) We will start by proving that the primitive recursive version correct: it returns a subset of its argument that is duplication free. This will be straightforward. (B) Then we'll define the while-loop version and we will prove it ``equivalent'' to the primitive recursive version. This will be challenging primarily because the two methods collect their answers in different orders; even stating the relationship between the two is interesting. Proving it will involve a few lemmas. But once we prove their ``equivalence'' the correctness of the while-loop version will be straightforward from the correctness of the primitive recursive version. (C) We will disable the rules we prove about the while-loop version and prove it correct directly, without exploiting the primitive recursive version. This requires leading the theorem prover more carefully because reasoning about tail-recursive functions that accumulate results is sometimes delicate. (D) Lessons learned -- a narrative that summarizes what we learn from these examples.

We follow The Method, which, recall, involves us in recursive attempts to prove lemmas. We use a notation to indicate our sequence of proof attempts. Here is an example (although in actual use we print things across multiple lines). The number in bracket indicates our ``stack depth''. The ``key term'' is some term from a Key Checkpoint in the failed proof which is responsible for our subsequent action. Sometimes instead of a Key Term we just give an English explanation of what we're thinking.

[0] (defthm main ...)     Failed!    Key Term: ...
[1] (defthm lemma-1 ...)  Succeeded!
[0] (defthm main ...)     Failed!    Key Term: ...
[1] (defthm lemma-2 ...)  Failed!    Key Term: ...
[2] (defthm lemma-2a ...) Succeeded!
[2] (defthm lemma-2b ...) Succeeded!
[1] (defthm lemma-2 ...)  Succeeded!
[0] (defthm main ...)     Succeeded!

The rest of this page is just a re-playable script.

; -----------------------------------------------------------------
; Section A:  The Primitive Recursive Version and Its Correctness

; The property of having duplications is defined as:

(defun dupsp (x) (if (endp x) nil (if (member (car x) (cdr x)) t (dupsp (cdr x)))))

; The primitive recursive method of collecting one copy of each element is:

(defun collect-once (x) (if (endp x) nil (if (member (car x) (cdr x)) (collect-once (cdr x)) (cons (car x) (collect-once (cdr x))))))

; [0] (defthm main-theorem-1-about-collect-once (subsetp (collect-once x) x)) ; Succeeded!

; [0] ; (defthm main-theorem-2-about-collect-once ; (not (dupsp (collect-once x)))) ; Failed! ; Key Term: (MEMBER (CAR X) (COLLECT-ONCE (CDR X)))

; [1] (defthm member-collect-once (iff (member e (collect-once a)) (member e a))) ; Succeeded!

; [0] (defthm main-theorem-2-about-collect-once (not (dupsp (collect-once x)))) ; Succeeded!

; That was really easy!

;----------------------------------------------------------------- ; Section B: The While-Loop Version and Its Correctness -- ; presented in two parts: its equivalence to the primitive recursive ; version and then its correctness proved via that equivalence

; The tail-recursive, or while-loop version, is defined as follows. The ; function below is the loop itself and it ought to be called with a = nil to ; implement the initialization of a in the pseudo-code above.

(defun while-loop-version (x a) (if (endp x) a (while-loop-version (cdr x) (if (member (car x) a) a (cons (car x) a)))))

; We wish to prove that the two are equivalent. But they are actually ; very different. For example,

; (collect-once '(2 4 1 3 1 2 3 4)) = (1 2 3 4) ; (while-loop-version '(2 4 1 3 1 2 3 4) nil) = (3 1 4 2)

; Things get a little more complicated if a is non-nil: ; (while-loop-version '(2 4 1 3 1 2 3 4) '(2 2 4 4)) = (3 1 2 2 4 4)

; Several observations help explain what is happening. (1) Collect-once ; collects the last occurrence of each element, in the order of their last ; occurrences. So, for example, since the last occurrence of 2 preceeds the ; last occurrence of 3 in '(2 4 1 3 1 2 3 4)), then the collected 2 preceeds ; the collected 3 in the answer. But while-loop-version collects the first ; occurrence of each element, in the reverse order of that occurrence. So it ; adds 2 to its accumulator first and adds 3 last, making 3 preceed 2 in the ; answer.

; (2) The while-loop-version does not collect anything already in a and indeed ; just adds stuff to the front of a, returning everything initially in a plus ; one occurrence of everything in x not in a.

; To state the relationship that holds between these two we have to define two ; other functions.

; This is our familiar list reverse function... (defun rev (x) (if (endp x) nil (append (rev (cdr x)) (list (car x)))))

; And this function ``removes'' from x all the elements in y, i.e., copies x ; while dropping the elements of y.

(defun list-minus (x y) (if (endp x) nil (if (member (car x) y) (list-minus (cdr x) y) (cons (car x) (list-minus (cdr x) y)))))

; The specific equivalence we're really interested in is ; (equal (while-loop-version x nil) ; (collect-once (rev x)))

; But we will not be able to prove that by induction because it has the ; constant nil where we need a variable, a, in order to admit an appropriate ; inductive instance. So we will attack the most general problem. What is ; (while-loop-version x a) equal to, in terms of collect-once?

; The most general relationship between the two collection functions is:

; (equal (while-loop-version x a) ; (append (collect-once (list-minus (rev x) a)) a))

; This formula bears thinking about! If you're like us, you won't believe it ; until it is proved!

; [0] ; (defthm general-equivalence ; (equal (while-loop-version x a) ; (append (collect-once (list-minus (rev x) a)) a))) ; Failed! ; Key term in checkpoint: ; (LIST-MINUS (APPEND (REV (CDR X)) (LIST (CAR X))) A)

; [1] (defthm list-minus-append (equal (list-minus (append a b) c) (append (list-minus a c) (list-minus b c)))) ; Succeeded!

; [0] ; (defthm general-equivalence ; (equal (while-loop-version x a) ; (append (collect-once (list-minus (rev x) a)) a))) ; Failed! ; Key term in checkpoint: ; (COLLECT-ONCE (APPEND (LIST-MINUS (REV (CDR X)) A) (LIST (CAR X))))

; [1] ; (defthm collect-once-append ; (equal (collect-once (append a b)) ; (append (list-minus (collect-once a) b) ; (collect-once b)))) ; Failed! ; Key term: ; (MEMBER (CAR A) (APPEND (CDR A) B))

; [2] (defthm member-append (iff (member e (append a b)) (or (member e a) (member e b)))) ; Succeeded!

; [1] (defthm collect-once-append (equal (collect-once (append a b)) (append (list-minus (collect-once a) b) (collect-once b)))) ; Succeeded!

; [0] ; (defthm general-equivalence ; (equal (while-loop-version x a) ; (append (collect-once (list-minus (rev x) a)) a))) ; Failed! ; Key term: ; (APPEND (APPEND (LIST-MINUS (COLLECT-ONCE (LIST-MINUS (REV (CDR X)) A))

; [1] (defthm assoc-append (equal (append (append a b) c) (append a (append b c)))) ; Succeeded!

; [0] ; (defthm general-equivalence ; (equal (while-loop-version x a) ; (append (collect-once (list-minus (rev x) a)) a))) ; Failed! ; Key term: ; (LIST-MINUS (COLLECT-ONCE (LIST-MINUS (REV (CDR X)) A)) ...)

; This key term makes us think of the lemma to move the LIST-MINUS inside the ; COLLECT-ONCE. But when that's done, we will have two LIST-MINUS terms ; nestled together and we will want to combine them into one. Call these two ; lemmas (a) and (b).

; [1] (a) ; (defthm list-minus-collect-once ; (equal (list-minus (collect-once x) a) ; (collect-once (list-minus x a)))) ; Failed! ; Key term: ; (MEMBER (CAR X) (LIST-MINUS (CDR X) A))

; [2] (A pretty fact) (defthm member-list-minus (iff (member e (list-minus x a)) (and (member e x) (not (member e a))))) ; Succeeded!

; [1] (a) (defthm list-minus-collect-once (equal (list-minus (collect-once x) a) (collect-once (list-minus x a)))) ; Succeeded!

; [1] (b) (defthm list-minus-list-minus (equal (list-minus (list-minus x a) b) (list-minus x (append b a)))) ; Succeeded!

; [0] (defthm general-equivalence (equal (while-loop-version x a) (append (collect-once (list-minus (rev x) a)) a))) ; Succeeded!

; That completes the proof of the ``equivalence'' of the two methods.

; Now we prove (1) that the result of while-loop-version is a subset, and (2) ; that it contains no duplications. We prove the two conjuncts separately.

; [0] (defthm main-theorem-1-about-while-loop (subsetp (while-loop-version x nil) x)) ; Succeeded!

; But the theorem prover works harder to do the proof above than one might have ; expected because it doesn't turn into an instance of ; main-theorem-1-about-collect-once because of the presence of the rev term. ; However, we're content that ACL2 managed to do the proof on its own.

; [0] (defthm main-theorem-2-about-while-loop (not (dupsp (while-loop-version x nil))))

; So we see that the proof of correctness of while-loop-version isn't hard, ; after we establish the relationship with the primitive recursive version. ; But finding and proving the relationship is fairly challenging.

; ----------------------------------------------------------------- ; Section C: A Direct Proof of the Correctness of the While-Loop Version

; Some would consider the proof in Section B ``indirect'' because we first showed ; how while-loop-version could be expressed as a collect-once and then proved ; our main theorems about while-loop-version, which means those main proofs ; were conducted in terms of collect-once, not while-loop-version.

; It is interesting to compare this proof with the ``direct'' one in which ; we don't use collect-once at all and reason only about while-loop-version.

; So to do that comparison, let's disable all the lemmas we've proved about ; while-loop-version and try to prove the two main theorems above about ; while-loop-version.

(in-theory (disable general-equivalence main-theorem-1-about-while-loop main-theorem-2-about-while-loop))

; [0] ; (defthm main-theorem-1-about-while-loop-redux ; (subsetp (while-loop-version x nil) x)) ; Failed! [Well, the truth is below...]

; We don't even submit this event above because we recognize that it is not ; general enough to permit proof by induction. We need to deal with the nil in ; the second argument of while-loop-version. Experience with induction tells ; us this should be a variable, so we can assume an appropriate inductive ; instance. Therefore, we adopt this subgoal immediately:

; [1] ; (defthm main-lemma-1-about-while-loop-version ; (subsetp (while-loop-version x a) (append x a))) ; Failed! ; Key Term: Does the wrong induction.

; [1] ; (defthm main-lemma-1-about-while-loop-version ; (subsetp (while-loop-version x a) (append x a)) ; :hints (("Goal" :induct (while-loop-version x a)))) ; Failed! Two key terms are suggested ; Key term: (IMPLIES (AND ... (SUBSETP (WHILE-LOOP-VERSION (CDR X) A) (APPEND (CDR X) A))) ; (SUBSETP (WHILE-LOOP-VERSION (CDR X) A) (CONS ... (APPEND (CDR X) A)))) ; Key term: (SUBSETP A A) ; So we'll prove both before trying again. ; [2] (defthm subsetp-cons (implies (subsetp a b) (subsetp a (cons e b)))) ; Succeeded!

; [2] (defthm subsetp-reflexive (subsetp a a)) ; Succeeded!

; [1] ; (defthm main-lemma-1-about-while-loop-version ; (subsetp (while-loop-version x a) (append x a)) ; :hints (("Goal" :induct (while-loop-version x a)))) ; Failed! ; Key Term: ; (IMPLIES (AND ... ; (SUBSETP (WHILE-LOOP-VERSION (CDR X) (CONS (CAR X) A)) ; (APPEND (CDR X) (CONS (CAR X) A)))) ; (SUBSETP (WHILE-LOOP-VERSION (CDR X) (CONS (CAR X) A)) ; (CONS (CAR X) (APPEND (CDR X) A))))

; We'd be done if we could rewrite the ; (APPEND (CDR X) (CONS (CAR X) A)) ; to ; (CONS (CAR X) (APPEND (CDR X) A)) ; These two terms are not equal! But they are ``set-equal'' and this kind of ; rewriting is possible using user-defined equivalences and congruence rules. ; But the new user should not dive into congruences yet. So we will do this ; with ordinary lemmas:

; The plan then is to prove ; (iff (subsetp a (append b (cons e c))) ; (subsetp a (cons e (append b c))))

; Consider the first half of this bi-implication: ; (implies (subsetp a (append b (cons e c))) ; hyp1 ; (subsetp a (cons e (append b c)))) ; concl ; Notice that if we knew ; (subsetp (append b (cons e c)) (cons e (append b c))) ; hyp2 ; then we could use hyp1 and hyp2 together with the transitivity of ; subsetp to get concl.

; The proof in the other direction is comparable but requires the ; (subsetp (cons e (append b c)) (append b (cons e c)))

; Thus, our plan is prove ; (a) transitivity of subsetp ; (b) (subsetp (append b (cons e c)) (cons e (append b c))) ; (c) (subsetp (cons e (append b c)) (append b (cons e c)))

; in order to prove ; (d) (iff (subsetp a (append b (cons e c))) ; (subsetp a (cons e (append b c))))

; [2] (a) (defthm trans-subsetp (implies (and (subsetp a b) (subsetp b c)) (subsetp a c))) ; Succeeded!

; [2] (b) (defthm append-cons-v-cons-append-1 (subsetp (append b (cons e c)) (cons e (append b c)))) ; Succeeded!

; [2] (c) (defthm append-cons-v-cons-append-2 (subsetp (cons e (append b c)) (append b (cons e c)))) ; Succeeded!

; [2] (d) (defthm subsetp-append-cons-cons-append (iff (subsetp a (append b (cons e c))) (subsetp a (cons e (append b c))))) ; Succeeded!

; [1] (defthm main-lemma-1-about-while-loop-version (subsetp (while-loop-version x a) (append x a)) :hints (("Goal" :induct (while-loop-version x a)))) ; Succeeded!

; [0] ; (defthm main-theorem-1-about-while-loop-version ; (subsetp (while-loop-version x nil) x)) ; Failed! [But the truth is below...]

; But we don't submit this because we don't expect it to be proved ; from the main lemma just proved: they don't match! But ; note that if we instantiated the main lemma, replacing a by nil, ; we get:

; (subsetp (while-loop-version x nil) (append x nil))

; and we could simplify the (append x nil) to x in this context, with ; another congruence rule -- if we were using them. So let's prove ; first that we can simplify (append x nil) inside a subsetp:

; [1] (defthm subsetp-append-nil (iff (subsetp x (append y nil)) (subsetp x y))) ; Succeeded!

; and then just tell ACL2 how to use the lemma to get the main theorem. Note ; that we give a hint to instantiate main-lemma-1... but we also disable ; main-lemma-1... because otherwise it will rewrite itself away! Once the ; instance of main-lemma-1... is sitting around as a hypothesis, ; subsetp-append-nil will rewrite the (append x nil) to x for us and finish the ; proof.

; [0] (defthm main-theorem-1-about-while-loop-version (subsetp (while-loop-version x nil) x) :hints (("Goal" :use (:instance main-lemma-1-about-while-loop-version (x x) (a nil)) :in-theory (disable main-lemma-1-about-while-loop-version)))) ; Succeeded!

; Recall that the main-theorem-1... just proved is just half of what we want. ; We also want:

; [0] ; (defthm main-theorem-2-about-while-loop-version ; (not (dupsp (while-loop-version x nil)))) ; Failed! [But the truth is below...]

; But, again, we don't submit that because the nil makes it not general enough for ; induction. Instead we go immediately to:

; [1] (defthm main-lemma-2-about-while-loop-version (implies (not (dupsp a)) (not (dupsp (while-loop-version x a))))) ; Succeeded!

; This time we know our main-lemma-2... will match (there's no (append x nil) ; in there to mess things up) and so we can complete the proof with:

; [0] (defthm main-theorem-2-about-while-loop-version (not (dupsp (while-loop-version x nil)))) ; Succeeded!

;----------------------------------------------------------------- ; Section D: Lessons Learned

; The most obvious lesson is that it is easier to reason about the primitive ; recursive collect-once than about the while-loop-version. Thus, if your only ; need is for a function that collects one occurrence of each element of a list ; and you don't care about the order in which you collect them and you don't ; need it to be very sparing of stack space when it executes, then use the ; primitive recursive definition and don't even think about while loops!

; So why might you be driven to while-loop-version? One possibility is that ; the list you wish to process is very long and the primitive recursive version ; would produce a stack overflow. In ACL2, that would mean the list would have ; to be several thousand long. Is your application really so demanding?

; Another possibility is that you are modeling in Lisp a while loop expressed ; in some other programming language. In that case, the fidelity of your model to ; the artifact being modeled is important and you should use while-loop-version.

; Another possibility is that for some reason order matters and you really are ; interested in collecting the first occurrence rather than the last. Of ; course this is most likely to be relevant in more interesting applications ; where the occurrences are somehow distinguishable.

; If you are forced to deal with the while-loop-version the question is do you ; do an indirect proof as in Section B or a direct proof as in Section C? ; The indirect proof involved 10 theorems and the direct proof involved 11. ; That is not a significant difference.

; But our sense is that the indirect proof is easier to find, once you figure ; out the basic shape of the relation between while-loop-version collect-once. ; In particular, we had to give the theorem prover two hints in the direct ; proof (versus no hints in the indirect proof). One of our hints was about ; what induction to do and the other was about how to use a previously proved ; instance of a lemma involving an accumulator. Furthermore, we had to think ; carefully about the use of the transitivity of subsetp and we had to hack our ; way around rewriting (append a (cons e b)) to (cons e (append a b)) in a ; subsetp-expression.

; Some of these ``set'' problems could have been handled a lot more elegantly by ; defining set-equal as an equivalence relation and proving the congruence ; rules to allow the rewriting of set-equal terms to set-equal terms inside ; certain expressions like subsetp and member. However, that involves a lot of ; overhead in the form of congruence rules showing that set-equality is ; maintained by replacement of set-equals by set-equals in various argument ; positions of the various functions. See :doc congruence. In general, we ; find congruence-based reasoning extremely neat and powerful when the ; appropriate infrastructure has been built up. But because the infrastructure ; is ``heavy'' we tend not to invest in it for small projects.

; In summary, different users might take home different lessons about whether a ; direct or indirect proof is better here. This is in part due to the ; complexity of the functional relationship between collect-once and ; while-loop-version, which additionall involved append, list-minus, and rev. ; Had the relationship been simpler, the indirect proof would have been ; preferred.

; An undeniable lesson, however, is that it is helpful to know both styles of ; proof and to be able to explore both as needed in your applications.

Use your browser's Back Button now to return to introductory-challenge-problem-4.