Section 8.2
Writing Correct Programs
Correct programs don't just happen. It takes planning and attention to detail to avoid errors in programs. There are some techniques that programmers can use to increase the likelihood that their programs are correct.
8.2.1 Provably Correct Programs
In some cases, it is possible to prove that a program is correct. That is, it is possible to demonstrate mathematically that the sequence of computations represented by the program will always produce the correct result. Rigorous proof is difficult enough that in practice it can only be applied to fairly small programs. Furthermore, it depends on the fact that the "correct result" has been specified correctly and completely. As I've already pointed out, a program that correctly meets its specification is not useful if its specification was wrong. Nevertheless, even in everyday programming, we can apply some of the ideas and techniques that are used in proving that programs are correct.
The fundamental ideas are process and state. A state consists of all the information relevant to the execution of a program at a given moment during its execution. The state includes, for example, the values of all the variables in the program, the output that has been produced, any input that is waiting to be read, and a record of the position in the program where the computer is working. A process is the sequence of states that the computer goes through as it executes the program. From this point of view, the meaning of a statement in a program can be expressed in terms of the effect that the execution of that statement has on the computer's state. As a simple example, the meaning of the assignment statement "x = 7;" is that after this statement is executed, the value of the variable x will be 7. We can be absolutely sure of this fact, so it is something upon which we can build part of a mathematical proof.
In fact, it is often possible to look at a program and deduce that some fact must be true at a given point during the execution of a program. For example, consider the do loop:
do { System.out.print("Enter a positive integer: "); N = TextIO.getlnInt(); } while (N <= 0);
After this loop ends, we can be absolutely sure that the value of the variable N is greater than zero. The loop cannot end until this condition is satisfied. This fact is part of the meaning of the while loop. More generally, if a while loop uses the test "while (condition)", and if there are no break statements in the loop, then after the loop ends, we can be sure that the condition is false. We can then use this fact to draw further deductions about what happens as the execution of the program continues. (With a loop, by the way, we also have to worry about the question of whether the loop will ever end. This is something that has to be verified separately.)
8.2.2 Preconditions and Postconditions
A fact that can be proven to be true after a given program segment has been executed is called a postcondition of that program segment. Postconditions are known facts upon which we can build further deductions about the behavior of the program. A postcondition of a program as a whole is simply a fact that can be proven to be true after the program has finished executing. A program can be proven to be correct by showing that the postconditions of the program meet the program's specification.
Consider the following program segment, where all the variables are of type double:
disc = B*B - 4*A*C; x = (-B + Math.sqrt(disc)) / (2*A);
The quadratic formula (from high-school mathematics) assures us that the value assigned to x is a solution of the equation A*x2 + B*x + C = 0, provided that the value of disc is greater than or equal to zero and the value of A is not zero. If we can guarantee that B*B-4*A*C >= 0 and that A != 0, then the fact that x is a solution of the equation becomes a postcondition of the program segment. We say that the condition, B*B-4*A*C >= 0 is a precondition of the program segment. The condition that A != 0 is another precondition. A precondition is defined to be a condition that must be true at a given point in the execution of a program in order for the program to continue correctly. A precondition is something that you want to be true. It's something that you have to check or force to be true, if you want your program to be correct.
We've encountered preconditions and postconditions once before, in Subsection 4.7.1. That section introduced preconditions and postconditions as a way of specifying the contract of a subroutine. As the terms are being used here, a precondition of a subroutine is just a precondition of the code that makes up the definition of the subroutine, and the postcondition of a subroutine is a postcondition of the same code. In this section, we have generalized these terms to make them more useful in talking about program correctness in general.
Let's see how this works by considering a longer program segment:
do { System.out.println("Enter A, B, and C."); System.out.println("A must be non-zero and B*B-4*A*C must be >= 0."); System.out.print("A = "); A = TextIO.getlnDouble(); System.out.print("B = "); B = TextIO.getlnDouble(); System.out.print("C = "); C = TextIO.getlnDouble(); if (A == 0 || B*B - 4*A*C < 0) System.out.println("Your input is illegal. Try again."); } while (A == 0 || B*B - 4*A*C < 0); disc = B*B - 4*A*C; x = (-B + Math.sqrt(disc)) / (2*A);
After the loop ends, we can be sure that B*B-4*A*C >= 0 and that A != 0. The preconditions for the last two lines are fulfilled, so the postcondition that x is a solution of the equation A*x2 + B*x + C = 0 is also valid. This program segment correctly and provably computes a solution to the equation. (Actually, because of problems with representing real numbers on computers, this is not 100% true. The algorithm is correct, but the program is not a perfect implementation of the algorithm. See the discussion in Subsection 8.1.3.)
Here is another variation, in which the precondition is checked by an if statement. In the first part of the if statement, where a solution is computed and printed, we know that the preconditions are fulfilled. In the other parts, we know that one of the preconditions fails to hold. In any case, the program is correct.
System.out.println("Enter your values for A, B, and C."); System.out.print("A = "); A = TextIO.getlnDouble(); System.out.print("B = "); B = TextIO.getlnDouble(); System.out.print("C = "); C = TextIO.getlnDouble(); if (A != 0 && B*B - 4*A*C >= 0) { disc = B*B - 4*A*C; x = (-B + Math.sqrt(disc)) / (2*A); System.out.println("A solution of A*X*X + B*X + C = 0 is " + x); } else if (A == 0) { System.out.println("The value of A cannot be zero."); } else { System.out.println("Since B*B - 4*A*C is less than zero, the"); System.out.println("equation A*X*X + B*X + C = 0 has no solution."); }
Whenever you write a program, it's a good idea to watch out for preconditions and think about how your program handles them. Often, a precondition can offer a clue about how to write the program.
For example, every array reference, such as A[i], has a precondition: The index must be within the range of legal indices for the array. For A[i], the precondition is that 0 <= i < A.length. The computer will check this condition when it evaluates A[i], and if the condition is not satisfied, the program will be terminated. In order to avoid this, you need to make sure that the index has a legal value. (There is actually another precondition, namely that A is not null, but let's leave that aside for the moment.) Consider the following code, which searches for the number x in the array A and sets the value of i to be the index of the array element that contains x:
i = 0; while (A[i] != x) { i++; }
As this program segment stands, it has a precondition, namely that x is actually in the array. If this precondition is satisfied, then the loop will end when A[i] == x. That is, the value of i when the loop ends will be the position of x in the array. However, if x is not in the array, then the value of i will just keep increasing until it is equal to A.length. At that time, the reference to A[i] is illegal and the program will be terminated. To avoid this, we can add a test to make sure that the precondition for referring to A[i] is satisfied:
i = 0;
while (i < A.length && A[i] != x) {
i++;
}
Now, the loop will definitely end. After it ends, i will satisfy either i == A.length or A[i] == x. An if statement can be used after the loop to test which of these conditions caused the loop to end:
i = 0; while (i < A.length && A[i] != x) { i++; } if (i == A.length) System.out.println("x is not in the array"); else System.out.println("x is in position " + i);
8.2.3 Invariants
Let's look at how loops work in more detail. Consider a subroutine for finding the sum of the elements in an array of int:
static int arraySum( int[] A ) { int total = 0; int i = 0; while ( i < A.length ) { total = total + A[i]; i = i + 1; } return total; }
(Note, by the way, that the requirements that A is not null is a precondition of the subroutine. If it is violated, the code in the subroutine will throw a NullPointerException.)
How can we be sure that this subroutine works? We need to prove that when the return statement is executed, the value of total is the sum of all the elements in A. One way to think about this problem is in terms of loop invariants.
A loop invariant is, roughly, a statement that remains true as the loop is executed. More precisely, we can show that a statement is an invariant for a loop if the following holds: As long as the statement is true before the code inside the loop is executed, then it will also be true after the code inside the loop has been executed. That is, a loop invariant is both a precondition and a postcondition of the body of the loop.
A loop invariant for the loop in the above subroutine is, "total is equal to the sum of the first i elements of A." Suppose this is true at the beginning of the while loop. That is, before the statement "total = total + A[i]" is executed, total is the sum of the first i elements of the array (namely A[0] through A[i-1]). After A[i] is added to total, total is now the sum of the first i+1 elements of the array. At this point, the loop invariant is not true. However, as soon as the next statement, "i = i + 1" is executed, replacing i with i+1, the loop invariant becomes true again. We have checked that if the loop invariant is true at the start of the body of the loop, then is also true at the end.
Note that a loop invariant is not necessarily true at every point during the execution of a loop. Executing one of the statements in the loop can make it false temporarily, as long as later statements in the loop make it true again.
So, have we proved that the subroutine arraySum() is correct? Not quite. There are still a few things to check. First of all, we need to make sure that the loop invariant is true before the very first time the loop is executed. At that point, i is zero, and total is also equal to zero, which is the correct sum of zero elements. So the loop invariant is true before the loop. Once we know that, we know that it remains true after each execution of the loop (because it's an invariant), and in particular, we know that it will still be true after the loop ends.
But for that to do us any good, we need to check that the loop actually does end! In each execution of the loop, the value of i goes up by one. That means that eventually it has to reach A.length. At that point, the condition in the while loop is false, and the loop ends.
After the loop ends, we know that i equals A.length, and we know that the loop invariant is true. At that point, since i is A.length, the loop invariant says, "total is the sum of the first A.length" elements of A." But that includes all of the elements of A. So, the loop invariant gives us exactly what we wanted to show: When total is returned by the subroutine, it is equal to the sum of all the elements of the array!
This might seem to you to be a lot of work to prove something that's obvious. But if you try to explain why it's obvious that arraySum() works, you'll probably find yourself using the logic behind loop invariants, even if you don't use the term.
Let's look more quickly at a similar example. Consider a subroutine that finds the maximum value in an array of int, where we assume that the array has length at least one:
static int maxInArray( int[] A ) { int max = A[0]; int i = 1; while ( i < A.length ) { if ( A[i] > max ) max = A[i]; i = i + 1; } return max; }
In this case, we have a loop invariant that says, "max is the largest value among the first i elements of A." This statement is true before the loop starts, when i is 1 and max is A[0]. Suppose it is true at the start of the loop, before the if statement. After the if statement, max is greater than or equal to A[i], because that is a postcondition of the if statement, and it is greater than or equal to A[0] through A[i-1], because of the truth of the loop invariant. Put those two facts together, and you get that max is the largest value among the first i+1 elements of A. When i is replaced by i+1 in the next statement, the loop invariant becomes true again. After the loop ends, i is A.length, and the loop invariant tells us exactly what we need to know: max is the largest value in the whole array.
Loop invariants are not just useful for proving that programs are correct. Thinking in terms of loop invariants can be useful when you are trying to develop an algorithm. As an example, let's look at the insertion sort algorithm that was discussed in Subsection 7.5.3. Suppose that we want to sort an array A. That is, at the end of the algorithm, we want it to be true that
A[0] <= A[1] <= ... <= A[A.length-1]
The question is, what step-by-step procedure can we use to make this statement true? Well, can we come up with a loop invariant that, at the end, will become the statement that we want to be true? If we want all of the elements to be sorted at the end, how about a loop invariant that says that some of the elements are sorted—say, that the first i elements are sorted. This leads to an outline for the algorithm:
i = 0; while (i < A.length ) { // Loop invariant: A[0] <= A[1] <= ... <= A[i-1] . . // Code that adds A[i] to the sorted portion of the array . i = i + 1; } // At this point, i = A.length, and A[0] <= A[1] <= ... <= A[A.length-1]
The loop invariant is true before the while loop, and when the loop ends, the loop invariant becomes precisely the statement that we want to be true at the end of the algorithm. We know what we have to do to complete the algorithm: Develop code for the inside of the loop that will preserve the truth of the loop invariant. If we can do that, the loop invariant will assure us that the algorithm that we have developed is correct. The algorithm for adding A[i] to the sorted portion of the array will require its own loop, with its own loop invariant. I'll leave you to think about that.
There is another kind of invariant that is useful for thinking about programs: class invariants. A class invariant is, roughly, a statement that is always true about the state of a class, or about objects created from that class. For example, suppose we have a PairOfDice class in which the values shown on the dice are stored in instance variables die1 and die2. (See Section 5.2 for a variety of such classes.) We might like to have a class invariant that says, "the values of die1 and die2 are in the range 1 through 6." (This would be a statement about any object created from the PairOfDice class, not about the class as such.) After all, this is a statement that should always be true about any pair of dice.
But in order to be a class invariant, the statement has to be guaranteed true at all times. If die1 and die2 are public instance variables, then no such guarantee is possible, since there is no way to control what values a program that uses the class might assign to them. So, we are led to make die1 and die2 private. Then we just have to make sure that all of the code in the class definition respects the class invariant. That is, first of all, when a PairOfDice object is constructed, the variables die1 and die2 must be initialized to be in the range 1 to 6. Furthermore, every method in the class must preserve the truth of the class invariant. In this case, that means that any method that assigns a value to die1 or die2 must ensure that the value is in the range 1 to 6. For example, a setter method would have to check that a legal value is being assigned.
In general, we can say that a class invariant is a postcondition of every constructor and is both a precondition and a postcondition of every method in the class. When you are writing a class, a class invariant is something that you want to be true at all times. When you write a method, you need to make sure that the code in that method respects the invariant: Assuming that the class invariant is true when the method in called, you need to ensure that it will still be true after the code in the method is executed. This kind of thinking can be a very useful tool for class design.
As another example, consider a dynamic array class, like the one in Subsection 7.2.4. That class uses an ordinary array to store values and a counter to keep track of how many items have been added to the dyanmic array:
private int[] items = new int[8]; private int itemCount = 0;
Class invariants include the facts that "itemCount is the number of items," that "0 <= itemCount < items.length," and that "the items are in the array elements items[0] through items[itemCount-1]." Keeping these invariants in mind can be helpful when writing the class. When writing a method for adding an item, the first invariant reminds you to increment itemCount in order to ensure that the invariant remains true. The second invariant tells you where the new item has to be stored. And the third invariant tells you that if incrementing itemCount makes it equal to items.length, then you will need to do something to avoid violating the invariant. (Since itemCount has to be incremented, the invariant means that you will have to make the array bigger.)
In future chapters, I will occasionally point out how it can be useful to think in terms of preconditions, postconditions, and invariants.
I should note that reasoning about invariants becomes much more complicated in parallel programs, when several threads that are running at the same time and are accessing the same data. This will be an issue when we encounter threads in Chapter 12.
8.2.4 Robust Handling of Input
One place where correctness and robustness are important—and especially difficult—is in the processing of input data, whether that data is typed in by the user, read from a file, or received over a network. Files and networking will be covered in Chapter 11, which will make essential use of material that will be covered in the next section of this chapter. For now, let's look at an example of processing user input.
Examples in this textbook use my TextIO class for reading input from the user. This class has built-in error handling. For example, the function TextIO.getDouble() is guaranteed to return a legal value of type double. If the user types an illegal value, then TextIO will ask the user to re-enter their response; your program never sees the illegal value. However, this approach can be clumsy and unsatisfactory, especially when the user is entering complex data. In the following example, I'll do my own error-checking.
Sometimes, it's useful to be able to look ahead at what's coming up in the input without actually reading it. For example, a program might need to know whether the next item in the input is a number or a word. For this purpose, the TextIO class includes the function TextIO.peek(). This function returns a char which is the next character in the user's input, but it does not actually read that character. If the next thing in the input is an end-of-line, then TextIO.peek() returns the new-line character, '\n'.
Often, what we really need to know is the next non-blank character in the user's input. Before we can test this, we need to skip past any spaces (and tabs). Here is a function that does this. It uses TextIO.peek() to look ahead, and it reads characters until the next character in the input is either an end-of-line or some non-blank character. (The function TextIO.getAnyChar() reads and returns the next character in the user's input, even if that character is a space. By contrast, the more common TextIO.getChar() would skip any blanks and then read and return the next non-blank character. We can't use TextIO.getChar() here since the object is to skip the blanks without reading the next non-blank character.)
/** * Reads past any blanks and tabs in the input. * Postcondition: The next character in the input is an * end-of-line or a non-blank character. */ static void skipBlanks() { char ch; ch = TextIO.peek(); while (ch == ' ' || ch == '\t') { // Next character is a space or tab; read it // and look at the character that follows it. ch = TextIO.getAnyChar(); ch = TextIO.peek(); } } // end skipBlanks()
(In fact, this operation is so common that it is built into TextIO. The method TextIO.skipBlanks() does essentially the same thing as the skipBlanks() method presented here.)
An example in Subsection 3.5.3 allowed the user to enter length measurements such as "3 miles" or "1ft". It would then convert the measurement into inches, feet, yards, and miles. But people commonly use combined measurements such as "3 feet 7 inches". Let's improve the program so that it allows inputs of this form.
More specifically, the user will input lines containing one or more measurements such as "1 foot" or "3 miles 20 yards 2 feet". The legal units of measure are inch, foot, yard, and mile. The program will also recognize plurals (inches, feet, yards, miles) and abbreviations (in, ft, yd, mi). Let's write a subroutine that will read one line of input of this form and compute the equivalent number of inches. The main program uses the number of inches to compute the equivalent number of feet, yards, and miles. If there is any error in the input, the subroutine will print an error message and return the value -1. The subroutine assumes that the input line is not empty. The main program tests for this before calling the subroutine and uses an empty line as a signal for ending the program.
Ignoring the possibility of illegal inputs, a pseudocode algorithm for the subroutine is
inches = 0 // This will be the total number of inches while there is more input on the line: read the numerical measurement read the units of measure add the measurement to inches return inches
We can test whether there is more input on the line by checking whether the next non-blank character is the end-of-line character. But this test has a precondition: We have to make sure that the next character in the input is in fact either an end-of-line or is a non-blank. To ensure that, we need to skip over any blank characters. So, the algorithm becomes
inches = 0 skipBlanks() while TextIO.peek() is not '\n': read the numerical measurement read the unit of measure add the measurement to inches skipBlanks() return inches
Note the call to skipBlanks() at the end of the while loop. The call to skipBlanks() ensures that the precondition for the test is again true. More generally, if the test in a while loop has a precondition, then you have to make sure that this precondition holds at the end of the while loop, before the computer jumps back to re-evaluate the test, as well as before the start of the loop.
What about error checking? Before reading the numerical measurement, we have to make sure that there is really a number there to read. Before reading the unit of measure, we have to test that there is something there to read. (The number might have been the last thing on the line. An input such as "3", without a unit of measure, is not acceptable.) Also, we have to check that the unit of measure is one of the valid units: inches, feet, yards, or miles. Here is an algorithm that includes error-checking:
inches = 0 skipBlanks() while TextIO.peek() is not '\n': if the next character is not a digit: report an error and return -1 Let measurement = TextIO.getDouble(); skipBlanks() // Precondition for the next test!! if the next character is end-of-line: report an error and return -1 Let units = TextIO.getWord() if the units are inches: add measurement to inches else if the units are feet: add 12*measurement to inches else if the units are yards: add 36*measurement to inches else if the units are miles: add 12*5280*measurement to inches else report an error and return -1 skipBlanks() return inches
As you can see, error-testing adds significantly to the complexity of the algorithm. Yet this is still a fairly simple example, and it doesn't even handle all the possible errors. For example, if the user enters a numerical measurement such as 1e400 that is outside the legal range of values of type double, then the program will fall back on the default error-handling in TextIO. Something even more interesting happens if the measurement is "1e308 miles". The number 1e308 is legal, but the corresponding number of inches is outside the legal range of values for type double. As mentioned in the previous section, the computer will get the value Double.POSITIVE_INFINITY when it does the computation. You might want to run the program and try this out.
Here is the subroutine written out in Java:
/** * Reads the user's input measurement from one line of input. * Precondition: The input line is not empty. * Postcondition: If the user's input is legal, the measurement * is converted to inches and returned. If the * input is not legal, the value -1 is returned. * The end-of-line is NOT read by this routine. */ static double readMeasurement() { double inches; // Total number of inches in user's measurement. double measurement; // One measurement, // such as the 12 in "12 miles" String units; // The units specified for the measurement, // such as "miles" char ch; // Used to peek at next character in the user's input. inches = 0; // No inches have yet been read. skipBlanks(); ch = TextIO.peek(); /* As long as there is more input on the line, read a measurement and add the equivalent number of inches to the variable, inches. If an error is detected during the loop, end the subroutine immediately by returning -1. */ while (ch != '\n') { /* Get the next measurement and the units. Before reading anything, make sure that a legal value is there to read. */ if ( ! Character.isDigit(ch) ) { System.out.println( "Error: Expected to find a number, but found " + ch); return -1; } measurement = TextIO.getDouble(); skipBlanks(); if (TextIO.peek() == '\n') { System.out.println( "Error: Missing unit of measure at end of line."); return -1; } units = TextIO.getWord(); units = units.toLowerCase(); /* Convert the measurement to inches and add it to the total. */ if (units.equals("inch") || units.equals("inches") || units.equals("in")) { inches += measurement; } else if (units.equals("foot") || units.equals("feet") || units.equals("ft")) { inches += measurement * 12; } else if (units.equals("yard") || units.equals("yards") || units.equals("yd")) { inches += measurement * 36; } else if (units.equals("mile") || units.equals("miles") || units.equals("mi")) { inches += measurement * 12 * 5280; } else { System.out.println("Error: \"" + units + "\" is not a legal unit of measure."); return -1; } /* Look ahead to see whether the next thing on the line is the end-of-line. */ skipBlanks(); ch = TextIO.peek(); } // end while return inches; } // end readMeasurement()
The source code for the complete program can be found in the file LengthConverter2.java.