So you thought it’d be easy becoming a programmer.
There are two schools of thought regarding the role of mathematics in programming. One school, led by Edsger Dijkstra, suggests that a programmer should be mathematician first. At the very least, every programmer should have a solid foundation of mathematics. Furthermore, computer science should be a branch of mathematics. For a more detailed discussion of this topic, visit http://en.wikipedia.org, and search for “cruelty computer science”.
Another school, including Donald Knuth (another giant and pioneer of computer science), claims that with sufficient advances of tools, everyone can program computers. This is at least partially true. In the “good old days”, programs are only written in machine language, consisting of long strings of 0’s and 1’s. Later on, with symbolic assemblers, writing programs became easier. These days, with Visual Studio and other development environments, the tools actually helps to fix problems not only in syntax, but also sometimes the logic of programs.
Nonetheless, it is still important for programmers to understand enough logic and mathematics. Programming by intuition is possible, but it only enables a programmer to a certain level. If not for anything else, a solid background of logic and mathematics enables a programmer trouble shoot faulty programs with great efficiency. Most programmers who are well trained in mathematics also tend to design more elegant programs that minimize chances of errors to begin with.
A pre condition is simply “what we know before the execution of a statement in a program.” A post condition is “what we know after the execution of a statement in a program.” The analysis of the pre and post conditions of a program is essential, because we can prove the correctness of a program (or an algorithm) rigorously.
If you consider an algorithm, the pre condition of the entire algorithm is what is given to the program. The post condition of an entire algorithm is the outcome of a program. Being able to mathematically derive the post condition of an algorithm based on its pre condition means we can prove whether an algorithm does what it is supposed to. There is no need to use test cases anymore, because a proof of correctness covers all cases that can be thrown at the algorithm.
Let’s start with something that’s relatively simple to deal with. Algorithm 3.1 is just a sequence of assignment statements.
Assuming this algorithm stands alone, there is no pre condition to it. In our notation, represents the pre condition of the statement on line 1. Because line 1 is the first line of code, also represents the pre condition of the entire algorithm.
Note that a pre condition is a condition. It expresses something that is true. How do we express true when we don’t know anything is true? Well, we know something is always true, it is “true” itself!
Consequently, we can simply say that .
What is the post condition of line 1? Since this is a constant assignment statement, we know that variable must end up with a value of 0. In other words, we know that after this statement. Consequently, we “add” the fact to so that If you look up the definition of conjunction, is simply . We can simplify our post condition so that .
The same argument applies to line 2. Note that because there is nothing that can change what we know about the program between the lines. What is ?
Line 2 changes variable to a constant of . We know that after line 2, . However, this is not the only thing that we know. What we knew earlier that is still true. As a result, .
Here comes the tricky part. What is ? If we simply add the fact that , then we end up with . However, this is impossible because contradicts . What do we need to do?
What we need is a way to “forget” some facts because a new fact is in, and the new fact may contradicts some old facts. This is accomplished by a “forget” operation. let us define to mean forgetting all components in condition that refers to variable .
In our example, we can now derive . Literally, this means “the post condition of line 3 is essentially the same as the post condition of line 2, except that all components referring to should be forgotten. Then, we add the fact that .”
This results in the following derivation:
Now, onto the last statement. We only have to reapply what we learned in the previous step:
Are you surprised at the result?
Let us consider the general case:
Assuming does not refer to variable itself, then .
If there are any “chainable” equalities, you should try to get everything resolved to constants whenever possible. For example, let us consider the following post condition:
Here, we already know, for sure, that has a value of 10. This means that the inequality can now be resolved as . The main purpose to do this is to decouple variables from each other. In other words, we want to lessen the impact of a change to one variable to conditions that involve other variables.
In this example, a later assignment can change the value of . However, that will not impact the fact that we already know that .
Now that we can handle constant assignments and a sequence, let’s move on to something slightly more interesting.
Let us consider algorithm 3.3.
Note that the first two statements of algorithm 3.3 are the same as the first two statements of algorithm 3.1. Let’s start with the first statement that is different. We know that . What is the ?
Let us examine what we are doing on line 3. In this assignment statement, we first evaluate the right hand side. at this point, so . Then, we take the value of 3, and use that to update . In other words, after the statement.
In the analysis of post conditions, we have a more obsure way to come to the same conclusion.
First, let us define a “function” . This is really just a notation so that , and etc. When some functions, we can also define an “inverse function”. In this case, the inverse function is . What this really means is simply that . In other words, undoes whatever does.
Next, let us define a substitution operation. means that in condition , replace all occurances of with . For example, .
With these new tools, we can get back to work. Let us keep the definition that and . Then we have the following derivation:
Of course, this doesn’t really come as a surprise. However, we now know why it is the case!
On to the last statement, we define . The inverse function is so that . Given this, we can finish the derivation:
This is yet another earth-breaking discovery (not!). Although it may seem that we could have used intuition (sometimes called “eyeball proof”) to come to the same conclusion, the tools that we have used in this section are very general and can be applied to much more difficult problems.
We can now derive the general case as follows:
Assume that is a function of that has an inverse function , then .
The forget operation is applicable for assignment statements whenever the substitution operation cannot be performed. This means that the following statement should use the “forget” operation:
This is because the RHS of the assignment does not refer to the LHS. There is no inverse function to reverse the new value of back to its previous value before the assignment statement.
As a result, you can use the substitution rule if and only if the following conditions are all true for an assignment statement ( is an expression):
For all other assignment statements, the forget rule should be used. This include the following statement:
This is because even though the RHS refers to the LHS in this case, it is not reversible. Once variable becomes zero after this statement, there is no way to “recover” its previous value before the assignment statement.
Let’s consider algorithm 3.5 that makes the maximum of and .
Let us express the pre and post conditions of each statement. In this case, let us not to assume any values in the variables. In other words,
The pre condition of line 2 is not just . This is because the only way that we get to line 2 is that the condition must be true. Consequently, . Using the same argument, the only way we get to line 4 is that , which is equivalent to saying . As a result, .
The post condition of line 2 is simply . Similarly, the post condition of line 4 is .
Here comes the tricky part: what is ? In other words, after the entire conditional statement, what can we conclude?
There are two ways to get to line 5. We can get there from line 2, or from line 4. Consequently, the post condition of line 5 is the post condition of line 2 or the post condition of line 4. In other words,
Does this make sense?
Here is the general conditional statement:
We are now on our final stretch!
Let us consider algorithm 3.6.
Let’s begin with . After the initialization of , we know that . However, the post condition of line 2 is not as simple. This is because there are actually two ways to get to line 2. In the first iteration, we enter from line 1. However, in subsequent iterations, we enter from line 3. On the other hand, no matter which way we come from, we know that the condition must be true if we enter the loop.
As a result, all we say at this time is that . Note that .
The statement on line 3 looks familiar. It involves a function of . In this case, the inverse function is . We can then derive the following:
This is interesting because the same notation appears on the left and the right hand side of the equation. The solution is to find a definition that works in this equation. Choosing works. This is because simplifies to because it includes the case of . Furthermore, can also simplify to just . In conclusion, we can define .
How about ? In theory, we can get there using two means. First, it is possible not to enter the loop at all. Second, we can go through the loop at least once, then exit the loop. Regardless of whether we get into the loop or not, one thing is for sure: must be false. As a result,
In other words, when we exit the loop, we know that .
In its general form, a prechecking loop is as follows:
The general form of a postchecking loop is as follows: