Something to Ponder
If you wrote 100,100 lines of code (LOC) and only 100 lines were good, you have learned the same as the programmer that carefully, thoughtfully wrote only 100 LOC; but you developed more bad habits than her. | ||
| --Anonymous | ||
Moral of the story: take your time to write really good software.
Most programmers go through the following stages of reasoning about writing loops.
Monkey at the Keyboard. First attempts. Don't bother to reason; just run it see if it breaks or not. If it breaks do some more "monkey at the keyboard" and repeat. Or worse, give up; steal or copy code from somewhere else that works.
Automated Monkey. Use a debugger to watch how the program behaves, step-by-step.
Be the Monkey. Play computer to simulate the steps taken.
Ultimately, these methods provide only perfunctory understanding, a dynamic changing mechanically iterating understanding. These methods' shortcomings are due to their inefficiency and error prone nature. While playing computer or running a debugger, it may take a very long time to iteratively go in circles until a loop boundary condition is met. More iterations take more brain effort, thus more difficult. The programmer is less Efficient.
Error Prone, too. Did you ever loose track while counting loop iterations?! Nested loops require thinking about combinatorially many more boundary cases.
Results:
- Off by one loop errors
- Lack of confidence
- Frustration
- Undetected or yet to be detected errors
- Immature reasoning
As programmers mature they take the next step in evolving their reasoning, reasoning statically, with loop invariants.
- Conceptually provides deeper understanding
- Think and reason about programs statically, not dynamically
- Semantic properties are static; no need to play computer while following
loops execution in dynamic cycles, nor follow the paces of a debugger.
- Like an out of body experience when playing computer: reason about the
code from high above (semantically) instead of up close scribbling
frantically while reasoning iteratively. Doesn't matter how many
iterations, reasoning semantically takes the same effort, whereas
reasoning iteratively while playing computer takes more time if/when
there are more iterations
Loop Invariant
The word, “invariant”, means "no change". An invariant is an assertion that is always true. In particular, there are three points in a program where a loop invariant is required to be true:
In the beginning, before 1st iteration, perhaps after some initialization.
During each iteration (specifically, true at the beginning of the loop body, temporarily false inside the loop body, but re-established after the last statement of the loop body) body and
Immediately after the loop is done.
Method for developing with loop invariant:
3. INV and negating loop test condition is the POST condition
2. INV re-established after each iteration
1. INV established by loop initializer
Here is a loop-invariant based proof and associated program to find the largest element of an array, A, where the array's dimenions are declared,
int A[0..n];
At the end of our loop's execution we want j to be the index of a largest element. This idea is captured with the following boolean expression,
POST: forall x in 0..n, A[j]>=A[x]
If you don't like the forall expression, let's rewrite the above expression into it's longhand equivalent,
A[j]>=A[0] and A[j]>=A[1] and ... and A[j]>=A[n]
This invariant intuitively says we have searched found a largest element from the array slice, A[i:n].
INV: forall x in i..n, A[j]>=A[x]
Use the general formula to determine our loop's test expression (the guard),
INV && not (guard) ==> POST
So, a little reasoning reveals that our guard should be,
( i != 0 )
The body must do two things: 1) make progress and 2) re-establish INV.
The following code makes progress,
i = i - 1;
And this re-establishes INV,
if A[i] > A[j] then j = i
This code re-establishes the invariant, INV, because
assert(INV); i = i - 1; if A[i] > A[j] then j = i assert(INV);
(see the WP-IF notes for a detailed analysis).
This code will initially establish the invariant.
i = n;
We confirm this with the following analysis,
wp("i = n"; INV)
Which expanded is,
wp("i = n"; forall x in i..n, A[j]>=A[x])
Substituting the LHS "i" for the RHS "n", we get,
forall x in n..n, A[j]>=A[x]
Simplifying,
A[n]>=A[n]
Simplifying more,
true
So we have proved,
assert(true); // true means can we start the program in any state i = n; assert(INV);
![]() | Key Concept |
|---|---|
Designing a loop with invariants calls for writing the invariant first then coding to the invariant. The proof drives the program. The code is simply written as a side effect of developing the proof. |
![]() | DEFINITION (humorous) |
|---|---|
informalism (n.) - a notation that is used in proofs but suffers in its imprecision or lack of clarity. e.g., proof by picture, also Big-OH notation and equals |
[explain notations to make life easier...]
forall is upside down 'A'
- develop a non-side effecting routine, sorted(), that operates on
array fragments
- returns boolean
- non side-effecting, so does not change the array contents nor anything else
- notation A[i:j] means A[i], A[i+1], A[i+2], ... A[j] in sequence
- A[4:3] is an empty sequence
sorted(A[i:j])
picture of an n-element array
The invariant is specified formally as,
INV: (forall u=1:i-1 forall v=i:n A[u] <= A[v]) and sorted(A[i:n])
An another, less formal way to express is through pictures,
1 i i+1 n
+-------------------------------------------+---------------------+
INV: | all A[1:i] <= all A[i+1:n] | sorted |
+-------------------------------------------+---------------------+
Even less formal is,
1 i i+1 n
+-------------------------------------------+---------------------+
INV: | <= A[i+1:n] | sorted |
+-------------------------------------------+---------------------+
Now we can almost write the guard. But the most important part is that we well author the guard code (the expression inside the while loop test) by reasoning statically.
The guard is obtained by envisioning the postcondition, shown here, and asking what value of i will achieve this?
1 n
+-----------------------------------------------------------------+
POST: | sorted |
+-----------------------------------------------------------------+
Clearly,
INV && (i==0)
will do this. So the guard is written using the boolean test,
i != 0
which is the negation of (i==0).
The program, developed this far, looks like this.
// some init code goes here
while (i != 0) {
// some loop body code goes here
}
Annotated with asserts, the program looks like this.
assert (TRUE);
// some init code goes here
assert (INV);
while (i != 0) {
assert (INV);
// some loop body code goes here
assert (INV);
}
assert (INV && i==0); /* same as */ assert (POST);
There are two important things to point out. First, we have been reasoning about the loop termination condition, by reasoning statically. This way we avoid the immature programmer's iterative or repetive thinking. The second point is that we have reasoned about this program by authoring the proof first; and the development of the prove drive the code. Finally, we have reduced or eliminated off-by-one error!
The loop initialization establishes (i.e., makes true) the invariant. So we want a peice of code at the beginning that does this:
assert(true); //some code goes here assert(INV);
Or the same written in the traditional Djiktsta notation, where curly braces are used to denote comment syntax in the tradition of Pascal programming.
{ true }
//some code goes here
{ INV }
Intuitively, an approach is to use some initialization code that makes INV true by "moving" i so far to the left that the sorted part is empty. Thus the initialization is just doing easy work of setting i's value, and doesn't require any "hard" sorting work.
![]() | Note |
|---|---|
This is a good time to make some general comments on invariant design. Good invariants are designed to be:
|
assert( PRE: true )
i := n +1; // establishes INV
{ INV: (forall u=1:i-1 forall v=i:n A[u] <= A[v]) and sorted(A[i:n])
assert( i==n+1 and INV )
while ( i != 1 ) {
... // INV must be true at each iteration...
}
assert( POST: i==1 and INV ) // negate while loop test condition
NOTE: we can simplify and rewrite as the equivalent,
assert( POST: sorted(A[1:n]) )
Intuitively know the loop body needs to move i to the left, which breaks (makes false) the invariant, so we code this:
i := i - 1;
Picture of broken invariant, i.e., what can be asserted just after this assignment statement,
1 i i+1 i+2 n
+-------------------------------------------+---------------------+
| | sorted |
+-------------------------------------------+---------------------+
Now the job is to re-establish the invariant, INV, and note, we could re-establish the invariant with:
i := i + 1;
But this does so so trivially as to make no progress towards the post condition and the ultimate goal of sorting. Better, instead to reestablish the invariant by scanning for the largest element in A[1:i+1], say it's A[m], and swapping the largest element A[m] into the right place, i.e., swapping with A[i+1]
This scanning business needs to be done with an inner loop - we can also design using another invariant, INV2 (two pictures).
1 i+1 i+2 n
+-------------------------------------------+---------------------+
INV2: | all <= all A[i:n] | sorted |
+-------------------------------------------+---------------------+
AND
1 j m i+1
+-------------+------------+---+------------+--...
| | <= A[m] | | <= A[m] |
+-------------+------------+---+------------+--...
Note that INV2 incorporates broken-INV as the first (picture) clause.
![]() | Design Strategy |
|---|---|
Note, we design the inner loop by formulating its invariant, first. Code will come later. |
{ INV: (forall u=1:i-1 forall v=i:n A[u] <= A[v]) and sorted(A[i:n])
{ PRE: true }
i := n +1; // establishes INV
{ i==n+1 and INV }
while ( i != n ) {
// INV at each start of iteration...
i := i - 1; // INV broken
{ INV2: (forall u=1:i forall v=i+1:n A[u] <= A[v]) and sorted(A[i+1:n])
and
A[m] == max(A[j:i]) }
j := i; m := i; // establishes INV2
{ j==i==m and INV2 }
while (j != 1) {
.
.
.
}
{ POST: INV2 and j==1 }
}
{ POST: sorted(A[1:n]) }
![]() | Important Subtilty |
|---|---|
The predicate, sorted(), here is different than the other sort algorithms, e.g., insertion sort. Here, the meaning is that all the sorted elements are indeed in their final positions w.r.t. the whole array. Formally, sorted() is defined as, forall X in [i..n], forall Y in [1..X], A[X] <= A[Y]
Whereas in the insertion sort's algorithm, Y ranges from i..X Must be very careful about these "informalisms". Note in bubble sort, sorted() can just mean relative to the array slice without consideration for the array elements not in that slice, but selection sort, sorted() has a stronger meaning: the sorted array elements are in their final positions w.r.t. the whole array. This has a subtle implication when establishing the initial invariant, the sorted slice must be empty, because sorted() might not be true for A[n]. Then again, even this definition for the predicate sorted() does not capture the notion of a permutation of the initial array elements. Which demonstrates an incompleteness in the notation, since we do not have a notation for initial values. Also we should have introduced the function, perm( A, B ), which is true when A contains all the same elements as B possibly in a rearranged permutation. |
Other Examples: Insertion Sort, Bubble Sort
Example, Selection Sort
1 i n
+-------------------------------------------+---------------------+
INV: | | sorted |
+-------------------------------------------+---------------------+
{ INV: sorted(A[i:n]) }
Caveats, Final Notes:
- Could introduce the idea of stable sorting