Graphic by Keith Ohlfs |
Pre-conditions, Post-conditions, |
[CS111 Home Page] [Syllabus] [Lecture Notes] [Assignments] [Labs] [Programs] [Documentation] [Software Installation] [FAQ] [CS Dept.] [CWIS]
When we write methods, we need to define for ourself and others how we expect our environment to be before and after the method. These expectations should be explicitly documented in the contract for the method.
Often, a method is written under the assumption that certain things are true about the environment: these are called pre-conditions. For instance, consider the following buggle method:
public void square1 (int n) { this.forward(n); this.left() this.forward(n); this.left() this.forward(n); this.left() this.forward(n); }
A first cut at the contract of square1() might be:
Causes this buggle to draw a square whose sides consist of n+1 cells. Uses this buggle's current color as the color of the square.
But the buggle will only draw the square if its brush is down when the method is invoked. The brush being down is a pre-condition for the method to work as expected. In a contract for a method, pre-conditions can be stated as assumptions. For example, here is a second cut at the contract for square1():
Assume that this buggle's brush is down. Causes this buggle to draw a square whose sides consist of n+1 cells. Uses this buggle's current color as the color of the square.
There are other assumptions necessary in order for square1() to work as expected. For instance there can't be any walls in the path of the square, or else the buggle will get stuck. This could also be stated as an explicit pre-condition, but for simplicity we will assume that it is understood that buggles cannot draw through walls.
A post-condition indicates the changes made to the state of the world by invoking a method. In the case of buggles and turtles, it is important to document any changes to their state as a result of executing a method. For instance, because the square1() method brings the buggle back to its initial position, but because it only turns the buggle left three times, the buggle ends up in a direction to the right of its initial direction. It is important to document such post-conditions in the contract. Here is a final cut at the square1() contract:
Assume that this buggle's brush is down. Causes this buggle to draw a square whose sides consist of n+1 cells. Uses this buggle's current color as the color of the square. The buggle's final position, color, and brush state are the same as its initial ones, but its final heading is 90 degrees to the right of its initial heading.
Aspects of the state that remain unchanged by the invocation of a method are called invariants. Thus, the buggle's position, heading, and brush state are invariants of square1(), but its heading is not.
When writing methods that change the state of objects like buggles or turtles, it is often a good idea to have as many invariants as possible. This simplifies reasoning about sequences of method invocations. For example, suppose we want to draw three nested squares that share a corner. Using square1(), we can do this as follows:
public void nestedSquares (int n) { this.square1(n); this.left(); this.square1(n+2); this.left(); this.square1(n+3); }
The fact that square1() effectively turns the buggle right means that it is necessary to call left() to set the buggle's heading properly for the next square. Note that the buggle's heading is not an invariant of nestedSquare().
Writing methods like nestedSquares() would be simpler if the square-drawing method did not change the heading of the buggle. Below is a square() method that leaves the position invariant, and the corresponding version of nestedSquares():
// Assume that this buggle's brush is down. Causes this buggle to draw a square whose sides consist // of n+1 cells. Uses this buggle's current color as the color of the square. The buggle's position, // heading, color, and brush state are invariants of the method. public void square (int n) { this.forward(n); this.left(); this.forward(n); this.left(); this.forward(n); this.left(); this.forward(n); this.left(); } // Assume that this buggle's brush is down. Causes this buggle to draw a three squares whose sides consist // of n+1, n+3, and n+5 cells, respectively. The squares share the corner at the buggles initial position. // Uses this buggle's current color as the color of the square. The buggle's position, // heading, color, and brush state are invariants of the method. public void nestedSquares (int n) { this.square(n); this.square(n+2); this.square(n+3); }
Invariants are especially important in cases where an object asks
questions about the environment. In these cases, we don't usually
expect that asking a question will change the object or its
environmen. As an example, let's write a method that will determine
if there is a wall to our buggle's right. Well, we could do the
following:
public boolean isWallToRight () { this.right(); // turn right return isFacingWall(); // return the result from asking if we're facing a wall }
So, what's wrong? Well, nothing initially, the method does give us
the answer we want. However, things get confusing if we try to use it
in a program. After we've asked the question, our buggle is no longer
facing its original direction. Instead, it's facing the direction
that's to the right of its original direction. Ok, no problem. Let's
do the same thing for figuring out if there is a wall to our buggle's
left:
public boolean isWallToLeft () { this.left(); // turn left return isFacingWall(); // return the result from asking if we're facing a wall }
Again, this works. However, if you ask the buggle if there is a
wall to its left, it will end up facing the direction left of its
original direction. Where is the problem? Let's say we want to figure
out whether or not our buggle is in a hallway. Our buggle is in a
hallway if there is a wall to the left and to the right of the
buggle. So, let's define a new method called isInHallway like
so:
public boolean isInHallway () { return (isWallToLeft() && isWallToRight()); // buggle is in hallway if there is a wall to the left and to the right }
Does this work? No. Why not?
Well, let's assume that the buggle is in the middle of a hall (so
there is no wall in front or behind). When we ask
isWallToLeft, the buggle turns to the left and answers
true. Now when we ask it isWallToRight, the buggle
turns to the right and gives us what answer? Well, the buggle is now
facing it's original direction with no wall in front, so it tells us
false. Therefore, our method isInHallway also returns
false (since true && false = false). In order
for our isInHallway method to work, we need to have the buggle
figure out whether there are walls to its left and right without
changing its position. It's not really possible to do so without
changing the buggle's position, but we can put the buggle back into
its original position before answering the question. So, the better
way to write isWallToLeft and isWallToRight
follows:
public boolean isWallToLeft () { this.left(); // turn left boolean result = isFacingWall(); // remember result from inquiry this.right(); // return to the original position; undo the this.left() return result; } public boolean isWallToRight () { this.right(); // turn right boolean result = isFacingWall(); // remember result from inquiry this.left(); // return to the original position; undo the this.right() return result; }
With the redefined methods above, our isInHallway method will now work correctly.
Moral of the story: Methods that answer questions about the state of the environment should not change the state of the environment!