Program Development

Introduction

Laboratory exercises thus far have involved writing short programs to solve simple problems. Because future tasks will involve larger, more complex problems, we need to develop strategies and practices that can help us to be effective and efficient in developing correct solutions to these problems.

Throughout the course, we will expand our problem-solving insights and techniques. Here we begin with three principles:

  1. Principle: Write code once, not multiple times — using mechanisms called functions and procedures.
  2. Principle: Comments can clarify thinking, reduce time for code development, and minimize the need for correcting errors after initial code is written.
  3. Principle: Write code incrementally, rather than all at once — a process called incremental program development.

This reading introduces each of these principles. First we need to define some additional, important terms.

When a solution to a problem might include the same code in several places, we need a mechanism to extract the details into a single, common place and then reference them as needed. In C, the mechanism to collect this common material is called a function or procedure. These are analogous to the lambda constructs you learned in Scheme.

When we separate the steps of a solution (collected into a procedure or function) and use them in several places, we are practicing procedural abstraction; one part of the code serves as a high-level outline of the steps of an algorithm using function/procedure calls, while the low-level details of those steps are given elsewhere and do not muddy the overall flow of the solution.

We should always test code as it is written. When errors are identified (i.e., because incorrect answers are produced), the error must be resolved. The process of finding and correcting errors is called debugging.

Write code once, not multiple times

Although this principle may seem obvious, applying it may require planning and insight. Programmers and developers often find points where they can edit and rewrite their code to improve it after it is "done". This is called "refactoring" code, and even the most experienced developers need to revise and improve their code over time.

Case Study

To illustrate the point, open this program in a new window and review it.

myroc-1.c

Notice that the program header identifies three main parts of the program. Although the code works without difficulty, at least three criticisms are possible.

  1. Because the application commands the robot to beep, an early section of the program identifies a sequence of pitch names and frequencies.¹ This listing has two big weaknesses:
    • A long listing of notes is error prone. For example, it would be easy to mistype
      const int pitchG6 = 1568;
      with a frequency of 1586 (switching the last two digits) or 1567 (mistyping the last digit).
    • This type of listing will likely be needed in many programs involving robot beeping, and retyping the notes in many programs would be tedious and time consuming, even with copy-and-paste.
  2. Part 3 exactly duplicates the code for Part 1: 13 lines of commands (plus 3 blank lines) are repeated. This repetition also has two weaknesses:
    • If we made an error in Part 1 (e.g., if rTurnRight (0.7, 0.5) should have been rTurnRight (0.5, 0.7), we would need to remember to make the correction in both places. Generally, whenever we wanted to change Part 1 (perhaps using different pitches), we would need to make the same change to Part 3.
    • When testing, knowing Part 1 is correct, does not mean we may assume Part 3 is correct. Because the code istyped separately, a typographical error (or other bug) in one part may or may not be present in the other part.
  3. The main flow of the program is obscured. Again, two weaknesses can be identified.
    • More than 1 dozen lines of details are combined with the high-level outline of the program, so it may be difficult for the author or another reader to follow both the high-level logic and the low-level details.
    • Adjusting one part of the program might undermine the logic in another part of the program.

With these weaknesses identified for myroc-1.c, we consider how to construct a better version, which we will produce incrementally and assemble at the end of the reading.

Headers for Abstracting Common Definitions

To begin, we create a new program, scale-notes.h, which contains C code to define the 88 notes on a piano. Rather than focus on just the four notes needed for this specific program, scale-notes.h defines the full range of pitches on the Western well-tempered scale.¹ We may not need all of these pitches in any one program. However, by having all pitches available, we can choose what we need; if we decide to expand the program later, the full range of pitches will already be available.

Once defined, we should place the file scale-notes.h in the directory with our other programs. Then we can incorporate it with a #include directive, in much the same way we incorporate standard libraries.

We will learn more about dividing a program into modules later in the semester. For now, you should note that when using a #include directive,

Operationally, when the C preprocessor encounters a #include directive, the preprocessor inserts the text of the specified file into the current program. Thus, the #include directive provides a way to to copy-and-paste exact copies of additional text into a file.

Functions for abstracting common algorithms

To address the challenge of duplicate code, we proceed in three steps:

  1. We declare a block of code with a name, following a specific format.
  2. We place the code block within braces { } following the declaration
  3. Within the main program itself, we use the given name to reference the code block.

This process helps us realize our goal of procedural abstraction.

Case study: Improved

Continuing our improvement of myroc-1.c, we will use the name forward_right to describe the common block of code for Parts 1 and 3. Within C the beginning of this block of code follows a specific format that also describes what the code will do.

/*
  Procedure to move the robot forward
     and right three times while playing
     an ascending musical chord
 */
void
forward_right (void)

Following this declaration, we place the desired block of code in braces. (The full program is shown below.)

As with any variable, the name of a function can be any combination of letters, numbers, and underscore characters (but should not start with a number).

In this example, the first appearance of the keyword void indicates the function will not return a value to the main program (or any other caller). The second appearance indicates it takes no parameters (or arguments). This code sends commands to the robot and prints text. When done, the main program does not expect a value in return.

When such a block of code does not return a value, it is often called a procedure. Thus, many programmers would designate forward_right as a procedure. However, when a block of code returns a value, it is often called a function. As an example, C's mathematics library includes a square root function: given a number, it returns the square root of that number. A possible line in a program might be

double root = sqrt (9.0);

Here, the sqrt is given 9.0 as an input value and returns 3.0 as the square root. Hence 3.0 is assigned to the variable root.

For this course, we will generally use the term function to refer to both, regardless of whether a value is returned from the block.

In the declaration above, the parentheses ( ) after forward_right indicate what follows is a function. Later in the course we will learn how to specify parameters for functions.

To use (or call) a function, we write its name with the parentheses.

forward_right ();

In C, you should declare a function before main, so the compiler will know what the function's label means while it is reading main. (Later we will encounter variations in how functions can be declared, but it is always safe to declare a function before it is first used.)

We could similarly define a function forward_left for Part 2. Although this code is only used once, defining a function will highlight the separate nature of those details. Thus, with forward_right and forward_left defined, the main procedure highlights the overall flow of the program.

A complete, revised program, with needed #include directives, is linked below. Open it and review the differences.

myroc-2.c

Much of the code in main is descriptive (e.g., comments, printing for the user). Because the details are placed in procedures, low-level details do not interfere with the high-level logic of the main program.

Comments

Our first reading on C programming emphasized that program code has at least three audiences: the author, other people, and computers. C's specific syntax and semantics allows computers to compile and run programs—largely handling communication to computers.

Comments and formatting can make a substantial difference in communication to the programming and to others.

Although comments can help others understand a program after it is written, comments can be particularly helpful to authors while they are writing the code. I often write notes to myself in the comments, setting up what the program needs to do BEFORE I write a single line of code.

Writing comments first (before code) can help an author understand the problem at hand. If the author truly understands the problem, writing comments is fast and easy — often requiring just a few minutes. Moreover, the act of writing comments can uncover fuzziness in the specification of a problem or ambiguity in what must be done.

Once the problem is understood, comments can help clarify an author's thinking. Writing effective comments requires full understanding of the algorithm involved and helps clarify the logical flow of a solution.

Altogether, programmers are urged to write comments early!. Your comments can be (should be?) informal and conversational. You should be able to see a difference between the comments in mortgage-good-comments.c and mortgage-poor-comments.c.

Write code incrementally, not all at once

Once an overall program is organized into pieces (e.g., with functions), an author often can write many elements of main. In getting started,

Such a skeletal program represents the intended overall structure of a solution, but with few details completed.

Stubs

A stub is a small block of code that identifies a logical step to be performed, but which has limited initial functionality.

For example, when starting to write the myroc-2.c program, the forward_right and forward_left functions might be simple stubs:

void
forward_right (void)
{
   printf ("function forward_right not yet implement\n");
} // forward_right

void
forward_left (void)
{
   printf ("function forward_left not yet implement\n");
} // forward_left

With function stubs, the overall program, as given above in the reading, can run using the proper structure, and the printf statements print text that checks the flow of operations.

Such "stubbed" code may not do much when development of a program begins, but the main pieces are in place, allowing the developer to fill-in pieces incrementally.

With this structure in place, a programmer can focus on one piece of the overall code at a time. There is no need to write all the details at once and then have to contend with possible issues in dozens or hundreds of lines of code!

By writing one piece of code at a time, a programmer can focus on a few lines, in writing, compiling, and running the program. Writing a short, focused piece of code is often less error-prone than writing a long, complex piece. Moreover, if something goes wrong, the error is likely to be in the short code segment that was just added. A programmer often can avoid looking at the entire program, if just one small piece has been inserted or changed.

Conclusion

In developing large programs, an important challenge is to manage complexity. If a complex solution can be divided into small tasks, then work can proceed methodically step-by-step, and a programmer does not have to keep many details and logical connections in mind all at once.

Decomposing the program into separate modules and functions, writing comments in English, and practicing incremental development techniques are all excellent strategies for managing the complex software development process.

Notes

1. Musical pitches

The names used for the pitches in myroc-1.c come from common labeling of keys on a piano.

Following Western musical tradition, notes on a scale are grouped into octaves. In particular, a piano has 88 keys, arranged in 12-note groupings. The lowest grouping is called octave 0 (with just 3 notes), the next octave 1 (with 12 notes), the next octave 2 (with 12 notes), and so forth through octave 7 (with 12 notes) and octave 8 (with just 1 note).

Within an octave, notes are labeled C, D, E, F, G, A, B. In addition, "half tones" are identified between some notes. For example, notes B flat and A sharp represent frequencies between A and B. On a piano, B flat and A sharp are the same (the scale is called well-tempered). On other instruments (e.g., a violin), distinctions are made between such half tones—for this course, we leave such matters to musicians and other courses.

In the myroc-1.c program, variables follow the naming convention: pitch, note label, and octave. Thus, pitchC6 is the frequency for the note C in the 6th octave. For notes involving sharps or flats, such a convention might use pitchAs0 and pitchBf0 for the notes A sharp and B flat in octave 0.

In Western music, frequencies are given as real numbers (with decimal points). Thus, pitchA0 is often specified as 27.5000 Hz and pitchC6 as 1046.50. For the Scribbler 2 robots, however, pitches must be integers, so these pitches must be rounded to either 27 or 28 for pitchA0 and 1046 or 1047 for pitchC6. Other pitches are rounded to the nearest integer without decision making.