##### Child pages
• CSU 290 (Spring 2008)
Go to start of banner

# CSU 290 (Spring 2008)

You are viewing an old version of this page. View the current version.

Version 17

This is the resource page for Section 2 of Logic and Computation, CSU 290 (Spring 2008). Welcome.

The purpose of this page is to simply complete the lectures, partly to keep us in sync with the other section, partly because there is only so much time and much interesting things to discuss.

These notes are meant to supplement the lectures, the readings and the lecture notes (available on the course web site) and not replace them.

Feel free to comment at the bottom of the page if you have any questions or anything is unclear. (Select "Add Comment" under "Add Content" down below.) I will generally merge your comments and questions into the main text when I get to them.

What you should do right now: Make sure you are logged into the wiki. (Select "Log In" under "Your Account" below if not, use you CCIS credentials to log in.) Look at the bottom of the page, under "Other Features", top right icon is an envelope. Press that button. That will register you as watching this page. Basically, every time the page changes, you get an email saying so. Keeps you from having to check the page regularly.

## February 15: Formal proof for `~(t=nil)`

Here is a complete formal proof for `~(t=nil)` using the axioms I gave in class. (This is the proof that I started on Monday, but ran out of time 5 steps into the proof.)

You should be able to understand all the steps in that proof, if not be able to come up with the proof yourself. We will not ask you to come up with these kind of formal proofs in this course. Instead, we will use the more informal "chain of equals" or "chain of relations" approach I presented in class.

In the proof below, I write `~A` for "not `A`" and `A & B` for "`A` and `B`".

```1. ~(equal (car (cons x y) x) x) = nil        [ axiom for equal/car/cons
2. ~(equal (car (cons a b) a) a) = nil)       [ instantiation of 1 with x->a, y->b
```

Okay, so 2 is something we will use later on. Let's remember it.

```3. ~(x = y) => (equal x y) = nil              [ axiom for equal
4. ~(car (cons a b)) = a =>
(equal (car (cons a b)) a) = nil     [ instantiation of 3 with x->(car (cons a b)), y->a
5. ~(~(car (cons a b)) = a)                   [ by MT applied to 4 and 2
6. ~(~A) => A                                 [ tautology of Boolean logic
7. ~(~(car (cons a b)) = a) =>
(car (cons a b)) = a                    [ instantiation of 6 with A->(car (cons a b))
8. (car (cons a b)) = a                       [ by MP applied to 7 and 5
9. x=y => (equal x y) = y                     [ axiom for equal
10. (car (cons a b)) = a =>
(equal (car (cons a b)) a) = t        [ instantiation of 9 with x->(car (cons a b)),y->a
11. (equal (car (cons a b)) a) = t            [ by MP applied to 10 and 8
```

Excellent. So now we can see that 2 and 11 together essentially give us that `t /= nil`.
We just have to work some to get that to come out of the proof.

```12. (A & B => C) => (A & ~C => ~B)            [ tautology of Boolean logic (check it!)
13. ((equal (car (cons a b)) a) = t
& t = nil =>
(equal (car (cons a b)) a) = nil))  [ instantiation of 12 with
=> ((equal (car (cons a b)) a) = t       [  A->(equal (car (cons a b)) a) = t
& ~(equal (car (cons a b)) a) = nil)) [  B->(t=nil)
=> ~(t = nil))                   [  C->(equal (car (cons a b)) a) = nil
14. x = y & y = z => x = z                    [ axiom of transitivity
15. (equal (car (cons a b)) a) = t            [ instantiation of 14 with
& t = nil =>                             [   x->(equal (car (cons a b)) a)
(equal (car (cons a b)) a) = nil     [   y->t  and z->nil
16. (equal (car (cons a b)) a) = t            [ by MP applied to 13 and 15
& ~(equal (car (cons a b)) a) = nil
=> ~(t = nil)
```

And we are just about done. We have both antecedents of the implication in 16.

```17. ~(t=nil)                                  [ by MP applied to 16, 11, and 2
```

## February 20: Review of Proving Theorems

Here is what we have seen until now as far as proving theorems are concerned.

We reviewed Boolean logic and the concept of a tautology of Boolean logic. Recall that a tautology is just a Boolean formula that is always true, irrespectively of the truth values you give to the Boolean variables in the formula.

We shall take Boolean (aka propositional) reasoning for granted.

We introduced the ACL2 logic, as essentially Boolean logic augmented with the ability to reason about basic formulas of the form EXP1 = EXP2, where EXP1 and EXP2 are ACL2 expressions.

The ACL2 logic is defined formally using the following axioms and rules of inferences:

• Every tautology of Boolean logic is an axiom
• `x = x` (reflexivity)
• `x = y => y = x` (symmetry)
• `x = y /\ y = z => x = z` (transitivity)
• `x1 = y1 /\ ... /\ xn = yn => (f x1 ... xn) = (f y1 ... yn)` for every function symbol `f` of arity n
• From `F` derive `F`/s (where `F` is an arbitrary formula, and s is a substitution of ACL2 expressions for free variables in `F`)
• From `F` and `F => G` derive `G` (modus ponens)

We also have axioms corresponding to the different primitives of the language. The basic ones are:

• `x = y => (equal x y) = t`
• `x /= y => (equal x y) = nil` (where `/=` represents "not equal to")
• `x = nil => (if x y z) = z`
• `x /= nil => (if x y z) = x`
• `(car (cons x y)) = x`
• `(cdr (cons x y)) = y`
• `consp (cons x y) = t`
• `consp (nil) = nil`
• `(integerp x) = t => (consp x)=nil`

Additionally, we can take as axioms the basic properties of arithmetic that you have seen in high school and discrete maths. Thus, when proving theorems, you will be free to perform standard simplification of arithmetic.

A formal proof of `F` is a sequence of formulas such that every formula in the sequence is either an axiom or derived from previous formulas using an inference rule, and such that the last formula of the sequence is `F` itself.

I gave you a formal proof of `t /= nil` above. We shall not write a lot of formal proof, and you certainly will not have to write any of them.

Instead, we shall use some proof techniques to prove formulas that have a certain form. These techniques do not work for all formulas, but they work for enough of them to be interesting.

If the formula you want to prove is of the form EXP1 = EXP2, then one technique is to use a sequence of equalities to equate EXP1 and EXP2:

```   EXP1
=
....
=
EXP2
```

where all the equalities are justified using the axioms or the inference rules above. By transitivity of =, this clearly shows that EXP1 = EXP2.

Alternatively, it is sometimes easier to simplify both EXP1 and EXP2 into a common expression EXP3.

```   EXP1
=
...
=
EXP3

EXP2
=
...
=
EXP3
```

Again, it should be pretty clear that this proves that EXP1 = EXP2. (By transitivity of = once again.)

If the formula you want to prove is of the form HYPS => EXP1 = EXP2, where HYPS is some hypotheses (formulas), then you can use a variant of the above, that is, prove that EXP1 = EXP2 using a sequence of equalities (or simplify EXP1 and EXP2 to the same expression EXP3), except that when you derive the equalities you can in addition to the axioms and inference rules use the hypotheses in HYPS to help you.

## February 25: Solutions to Exam 3 Proof Questions

Recall the definitions:

```(defun add99 (l)
(if (endp l)
nil
(cons (cons 99 (car l)) (add99 (cdr l)))))

(defun same-lenp (l n)
(if (endp l)
t
(if (equal (len (car l)) n)
(same-lenp (cdr l) n)
nil)))

(defun len (l)
(if (endp l)
0
(+ 1 (len (cdr l)))))
```

Question 2. Prove `(consp x) => (len (car (add99 x))) = (+ (len (car x)) 1)`

```  (len (car (add99 x)))
(len (car (if (endp x) nil (cons (cons 99 (car x)) (add99 (cdr x))))))
=   { (endp x) = nil }
(len (car (cons (cons 99 (car x)) (add99 (cdr x)))))
=   {car-cons axiom}
(len (cons 99 (car x)))
=   {def of len }
(if (endp (cons 99  (car x)))
0
(+ 1 (len (cdr (cons 99 (car x))))))
=   {endp(cons ...)=nil}
(+ 1 (len (cdr (cons 99 (car x)))))
=  {cdr-cons axiom}
(+ 1 (len (car x)))
```

Question 3. Prove `(endp x) => (same-lenp (add99 x) y)`

```  (same-lenp (add99 x) (+ y 1))
(same-lenp (if (endp x) nil ...))
=   { (endp x) = t }
(same-lenp nil (+ y 1))
=   {def of same-lenp }
(if (endp nil) t ...)
=   { (endp nil)=t }
t
```

Question 4. Prove `((same-lenp x y) /\ (consp x) /\ (same-lenp (add99 (cdr x)) (+ y 1))) => (same-lenp (add99 x) (+ y 1))`

(You can use the fact that `((same-lenp x y) /\ (consp x)) => (+ 1 (len (car x))) = (+ 1 y)`.)

```  (same-lenp (add99 x) (+ y 1))
(same-lenp (if (endp x) nil (cons (cons 99 (car x)) (add99 (cdr x)))) (+ y 1))
=   {(endp x) = nil}
(same-lenp (cons (cons 99 (car x)) (add99 (cdr x))) (+ y 1))
=    {def of same-lenp}
(if (endp (cons (cons 99 (car x)) (add99 (cdr x))))
t
(if (= (len (car (cons (cons 99 (car x)) (add99 (cdr x))))) (+ y 1))
(same-lenp (cdr (cons (cons 99 (car x)) (add99 (cdr x)))) (+ y 1))
nil))
=  { (endp (cons ...)) = nil }
(if (= (len (car (cons (cons 99 (car x)) (add99 (cdr x))))) (+ y 1))
(same-lenp (cdr (cons (cons 99 (car x)) (add99 (cdr x)))) (+ y 1))
nil)
=  { car-cons and cdr-cons axioms}
(if (= (len (cons 99 (car x))) (+ y 1))
(same-lenp (add99 (cdr x)) (+ y 1))
nil)
=   { def of len }
(if (= (if (endp (cons 99 (car x)))
0
(+ 1 (len (cdr (cons 99 (car x)))))) (+ y 1))
(same-lenp (add99 (cdr x)) (+ y 1)))
nil)
= { (endp (cons ...)) = nil
(if (= (+ 1 (len (cdr (cons 99 (car x)))) (+ y 1)))
(same-lenp (add99 (cdr x)) (+ y 1)))
nil)
= { car-cons axiom }
(if (= (+ 1 (len (car x)) (+ 1 y)))
(same-lenp (add99 (cdr x)) (+ y 1)))
nil)
=  { by fact above }
(same-lenp (add99 (cdr x)) (+ y 1))
=  { by hypothesis }
t
```

## February 26: On Termination

Here is the argument that, at least in Scheme, it is impossible to write a function that correctly determines whether another function terminates. We argue by contradiction.

Suppose that we could write a function (terminates? f x) that returns #t (true) when (f x) terminates, and returns #f (false) when (f x) does not terminate. I don't care how terminates? is written - just suppose you managed to write it. I will not show that you get something completely absurd as a result.

In particular, I can now write the following perfectly legal scheme function:

```  (define (loop-forever) (loop-forever))

(define (oops f x)
(if (terminates? oops empty)
(loop-forever)
1))
```

My question: does (oops empty) terminate?

Let's see. It either does or doesn't. So let's consider both cases.

1. If (oops empty) terminates, then by assumption (terminates? oops empty) must be true, and therefore, by the code of oops, (oops empty) must loop forever, i.e., not terminate, contradicting that (oops empty) terminates.
1. if (oops empty) does not terminate, then by assumption (terminates? oops empty) must be false, and therefore by the code of oops, (oops empty) returns 1 immediately, i.e., terminates, contradicting that (oops empty) does not terminate.

Because we get a contradiction no matter what, it must be that what we initially assumed was wrong, i.e., that terminates? works correctly. In other words, we cannot write a correct terminates? function in Scheme.

It turns out that this argument can be made to work for any programming language, but you'll have to wait for CSU 390 to see it.

One approach to arguing that a recursive function terminates is to argue that we are making progress towards the base case. The design recipe that we gave you and that you learned in 211 is in fact meant to ensure exactly this, at least for the kind of functions that we have seen until now.

It is sometimes a little subtle to argue that you are indeed making progress towards termination. In particular, consider the following ACL2 function:

```  (defun foo (n)
(cond ((zp n) 0)
((= n 1) 1)
((evenp n) (foo (/ n 2)))
((oddp n) (foo (+ n 1)))))
```

(Assume that we have correct definitions for `evenp` and `oddp`.) I claim that this terminates for all inputs. Clearly, if the input is not a natural number, it terminates immediately. (Why?) Otherwise, can we argue that we are making progress towards the base cases (either 0 or 1)? Here are two arguments: the first, the one raised in class on Monday, is that that even though when the input is odd the recursive call is done on a bigger number (that therefore does not immediately progresses towards the base case), (foo (+ n 1)) will be called on an even number, which means that at the following iteration, foo will be invoked on (/ (+ n 1) 2), which is smaller than n. Thus, even though at every step we are not making progress towards the base case, we are making progress towards the base case at either every step or every two steps, depending on whether the input is even or odd. That's actually sufficient to establish termination. (Why?)

Here's another way of thinking about it that actually shows progress towards termination at every step. Write the input as a binary number, and consider the following number <n> derived from the input n:

```  <n> = the number of 1's in the binary representation of n
+ 2 * (the length of n in binary)
- the number of 0's in the binary representation of n to the right of the rightmost 1.
```

Thus,

• 4 is 100 in binary, so <4> = 1 + (2 * 3) - 2 = 1 + 6 - 2 = 5,
• 5 is 101 in binary, so <5> = 2 + (2 * 3) - 0 = 2 + 6 = 8,
• 6 is 110 in binary, so <6> = 2 + (2 * 3) - 1 = 2 + 6 - 1 = 7.

You can check that at every recursive step of the function, foo is called on an input n whose <n> is strictly smaller than the original input to the function. And we are making progress towards the base case, with <1> = 3.

However, this doesn't work for every function, even those that look similar to the above. One of the most famous examples is the following so-called Collatz function:

```  (defun collatz (n)
(cond ((zp n) 0)
((= n 1) 1)
((evenp n) (collatz (/ n 2)))
((oddp n) (collatz (+ (* 3 n) 1)))))
```

It is unknown whether this function terminates on all inputs. Plot out a few calls for small values of n, and you'll get a sense for how long some of the iterations take. Termination has been checked for all natural numbers up to 10^16, but for all we know it fails to terminate on some input greater than 10^17. We just don't know enough about number theory to say anything else.

I posted some more mathematical references to this function here.

## February 27: The Final Bit of the Proof that `(len (add1 a)) = (len a)`

I went over it quickly at the end of lecture today, so it pays to look at it more slowly.

Recall the definitions I gave in class:

```  (defun len (l)
(if (endp l)
0
(+ 1 (len (cdr l)))))

(if (endp l)
l
(cons (+ 1 (car l))
```

I claimed that in order to prove that `(len (add1 a)) = (len a)`, it suffices to prove the following two theorems:

```  Add1EndP:    (endp a) => (len (add1 a)) = (len a)

Add1ConsP:   (consp a) /\ (len (add1 (cdr a))) = (len (cdr a)) => (len (add1 a)) = (len a)
```

We saw that these are actually quite easy to prove. (Do it again if you're shaky on the details.)

We need to argue, though, that `Add1EndP` and `Add1ConsP` together give you that `(len (add1 a)) = (len a)` for all possible `a` in the ACL2 universe.

We are going to argue this by contradiction. So assume that it is not the case that `(len (add1 a)) = (len a)` for all `a`.

Let A be the set of all values in the ACL2 universe for which `((len (add1 a))` is not equal to `(len a)`. By assumption, A is not empty.

Let `s` be an element of A that has smallest length amongst all elements of A. (If more than one element has smallest length, pick any of them.)

First off, we know that `s` cannot be an atom. Why? Because we proved `Add1EndP`, which says that atoms cannot be in A. So `s` must be a cons pair, that is, `(consp s) = t`.

Because `s` is a cons pair, `(cdr s)` must have length strictly less than that of `s`. Because `s` was chosen to have smallest length in A, then `(cdr s)` cannot be in A.

But because `(cdr s)` is not in A, then it must be that `(len (add1 (cdr s))) = (len (cdr s))`. (Recall how we defined A in the first place.)

So we know that `(consp s) = t`, and that `(len (add1 (cdr s))) = (len (cdr s))`. But we proved `Add1Consp`, which says that it must be the case that `(len (add1 s)) = (len s)`. And this contradicts the fact that `s` is in A in the first place!

We derive a contradiction, so what we assumed in the first place must be false - so there cannot be any `a` such that `(len (add1 a))` is not equal to `(len a)`, that is, `(len (add1 a)) = (len a)` for all `a` in the universe.

## March 11: Some Easy Proof Exercises

Consider the following definitions:

```(defun andp (x y)
(if x
(if y t nil)
nil))

(defun orp (x y)
(if x
t
(if y t nil)))
```

Make sure you understand them. You can assume the following properties about `andp` and `orp`:

```  (booleanp (andp x y))
(andp x y) = (andp y x)
(booleanp (orp x y))
(orp x y) = (orp y x)
```

EDIT: Let me add the following properties that you can assume too:

```  (booleanp z) => (andp z t) = z
(booleanp z) => (andp t z) = z
(booleanp z) => (orp z nil) = z
(booleanp z) => (orp nil z) = z
```

(You can give a shot at proving these, but the proof is a bit different than what we're used to - it requires case analysis, that you can do using only Boolean reasoning. For now, just take the properties above as true. I will talk about the proof technique for this in some other entry below.)

Now, let's write a function that applies `andp` to a list of values, so that it returns true exactly when not all values in the list are nil.

```(defun and-foldp (l)
(if (endp l)
t
(andp (car l) (and-foldp (cdr l)))))
```

Note what happens when we evaluate `(and-foldp '())`, or when we evaluate `(and-foldp 5)`, or when we evaluate `(and-foldp '(t))`.

Let's do something similar for `orp`:

```(defun or-foldp (l)
(if (endp l)
nil
(orp (car l) (or-foldp (cdr l)))))
```

Again, note what happens when we evaluate `(or-foldp '())`, or when we evaluate `(or-foldp 5)`, or when we evaluate `(or-foldp '(t))`.

Let's prove some theorems. (Many of these are proof obligations obtained from the induction principle - can you identify the formula that these proof obligations end up proving?) Make sure that you understand what the theorem you are trying to prove is actually saying! Otherwise, this exercise is just meaningless symbol pushing, which is going to be excruciatingly boring.

• `(endp x) => (booleanp (and-foldp x))`
• `(consp x) & (booleanp (and-foldp (cdr x))) => (booleanp (and-foldp x))`
• `(endp x) => (booleanp (or-foldp x))`
• `(consp x) & (booleanp (or-foldp (cdr x))) => (booleanp (or-foldp x))`
• `(and-foldp (cons a b)) = (andp a (and-foldp b))`
• `(or-foldp (cons a b)) = (orp a (or-foldp b))`

Prove the following theorems, using the standard definition of `app`:

```(defun app (x y)
(if (endp x)
y
(cons (car x) (app (cdr x) y))))
```
• `(endp x) => (and-foldp (app x y)) = (andp (and-foldp x) (and-foldp y))`
• `(consp x) & (and-foldp (app (cdr x) y)) = (andp (and-foldp (cdr x)) (and-foldp y)) => (and-foldp (app x y)) = (andp (and-foldp x) (and-foldp y))`
• `(endp x) => (or-foldp (app x y)) = (orp (or-foldp x) (or-foldp y))`
• `(consp x) & (or-foldp (app (cdr x) y)) = (orp (or-foldp (cdr x)) (or-foldp y)) => (or-foldp (app x y)) = (orp (or-foldp x) (or-foldp y))`

EDIT: note that proving the above gives you that the following two theorems are true:

• `(and-foldp (app x y)) = (andp (and-foldp x) (and-foldp y))`
• `(or-foldp (app x y)) = (orp (of-foldp x) (or-foldp y))`

These may come in handy in the proofs of the next theorems.

For the next few theorems, you will want to prove the following lemma first:

• `(consp x) => (app (list (car x)) (cdr x)) = x`

(Recall that `(list z)` is just an abbreviation for `(cons z nil)`. You can use the following axiom for cons: `(consp x) => (cons (car x) (cdr x)) = x`.)

Use the standard definition of `rev`:

```(defun rev (x)
(if (endp x)
nil
(app (rev (cdr x)) (list (car x)))))
```

Prove the following theorems:

• `(endp x) => (and-foldp (rev x)) = (and-foldp x)`
• `(consp x) & (and-foldp (rev (cdr x))) = (and-foldp (cdr x)) => (and-foldp (rev x)) = (and-foldp x)`
• `(endp x) => (or-foldp (rev x)) = (or-foldp x))`
• `(consp x) & (or-foldp (rev (cdr x))) = (or-foldp (cdr x)) => (or-foldp (rev x)) = (or-foldp x)`

## March 17: Proof of `(true-listp x) => (rev (rev x)) = x`

Here is the proof of `(true-listp x) => (rev (rev x)) = x` that I did in class, complete with all auxiliary lemmas.

We proceed by induction on `x`, meaning that we have to prove the following proof obligations:

• `(endp x) & (true-listp x) => (rev (rev x)) = x`
• `(consp x) & ((true-listp (cdr x)) => (rev (rev (cdr x))) = (cdr x)) & (true-listp x) => (rev (rev x)) = x`

Proof of `(endp x) & (true-listp x) => (rev (rev x)) = x`:

```  (rev (rev x))
=  { by def of rev, if axiom, hypothesis}
(rev nil)
=  { by def of rev }
nil
=  { by lemma 1 below, (endp x) & (true-listp x) => x = nil }
x
```

This proof relies on the following lemma, which says that if we have a true list and it is an atom, then it must be `nil`. Note that we use this lemma to add a new result to the context, that is, to the set of known facts that include the hypotheses.

Proof of Lemma 1: `(endp x) & (true-listp x) => x = nil`

```We first prove that (endp x) & (true-listp x) => (equal x nil) = t:

t
=  {by hypothesis}
(true-listp x)
=  {by def of true-listp, hypothesis, if axiom}
(equal x nil)

This gives us that (equal x nil) = t, that is, x = nil (by the equal axiom).
```

Back to the main proof, where we now have to prove the inductive case:

Proof of `(consp x) & ((true-listp (cdr x)) => (rev (rev (cdr x))) = (cdr x)) & (true-listp x) => (rev (rev x)) = x`:

```  (rev (rev x))
=  {by def of rev, hypothesis, if axiom}
(rev (app (rev (cdr x)) (list (car x))))
=  {by Lemma 2 below}
(app (rev (list (car x))) (rev (rev (cdr x))))
=  {by induction hypothesis, knowing that (true-listp (cdr x)) using Lemma 3 below}
(app (rev (list (car x))) (cdr x))
=  {by def of rev}
(app (list (car x)) (cdr x))
=  {by def of app}
(cons (car x) (cdr x))
=  {by cons axiom, knowing that (consp x)}
x
```

The proof uses two lemmas. Let's take care of the third lemma first.

Proof of Lemma 3: `(true-listp x) & (consp x) => (true-listp (cdr x))`:

```  t
=  {by hypothesis}
(true-listp x)
=  {by def of true-listp, hypothesis, if axiom}
(true-listp (cdr x))
```

That was easy. Slightly harder is lemma 2, the one that Eric proved on the board today.

Proof of Lemma 2: `(rev (app a b)) = (app (rev b) (rev a))`:

```By induction on a. (Why?)

Base case: (endp a) => (rev (app a b)) = (app (rev b) (rev a))

Let's prove that both sides equal a common value.

(rev (app a b))
=  {by def of app, hypothesis, if axiom}
(rev b)

(app (rev b) (rev a))
=  {by def of rev, hypothesis, if axiom}
(app (rev b) nil)
=  {by lemma (true-listp x) => (app x nil) = x, knowing that (rev b) is a true-listp by Lemma 4 below}
(rev b)

Inductive case: (consp a) & (rev (app (cdr a) b)) = (app (rev b) (rev (cdr a))) =>
(rev (app a b)) = (app (rev b) (rev a))

Again, we prove that both sides equal a common value.

(rev (app a b))
=  {by def of app, hypothesis, if axiom}
(rev (cons (car a) (app (cdr a) b)))
=  {by def of rev, endp axiom, if axiom, car-cons axiom}
(app (rev (app (cdr a) b)) (list (car a)))
=  {by induction hypothesis}
(app (app (rev b) (rev (cdr a))) (list (car a))

(app (rev b) (rev a))
=  {by def of rev, hypothesis, if axiom}
(app (rev b) (app (rev (cdr a)) (list (car a))))
=  {by lemma (app a (app b c)) = (app (app a b) c)}
(app (app (rev b) (rev (cdr a))) (list (car a)))
```

All that's missing now is Lemma 4, that says that `(rev b)` is a true list for any `b`. I left it as an exercise for you to prove for next time.

• No labels