Reading:
Rosen 3.6 (program correctness), 2.5 (first part: integer representations)
Numbers vs Numerals
Number is an abstract idea;
Numeral is an encoding of a number as a string.
(Roman numerals, base-10 numerals, ...).
When necessary add brackets and base around a numeral.
(And, we'll write numbers as base-10 numerals w/o brackets...)
This is the same old semantics/syntax distinction (quoting).
How to convert?
471 = 4*10^2 + 7*10 + 1
What is [471]8 ?
[471]8 = 4*8^2 + 7*8^1 + 1*8^0 = 313
How to represent 313 in base 8?
We just saw the answer, 313.
How to calculate this if we didn't know the answer?
313 = 4*64 + 57
= 4*46 + 7*8 + 1*1
= [471]8
How to represent the number 313 in binary?
313 = 1*256 + 57
= 1*256 + 0*128 + 0*64 + 1*32 + 19
= 1*256 + 0*128 + 0*64 + 1*32 + 1*16 + 0*8 + 0*4 + 1*2 + 1*1
= [100 110 011]2
Hmm, note that each group of three binary digits corresponds to a single
octal digit.
Separately:
Given a decimal number, how do you multiply by 10, or divide by 10?
For a binary: What is [110 110 011]2 doubled, and written
(again) in binary? What is it halved, rounded down?
This observation leads to
a bottom-up way of expressing (say) 41 in binary (generalizes to any base):
Note that 41 is odd; what is the last digit of its binary representation?
Has to be 1!
And, all the other digits will be the representation of 40/2 = 20,
since we can take that numeral and append a 1, to get twice 20 plus 1.
Repeating the process:
41 = 20*2 + 1
= (10*2+0)*2 + 1
= 10*2^2 + 0*2 + 1
= (5*2+0)*2^2 + 0*2 + 1
= 5*2^3 + 0*2^2 + 0*2 + 1
= (2*2+1)*2^3 + 0*2^2 + 0*2 + 1
= 2*2^4 + 1*2^3 + 0*2^2 + 0*2 + 1
= (1*2+0)*2^4 + 1*2^3 + 0*2^2 + 0*2 + 1
= 1*2^5 + 0*2^4 + 1*2^3 + 0*2^2 + 0*2 + 1
= 1*2^5 + 0*2^4 + 1*2^3 + 0*2^2 + 0*2^1 + 1*2^0
= [101001]2.
Thus we can go left-to-right or right-to-left: Either
- start with 41 and look for how many big powers of two it has
(how many 32, how many 16s, ...) and
whittle down the amount left over,
or
- ask whether 41 is even/odd, write down the least-significant digit,
and then recur on half our number (rounded down),
prepending that sub-answer to our overal numeral.
To yourself, compare each of these algorithms in base 10.
Btw, "bit" comes from "binary digit".
Proofs about programs
;; rdigs->int: (listof real, int) → real
;; Return the number represented by d,
;; a list of digits base b, reversed.
;;
;; Test cases:
;; (rdigs->int (list 1 7 4)) = 471 ; Using default b=10.
;; (rdigs->int (list 1 7 4) 8) = 313
;; ; Since for base 8, 4*64 + 7*8 + 1 = 313.
;;
(define/opt (rdigs->int d [b 10])
(cond [(empty? digs) 0]
[(cons? digs) (+ (first digs) (* b (rdigs->int (rest d) b)))]))
;
; Note: for allowing optional arguments in scheme via "define/opt",
; see the teachpack-demo page,
; http://www.owlnet.rice.edu/~comp210/02fall/Handouts/teachpack-demo.shtml
; and follow the link at the bottom of the page for "optional arguments".
Let rd2i be the function:
rd2i(d,b) = sum{i}_0^{n-1} (d_i * b^i) (where n is d.length())
where d = (list d_0 d_1 d_2 ... d_(n-1) d_n)
(_ means subscript, and ^ means superscript)
Proof goal:
For a list of digits d, let P(d) be
"(rdigs->int d b) = rd2i(d,b) = sum{i}_0^{n-1} (d_i * b^i)"
(for arbitrary b>0)
Proof, by structural induction on d.
case 1: d is empty; (rdigs->int empty) = 0 = sum{i}_0^{-1} holds.
case 2: d is (cons d_0 r), where r = (list d_1 ... d_{n-1}).
We have:
(rdigs->int d)
= (+ (first d) (* b (rdigs->int (rest d)))) ; By code.
= (+ d_0 (* b rd2i((rest d)))) ; Ind.hyp.
= (+ d_0 (* b sum{i}_0^{(n-1)-1} d_(i+1)*b^i)) ; By def'n of rd2i.
= (+ d_0 (* b sum{j}_1^{n-1} d_j *b^{j-1})) ; Re-index[i/j-1].
= (+ d_0 sum{j}_1^{n-1}d_j b^j) ; Distribute b*.
= (+ (* d_0 b^0) sum{j}_1^{n-1}d_j*b^j) ; Mult d_0 by 1.
= sum{j}_0^{n-1} d_j*b^j. ; Sum notation.
= rd2i(d) ; By def'n of rd2i.
which is P(d), QED!
In retrospect, we could break this proof into two parts:
in the math domain,
show that rd2i as defined by the summation
is the same as rd2i defined recursively;
then in the program domain (which mirrors the recursive def'n of rd2i)
we just leverage off the previous result.
Thus the above could all be re-stated:
Lemma: For d a non-empty list of digits, rd2i(d) = (first d) + b*rd2i((rest d)).
Proof of lemma:
rd2i(d)
= sum{j}_0^{n-1} d_j*b^j. ; By def'n of rd2i.
= d_0*b^0 + sum{j}_1^{n-1}d_j*b^j ; Sum notation.
= d_0 + sum{j}_1^{n-1}d_j b^j) ;
= d_0 + b*sum{j}_1^{n-1} d_j *b^{j-1})) ; Factor out a b.
= d_0 + b*sum{i}_0^{(n-1)-1} d_(i+1)*b^i)) ; Re-index[j/i+1].
= d_0 + b*rd2i((rest d)))) ; By def'n of rd2i.
Proof of P(d) = "rd2i(d) = (rdigs->int d)", by structural induction
on d (a list of digits).
case 1: d is empty; (rdigs->int empty) = 0 = sum{i}_0^{-1} holds.
case 2: d is (cons d_0 r), where r = (list d_1 ... d_{n-1}).
We have:
(rdigs->int d)
= (first d) + b*(rdigs->int (rest d)))) ; By code.
= (first d) + b*rd2i((rest d)))) ; Ind.hyp.
= rd2i(d) ; By lemma.
Reached here 04.mar.18,
having also finished up structural induction.
An iterative version:
/* rdigs->int:
* rdigs->int: (listof real, int) → real
* Return the number represented by d,
* a list of digits base b, reversed.
*
*/
real rdigs->int( (listof real) d, int b=10 ) {
real sumSoFar ← 0;
for (int i <- 0; i < d.length(); i <- i+1 )
sumSoFar ← sumSoFar + (list-ref d i) * pow(b,i)
return sumSoFar;
}
/* Test cases:
* List A ← new Cons( 1, new Cons( 7, (new Cons( 4, theEmptyList))));
*
* // Using default b=10:
* unless (rdigs->int(A) == 471) { println("Test case 1 failed."); };
* // Since for base 8, 4 + 7*8 + 1*64 = 313:
* unless (rdigs->int(A, 8) == 289) { println("Test case 2 failed."); };
*/
Our loop invariant L:
sumSoFar = sum{k}0i-1 dk*bk
Our goal G:
sumSoFar = sum{k}0d.length()-1 dk*bk
Our loop-test C: 0 ≤ i < d.length();
Here's a rule of inference requiring four premises (our "proof obligation"):
- L holds initially (precondition)
- L∧C → L′ (loop invariant maintained)
- L∧¬C → G (correctness)
- ¬C will eventually hold. (termination)
Lemma: d = d′; b = b′.
Corollary: d.length() = d′.length(); d.i = d′.i for all i
Show:
(0) [precondition] Check.
(1) [loop invariant maintained]
Suppose L ∧ C: That is, we know.
sumSoFar = sum{k}_0^{i-1} d_k*b^k i ≤ d.length();
(L is sometimes called the "loop hypothesis", analagous to "inductive hyp.")
Then:
sumSoFar′ = sumSoFar + d_i*b^i
= sum{k}_0^{i-1} d_k*b^k + d_i*b^i
= sum{k}_0^{i} d_k*b^k;
= sum{k}_0^{i′-1} d_k*b^k;
which is exactly L′, like we want.
[Hmm, we didn't actually ever use C at all, oh well.]
(2) [correctness] L∧¬C → G
If ¬C, then i-1 = d.length()-1, and so L = G, voila.
Hmmm, wait a sec -- how dis we have i-1 = d.length()?
But ¬C actually only gives us i≥d.length(),
instead of equality.
We want to exclaim "but of course i=d.length(); it
starts at 0 and we stop as soon as we hit d.length()!"
Well, if it's so clearly true, maybe we should deign to
mention the fact.
Let's make our loop invariant stronger -- make it L∧L2,
where L2="i≤d.length()".
Now, correctness is easy -- out of (L ∧ L2) ∧ ¬C,
L2∧¬C gives us i=d.length(); that and L gives
us G.
Of course, having boosted our loop invariant, we need to go back
and show that (0) L2 is also true as a precondition, and
that (1) it's maintained. This is pretty easy (and as a matter of
fact, in (1) we now need to appeal to C holding).
Also, it requires knowing that length() always returns a
non-negative value (which presumably you'd do by looking at that
code and proving about it).
(3) [termination]
Initially i=0, and increases by a constant at each iteration,
thus it will eventually become greater than d.length().
[This is a standard math th'm; provable by a variant of induction
(though if that constant isn't an integer, you need to be a
bit clever about the induction).]
When writing proofs, it's not unusual to discover snags, and need
to go back and patch them up; your first write-up is rarely your last.
You're encouraged to write up your proofs in a word processor, to
avoid having to re-copy your homework.
Reflecting a moment, on the style of proof:
Programs that respect the intrinsic structural definition of their data
(the "natural recursion" of Comp210)
are amenable to proofs by induction.
Programs which need to change state are harder to reason about (partly
because our mathematics doesn't naturally reaon about time and state
(change-over-time).
Especially when the state is spread out over different functions
(and if a function doesn't document what state it's modifying,
things are bleak indeed).
Such code is not only harder to reason about, it tends to be
far more bug-prone.
A good programmer is aware of when a functional (state-free) approach
is good, and when keeping state is appropriate;
this is an important distinction you probably didn't realize you
were learning in Comp210/How to Design Programs.
More examples: The other direction
;; int->rdigs: integer, [int] → (listof int)
;; Return a list of digits base b, reversed.
;;
(define/opt (int->rdigs n [b 10])
(cond [(zero? n) empty]
[(positive? n) (cons (remainder n b) (int->rdigs (quotient n b) b))]))
; Test cases:
;
(int->rdigs 471) = (list 1 7 4) ; Using default b=10.
(int->rdigs 289 8) = (list 1 7 4)
; Since for base 8, 4 + 7*8 + 1*64 = 289.
; (Hmm, 289 = 17^2 and 2^2 = 4, hmm 1,7,4... Numerology *is* real!)
Prove: rd2i((int->rdigs n b), b) = n.
Proof by strong induction(*) on n:
*base case, n=0: Check.
*inductive step, n>0
which can be expressed n=b*m+j, where 0≤j<b and 0≤m<n;
note that (remainder n b) = j and (quotient n b) = m.
rd2i((int->rdigs n))
= rd2i((cons (remainder n b) (int->rdigs (quotient n b) b))) ; By code.
= (remainder n b) + b*rd2i((int->rdigs (quotient n b) b))) ; Lemma above.
= (remainder n b) + b*(quotient n b) ; Ind.Hyp.
= n. ; Thm:Rem/Quot.
(*)This proof by strong induction is kinda a proof by
(regular ol') math induction on the number of base-b digits of n.
That's kinda what we're proving, in fact.
Now let's show the same thing for an imperative version.
This version, unlike the above, is tail-recursive (ie corresponds
to an accumulator-style program).
List int2digs( int n, int b = 10 ) {
List soln = theEmptyList();
while (n > 0) {
soln ← new Cons( remainder(n,b), soln );
n ← quotient(n,b)
}
return soln;
}
What is the statement of correctness?
What will the loop invariant be?
attempt 1
Trying to phrase these is difficult; we'll tweak our program
so that these things can be stated better.
List int2digs( int n, int b = 10 ) {
List soln = theEmptyList();
int m = n;
int loops = 0; // A dummy variable to help state loop invariant.
// Just counts how many times we've executed the loop.
while (m > 0) {
soln ← new Cons( remainder(m,b), soln );
m ← quotient(m,b)
loops ← loops + 1;
}
return soln;
}
/* Test cases:
*
* unless ((int->rdigs(471)).equals( new Cons( 4, new Cons( 7, new Cons( 1, theEmptyList()))) )) {
* printf("test case failed: int->rdigs(471) == %s.", int->rdigs(471).tostring());
* };
*
* unless ((int->rdigs(471,8)).equals( new Cons( 3, new Cons( 1, new Cons( 3, theEmptyList()))) )) {
* printf("test case failed: int->rdigs(313) == %s.", int->rdigs(313).tostring());
* };
*
*/
Goal G: rd2i(soln) = n
Loop invariant:
At each iteration, soln is more and more of the answer,
but *exactly* how much "more and more"? Well, after one time
through the loop we have the least-significant digit;
after two times through we have two of them, etc. So:
rd2i(soln) = remainder(n,b^loops).
It turns out, this loop invariant can be shown
but takes a lot of work (since it deals with taking the quotient(n,b)
loops times and then remainder(m,b) once,
proving that this is the loops digit of n.)
Attempt 2
Looking for a better loop invariant:
If we consider the accumulator (tail-recursive) version,
what is the contract for this function?
We'll use this!
(define/opt (int->rdigs n [b 10])
(local {;; help: int, (listof digits) → list-of-digits
;; [What purpose statement?]
(define (help n digsSoFar)
(cond [(zero? n) digsSoFar]
[(positive? n) (help (quotient n b)
(cons (remainder n b) digsSoFar))]))}
(help n empty)))
The Purpose statement for "help" -- defining what it returns
in terms of its inputs n, digsSoFar -- is:
;; help: returns the reversed list of digits representing n (base b),
;; appended with digsSoFar.
This formulation gives us the insight for a what more manageable
loop invariant can be
(after all, the accumulator helper-function was really just a loop).
The loop invariant L is:
n = m*b^(length digsSoFar) + d2n(digsSoFar).
and b>1 and n>=0.
(note how n is always the same value in this program.)
The proof goal G is that d2n(digsSoFar) = n.
The condition C is m>0.
Lemma: d2n( (cons x l), b) = x*b^length(l)+d2n(l)
since d2n is the digits→numbers function where
digits are in descending order.
[subscripted proof omitted, for this plain-text.]
Lemma: for any x>=0, d>1, we have:
n = (quotient n d) * d + (remainder n d) .
This is a property of quotient and remainder
List int2digs( int n, int b = 10 ) {
List digsSoFar = theEmptyList();
int m = n;
while (m > 0) {
digsSoFar ← new Cons( remainder(m,b), digsSoFar );
m ← quotient(m,b)
}
}
(0) Precondition (loop invariant holds when we start the loop):
Note that when digsSoFar is empty, this gives us exactly
what we want.: n = m*b^0 +d2n(empty) = m,
which is true by the previous lines.
(1) (Loop invariant maintained).
L∧C → L′
Let D be the list digsSoFar, and |D| be the length of that list.
n = m*b^|D| + d2n(digsSoFar) [the loop hypothesis L]
= (quotient(m,b)*b + remainder(m,b))*b^|D| + d2n(D)
[relate quotient, remainder; need b>1 here!]
= quotient(m,b)*b^(|D|+1) + remainder(m,b)*b^|D| + d2n(|D|)
= quotient(m,b)*b^(|D|+1) + d2n( (cons (remainder m b) D) )
[by lemma about d2n]
= quotient(m,b)*b^|D′| + d2n(D′) [renaming primed concepts]
which is L′, QED. Interestingly, m>0 not needed here!
(2) correctness: L∧¬C → G.
This is straightforward, with one hitch:
(not C) gives us that m <= 0; we need to actually know that m=0.
The fix is that we really need to add that to our loop condition: m≥0.
[It is left to the reader to go back and show that L∧C → L′
for this updated loop condition.)
In that case, L ∧ (m=0) implies that
n=d2n(digsSoFar), which is exactly our loop-goal G.
(3) termination.
m is a natnum which gets smaller by at least 1 each time through the loop,
so it will eventually stop being positive, so ¬C will eventually hold.
Other variants: this same thing, but with accumulator versions.
These give rise to versions, that don't reverse the list of digits.
Write the code for the iterative non-reversing version.
What is the loop invariant?
This can be hard to get right;
my first try gave something that required a scad of intricate, boring
math details (but finally did work).
A better way to get the invariant came from first examining a
tail-recursive (accumulator) function, and stating its contract.
--------- Sample exercise --------
Not an interesting program, but one to test your knowledge:
Prove the following code correct:
int sumFrom( int a, int b )
sum ← 0
i ← a
while (i < b)
sum ← sum+i
i ← i+1
return sum
[First you'll need to add comments saying what this
function does, and then you'll need to phrase
its purpose in terms of functions, summation-notation, sets, whatever --
something that we can reason about mathematically.]
solution