Karel the Robot Learns Python

Ch1   Ch2   Ch3   Ch4   Ch5

Chapter 4
Stepwise Refinement

When you are faced with a complex programming problem, figuring out how to decompose the problem into pieces is usually one of your most important tasks. One of the most productive strategies is called stepwise refinement, which consists of solving problems by starting with the problem as a whole. You break the whole problem down into pieces, and then solve each piece, breaking those down further if necessary.

An exercise in stepwise refinement

Suppose that Karel is initially facing east at the corner of 1st Street and 1st Avenue in a world in which each avenue may contain a vertical tower of beepers of an unknown height, although some avenues may also be empty. Karel’s job is to collect the beepers in each of these towers, and then carry them them all to the easternmost corner of 1st Street. Once there, Karel should put all the beepers into a single pile on that corner and then return to its starting position, as illustrated in the following before-and-after diagram:

before     after
Initial Beeper Towers world     Final Beeper Towers world

The key to solving this problem is to decompose the program in the right way. This task is more complex than the others you have seen, which makes choosing appropriate subproblems more important to obtaining a successful solution.

The principle of top-down design

The central idea in stepwise refinement is that you should start the design of your program from the top, which refers to the level of the program that is conceptually highest and most abstract. At this level, the beeper tower problem is clearly divided into three independent phases. First, Karel has to collect all the beepers. Second, Karel has to deposit them on the last intersection. Third, Karel has to return to its home position. This outline suggests the following decomposition of the problem:

def beeper_towers(): collect_all_beepers() drop_all_beepers() return_home()

At this level, the problem is easy to understand. Even though you have not written the code for the functions in the body of beeper_towers, it is important to convince yourself that, as long as you believe that the functions you are about to write will solve the subproblems correctly, you will have a solution to the problem as a whole.

Refining the first subproblem

Now that you have defined the structure for the program as a whole, it is time to move on to the first subproblem, which consists of collecting all the beepers. This task is itself more complicated than the problems you have seen so far. Collecting all the beepers means that you have to pick up the beepers in every tower until you get to the final corner. The fact that you need to repeat an operation for each tower suggests that you need to use a while loop.

But what does this while loop look like? First of all, you should think about the conditional test. You want Karel to stop when it hits the wall at the end of the row, which means that you want Karel to keep going as long as the space in front is clear. The collect_all_beepers function will therefore include a while loop that uses the front_is_clear test. At each position, you want Karel to collect all the beepers in the tower beginning on that corner. If you give that operation a name like collect_one_tower, you can then write a definition for the collect_all_beepers function even though you haven’t yet filled in the details. You do, however, have to be careful. To avoid the fencepost problem described in Chapter 3, the code must call collect_one_tower after the last cycle of the loop, as follows:

def fill_all_potholes(): while front_is_clear(): collect_one_tower() move() collect_one_tower()

As you can see, this function has the same structure as the fill_all_potholes function in Figure 3-1. The only difference is that collect_all_beepers calls collect_one_tower where the earlier one called fill_pothole. These two programs are each examples of a general strategy that looks like this:

def fill_all_potholes(): while front_is_clear(): Perform some operation. move() Perform the same operation for the final corner.

You can use this strategy whenever you need to perform an operation on every corner as you move along a path that ends at a wall. If you remember the general strategy, you can quickly write the code whenever you encounter a problem of a similar form. Reusable strategies of this sort come up frequently in programming and are referred to as programming idioms or patterns. The more patterns you know, the easier it will be for you to find one that fits a particular type of problem.

Coding the next level

Even though the code for collect_all_beepers is complete, you can’t run the program until you implement collect_one_tower. When collect_one_tower is called, Karel is standing either at the base of a tower or on an empty corner. In the former case, you need to collect the beepers in the tower. In the latter case, you can simply move on.

The collect_one_tower function is still complex enough that an additional level of decomposition makes sense. To collect all the beepers in a tower, Karel has to climb the tower to collect each beeper, turn around, and then return to the wall that marks the southern boundary of the world. These steps suggest the following code:

def collect_one_tower(): if beepers_present(): turn_left() collect_beeper_line() turn_around() move_to_wall() turn_left()

The turn_left instructions at the beginning and end of the code inside the body of the if statement are critical to the correctness of this program. When collect_one_tower is called, Karel is always somewhere on 1st Street facing east. When it has completed its operation, the program works correctly only if Karel is once again facing east. Conditions that must be true prior to a function call are preconditions; conditions that must apply after the function finishes are postconditions.

Finishing up

Although the hard work has been done, a few loose ends still need to be resolved because several functions are as yet unwritten. Fortunately, each of these functions can easily be coded without any further decomposition.

Figure 4-1 shows an animation of the beeper_towers program. This animation operates a little differently from the ones in the earlier chapters to emphasize the idea that it is easier to understand large programs if you think about just one function at a time. Each time you step into a new function, the animation produces a new overlay called a stack frame that contains the code for the function you’re calling. The new stack frame slides into the trace window, covering up the stack frame from the previous function. When the new function returns, its frame goes away, and the program continues from where it left off.

Figure 4-1. A program to solve the “Beeper Towers” problem
Ch1   Ch2   Ch3   Ch4   Ch5