Module 0046: Pre and post conditions

Tak Auyeung, Ph.D.

October 26, 2018

Contents

1 About this module
2 What is this crazy math?
3 Pre and post conditions
 3.1 Constant assignment
 3.2 Resolving references to variables to constants
 3.3 Simple self referencing assignments
 3.4 Revisiting the Forget operation
 3.5 Conditional statements
 3.6 Loops

1 About this module

2 What is this crazy math?

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.

3 Pre and post conditions

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.

3.1 Constant assignment

Let’s start with something that’s relatively simple to deal with. Algorithm 3.1 is just a sequence of assignment statements.

1x0  
2y2  
3x5  
4y1

Assuming this algorithm stands alone, there is no pre condition to it. In our notation, pre(1) represents the pre condition of the statement on line 1. Because line 1 is the first line of code, pre(1) 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 pre(1) = true.

What is the post condition of line 1? Since this is a constant assignment statement, we know that variable x must end up with a value of 0. In other words, we know that x = 0 after this statement. Consequently, we “add” the fact to pre(1) so that post(1) = pre(1) (x = 0) If you look up the definition of conjunction, x true is simply x. We can simplify our post condition so that post(1) = (x = 0).

The same argument applies to line 2. Note that pre(2) = post(1) because there is nothing that can change what we know about the program between the lines. What is post(2)?

Line 2 changes variable y to a constant of 2. We know that after line 2, y = 2. However, this is not the only thing that we know. What we knew earlier that x = 0 is still true. As a result, post(2) = (x = 0) (y = 2).

Here comes the tricky part. What is post(3)? If we simply add the fact that x = 5, then we end up with post(3) = (x = 0) (y = 2) (x = 5). However, this is impossible because x = 0 contradicts x = 5. 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 forget(c,v) to mean forgetting all components in condition c that refers to variable v.

In our example, we can now derive post(3) = forget(post(2),x) (x = 5). 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 x should be forgotten. Then, we add the fact that x = 5.”

This results in the following derivation:

post(3) = forget(post(2),x) (x = 5) (1) = forget((x = 0) (y = 2),x) (x = 5) (2) = (y = 2) (x = 5) (3)

Now, onto the last statement. We only have to reapply what we learned in the previous step:

post(4) = forget(post(3),y) (y = 1) (4) = forget((y = 2) (x = 5),y) (y = 1) (5) = (x = 5) (y = 1) (6)

Are you surprised at the result?

Let us consider the general case:

1xe

Assuming e does not refer to variable x itself, then post(1) = forget(pre(1),x) (x = e).

3.2 Resolving references to variables to constants

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:

post(1) = (x < y) (y = 10)

Here, we already know, for sure, that y has a value of 10. This means that the inequality (x < y) can now be resolved as (x < 10). 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 y. However, that will not impact the fact that we already know that (x < 10).

3.3 Simple self referencing assignments

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.

1x0 
2y2 
3xx+3  
4yy1

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 pre(3) = (x = 0) (y = 2). What is the post(3)?

Let us examine what we are doing on line 3. In this assignment statement, we first evaluate the right hand side. x = 0 at this point, so x + 3 = 3. Then, we take the value of 3, and use that to update x. In other words, x = 3 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” f(x) = x + 3. This is really just a notation so that f(3) = 6, f(2) = 1 and etc. When some functions, we can also define an “inverse function”. In this case, the inverse function is f1(x) = x 3. What this really means is simply that f1(f(x)) = x. In other words, f1 undoes whatever f does.

Next, let us define a substitution operation. sub(c,x,y) means that in condition c, replace all occurances of x with y. For example, sub(x + y = z,y,y 1) = x + (y 1) = z.

With these new tools, we can get back to work. Let us keep the definition that f(x) = x + 3 and f1(x) = x 3. Then we have the following derivation:

post(3) = sub(pre(3),x,f1(x)) (7) = sub((x = 0) (y = 2),x,x 3) (8) = ((x 3) = 0) (y = 2) (9) = (x = 3) (y = 2) (10)

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 g(y) = y 1. The inverse function is g1(y) = y + 1 so that g1(g(y)) = y. Given this, we can finish the derivation:

post(4) = sub(post(3),y,g1(y)) (11) = sub((x = 3) (y = 2),y,y + 1) (12) = (x = 3) ((y + 1) = 2) (13) = (x = 3) (y = 1) (14)

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:

1xf(x)

Assume that f(x) is a function of x that has an inverse function f1(x), then post(1) = sub(pre(1),x,f1(x)).

3.4 Revisiting the Forget operation

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:

1xy

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 x 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 x e (e is an expression):

For all other assignment statements, the forget rule should be used. This include the following statement:

1xxx

This is because even though the RHS refers to the LHS in this case, it is not reversible. Once variable x becomes zero after this statement, there is no way to “recover” its previous value before the assignment statement.

3.5 Conditional statements

Let’s consider algorithm 3.5 that makes z the maximum of x and y.

1if x>y then  
2  zx  
3else 
4  zy  
5end if 

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, pre(1) = true

The pre condition of line 2 is not just true. This is because the only way that we get to line 2 is that the condition x > y must be true. Consequently, pre(2) = (x > y). Using the same argument, the only way we get to line 4 is that ¬(x > y), which is equivalent to saying x y. As a result, pre(4) = (x y).

The post condition of line 2 is simply post(2) = pre(2) (z = x) = (x > y) (z = x). Similarly, the post condition of line 4 is post(4) = pre(4) (z = y) = (x y) (z = y).

Here comes the tricky part: what is post(5)? 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,

post(5) = post(2) post(4) (15) = ((x > y) (z = x)) ((x y) (z = y)) (16)

Does this make sense?

Here is the general conditional statement:

1if c then  
2    thenblock  
3else 
4    elseblock  
5end if 

3.6 Loops

We are now on our final stretch!

Let us consider algorithm 3.6.

1x0  
2while x<3 do  
3  xx+1  
4end while 

Let’s begin with pre(2). After the initialization of x, we know that pre(2) = (x = 0). 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 (x < 3) must be true if we enter the loop.

As a result, all we say at this time is that post(2) = (x < 3) ((x = 0) post(3)). Note that pre(3) = post(2).

The statement on line 3 looks familiar. It involves a function of x. In this case, the inverse function is x 1. We can then derive the following:

post(3) = sub(pre(3),x,x 1) (17) = ((x 1) < 3) (((x 1) = 0) post(3)) (18) = (x 3) ((x = 1) post(3)) (19)

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 post(3) = (x 3) works. This is because (x = 1) (x 3) simplifies to x 3 because it includes the case of x = 1. Furthermore, (x 3) (x 3) can also simplify to just x 3. In conclusion, we can define post(3) = (x 3).

How about post(4)? 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: x < 3 must be false. As a result,

post(4) = ¬(x < 3) (pre(2) post(3)) (20) = (x 3) ((x = 3) (x 3)) (21) = (x 3) (x 3) (22) = (x = 3) (23)

In other words, when we exit the loop, we know that x = 3.

In its general form, a prechecking loop is as follows:

1while c do  
2    block  
3end while 

The general form of a postchecking loop is as follows:

1repeat  
2    block  
3until c