When programmers write code, they also document that code; that is, they write natural language and a bit of mathematics to clarify what their code does. The computer certainly doesn’t need any such documentation (and even ignores it), so why should one take the time to write documentation? There are a host of reasons.
reduce, and a host of other procedures without understanding how they are defined.
As all three examples suggest, when we write code, we write not just for the computer, but also for a human reader. Even the best of code needs to be checked again on occasion, and lots of code gets modified for new purposes. Good documentation helps those who must support or modify the code understand it. And while humans should be able to read code, most read code more easily if the code has comments.
As you should have learned in Tutorial, every writer needs to keep in mind not only the topic they are writing about, but also the audience for whom they are writing. This understanding of audience is equally important when writing documentation.
One way to think about your audience is in terms of how the reader will be using your code. Some readers will be reading your code to understand techniques that they plan to use in other situations. Other readers will be responsible for maintaining and updating your code. Most readers will be using the procedures you write. You are often your own client. For example, you are likely to reuse procedures you wrote early in the semester. The documentation you write for your client programmers is the most important documentation you can write.
When thinking about those clients, you should first remember that they care most about what your procedures do: What values do they compute? What inputs do they take? Although you will be tempted to describe how you reach your results, most of your clients will not need to know your process, but only the result.
But you need to think about more than how your audience will use your code. You also need to think about what they know and don’t know. Because you are novices, you should generally plan to write for people like you: Assume that your client programmers know very little about Scheme, the kinds of things your program might do, even the terminology you use.
Different organizations have different styles of documentation. After too many years documenting procedures and teaching students to document procedures, Samuel A. Rebelsky developed a style that we find helps students think carefully about their work. (SamR has also received a few notes from alums and from other folks in industry who see this documentation and praise him for teaching it to students.)
To keep it easy to remember what belongs in the documentation for a procedure, Sam says that students should focus on “the Six P’s”: Procedure, Parameters, Purpose, Produces, Preconditions, and Postconditions.
The Procedure section simply names the procedure. Although the name of the procedure should be obvious from the code, by including the name in the documentation, we make it possible for the client programmer to learn about the procedure only through the documentation.
The Parameters section names the parameters to the procedure and gives them types. For example, if a procedure operates only on numbers or only on positive integers, the parameters section should indicate so.
The Purpose section presents a few sentences or sentence fragments that describe what the procedure is supposed to do. The sentences need not be as precise as what you’d give a computer, but they should be clear to the “average” programmer. (As you’ve learned in your other writing, write to your audience.)
The Produces section provides a name and type for the result of the procedure. Often, the result is not named in the code of the procedure. So why do we both to include such a section? Because naming the result lets us discuss it, either in the purpose above or in the preconditions and postconditions below.
These first four P’s give an informal definition of what the procedure does. The last two P’s give a much more formal definition of the purpose of the procedure. You’ve seen at the beginning of this reading that the preconditions are the conditions that must be met in order for the procedure to work and that preconditions and postconditions are a form of contract. Since they are a contract, we do our best to specify them formally.
The Preconditions section provides additional details on the valid
inputs the procedures accepts and the state of the programming system
that is necessary for the procedure to work correctly. For example,
if a procedure depends on a value being named elsewhere, the dependency
should be named in the preconditions section. The preconditions section
can be used to include restrictions that would be too cumbersome to put
in the parameters section. For example, in many programming languages,
there is a cap on the size of integers. In such languages, it would
therefore be necessary for a
square procedure to put a cap on the size
of the input value to have an absolute value less than or equal to the
square root of the largest integer.
When documenting your procedures, you may wish to note whether a precondition is verified (in which case you should make sure to print an error message) or unverified (in which case your procedure may return any value). At this point in your career, we will assume that most preconditions are unverified.
The Postconditions section provides a more formal description of the results of the procedure. For example, we might say informally that a procedure reverses a list. In the postconditions section, we provide formulae that indicate what it means to have reversed the list.
Typically, some portion of the preconditions and postconditions are expressed with formulae or code.
How do you decide what preconditions and postconditions to write? It takes practice to get good at it. We usually start by thinking about what inputs we are sure it works on and what inputs we are sure that it doesn’t work on. We then try to refine that understanding so that for any value someone presents, we can easily decide which category is appropriate.
For example, if we design a procedure to work on numbers, our general sense is that it will work on numbers, but not on the things that are not numbers. Next, we start to think about what kinds of number it won’t work on. For example, will it work correctly with real numbers, with imaginary numbers, with negative numbers, with really large numbers? As we reflect on each case, we refine our understanding of the procedure, and get closer to writing a good precondition.
The postcondition is a bit more tricky. We try to phrase what we expect of the procedure as concisely and clearly as we can, frequently using math, code when it’s clearer than the math, and English when we can’t quite figure out what math or code to write. But we always remember, consciously or subconsciously, that English is ambiguous, so we try to use formal notations whenever possible.
Note: When documenting preconditions, we generally don’t duplicate the type restrictions given in the Parameters section. You can assume that those are implicit preconditions. At times, those are the only preconditions.
Let us first consider a simple procedure that squares its input value and that restricts that value to an integer. Here is one possible set of documentation.
;;; Procedure: ;;; square ;;; Parameters: ;;; val, an integer ;;; Purpose: ;;; Computes the square of val. ;;; Produces: ;;;; result, an integer ;;; Preconditions: ;;; [No additional] ;;; Postconditions: ;;; (sqrt result) is val
You’ll note that we did not say that “result is val*val”. Why not? We generally try to focus on important characteristics of the result, rather than the process used to compute them.
What else might we think about? In Scheme, there’s not an upper limit to
the value of integers. In other languages, such a limit may exist. Let’s
suppose there is such a limit and it is called
MAXINT. In that case,
trying to square a value larger than the square root of
necessarily lead to an error. We might therefore add a precondition to
the documentation as follows.
;;; Procedure: ;;; square ;;; Parameters: ;;; val, an integer ;;; Purpose: ;;; Computes the square of val. ;;; Produces: ;;;; result, an integer ;;; Preconditions: ;;; (abs val) <= (sqrt MAXINT) ;;; Postconditions: ;;; (sqrt result) is val
You will note that the preconditions specified are those described in the
narrative section: We must ensure that
val is not too large. Here,
we started with the idea of numbers (or integers) and, as we started
to think about special cases, realized that the procedure would not
work with too large numbers. In reacting to the realization, we added
a restriction to the size.
In DrRacket, integers are not restricted, so there’s no reason to add that precondition. However, we do want to think more carefully about types. If we restrict ourselves to exact integers, we know that our computations are both arbitrarily large and do not lose accuracy. Hence, we can write something like the following.
;;; Procedure: ;;; square ;;; Parameters: ;;; val, an exact integer ;;; Purpose: ;;; Computes the square of val. ;;; Produces: ;;;; result, an exact integer ;;; Preconditions: ;;; [No additional] ;;; Postconditions: ;;; (sqrt result) = val
That seems fairly restrictive. We want to be able to square inexact integers, real numbers (both exact and inexact), and perhaps even complex numbers. So let’s write some more general documentation.
;;; Procedure: ;;; square ;;; Parameters: ;;; num, a number ;;; Purpose: ;;; Compute the square of num ;;; Produces: ;;; result, a number ;;; Preconditions: ;;; [No additional] ;;; Postconditions: ;;; If num is exact, (sqrt result) = num ;;; If num is inexact, (sqrt result) approximates num ;;; result has the same "type" as num ;;; If num is an integer, result is an integer ;;; If num is real, result is real ;;; If num is exact, result is exact ;;; If num is inexact, result is inexact ;;; And so on and so forth
You’ll note that we spent extra effort to discuss both the result and
the type of the result. When possible, we try to give client programmers
as much useful information as we can. Many programmers care to know
whether a computation produces inexact numbers (like
sqrt often does)
or always keeps the exactness the same.
Let’s look at another example. Suppose a colleague in another department comes to us and says “I was too harsh on the last exam; the average was only 60. The average should really be closer to 85. I need a program to scale the grades appropriately.” So, we start by thinking about the documentation. We’ll need to ask a few questions first.
Do you represent grades as whole numbers, or can grades have a fraction?
Oh, grades can have a fractional portion. For example, a student might have 72.4.
When you say that the average should be close to 85, do you mean that we should add 25 (that’s 85-60) or that we should multiply by 85/60?
Let’s just add 25.
Here’s a first attempt at the documentation.
;;; Procedure: ;;; scale-grades ;;; Parameters: ;;; grades, a list of real numbers ;;; Purpose: ;;; scale all of the grades in the list so that the average is 85. ;;; Produces: ;;; scaled-grades, a list of real numbers ;;; Preconditions: ;;; [No additional] ;;; Postconditions: ;;; For each grade in the list, we have added 25 points.
Is that enough? Probably not. It’s a bit vague how the grades in the second list correspond exactly to the grades in the first list. We could, for example, achieve the “average 85” goal by just creating a list with one element whose value is 85. Even if we cover all the grades, must they be in the same order? Let’s clarify.
;;; Procedure: ;;; scale-grades ;;; Parameters: ;;; grades, a list of real numbers ;;; Purpose: ;;; scale all of the grades in the list ;;; Produces: ;;; scaled-grades, a list of real numbers ;;; Preconditions: ;;; [No additional] ;;; Postconditions: ;;; (length scaled-grades) = (length grades) ;;; (average scaled-grades) is approximately 85 ;;; For all i, 0 <= i < (length grades) ;;; (list-ref scaled-grades i) = (+ 25 (list-ref grades i))
The use of Scheme clarifies things a bit. But we’re not done yet. We should think about the possible limits of grades. Can a student have a negative grade? We’d hope not. Can a student have a grade over 100 (e.g., if they started with a relatively high grade)? Let’s suppose we’ve asked and been told that the maximum grade is 105.
;;; Procedure: ;;; scale-grades ;;; Parameters: ;;; grades, a list of non-negative real numbers. ;;; Purpose: ;;; scale all of the grades in the list. ;;; Produces: ;;; scaled-grades, a list of non-negative real numbers. ;;; Preconditions: ;;; All numbers in grades are <= 105. ;;; Postconditions: ;;; (length scaled-grades) = (length grades) ;;; (average scaled-grades) is approximately 85 ;;; For all i, 0 <= i < (length grades) ;;; If (list-ref grades i) <= 80 ;;; (list-ref scaled-grades i) = (+ 10 (list-ref grades i)) ;;; Otherwise ;;; (list-ref scaled-grades i) = 105
But wait! If we’re not scaling all grades the same, then we may not achieve the average of 85. We’ll need to drop that guarantee and perhaps think of another one. It’s hard to say clearly what we’ll get. The scaled average has to be higher than the old average. It can’t be higher than 85. So let’s go with the following.
;;; 60 <= (average scaled-grades) <= 85
But can we really guarantee that? We’re taking their word that the average grade is 60. Maybe we should just rely on what we know.
;;; (average grades) < (average scaled-grades) <= 85
Are we sure about that? We know that none of the original grades is negative, so adding ten and scaling will make them larger. We know that none of them start greater than 105, so we can’t accidentally reduce one to 85. Yes, that’s safe. Of course, if the average started out higher than 85, this doesn’t work. So maybe we want to add that as a precondition.
;;; Preconditions: ;;; All numbers in grades are <= 105. ;;; (average grades) = 60
That would fix our earlier postcondition problem, too. But we’ll leave the postcondition as is. Are we done? Not quite. We’ve “hard coded” the strategy in our documentation. What if they decide to have us use a different formula, such as adding 10 and then multiplying by 85/70? Perhaps we should guarantee less.
;;; Procedure: ;;; scale-grades ;;; Parameters: ;;; grades, a list of non-negative real numbers. ;;; Purpose: ;;; scale all of the grades in the list. ;;; Produces: ;;; scaled-grades, a list of non-negative real numbers. ;;; Preconditions: ;;; All numbers in grades are <= 105. ;;; (average grades) = 60 ;;; Postconditions: ;;; (length scaled-grades) = (length grades) ;;; (average grades) < (average scaled-grades) <= 85 ;;; For all i, 0 <= i < (length grades) ;;; (list-ref scaled-grades i) > (list-ref grades i) ;;; (list-ref scaled-grades i) = 105
No, that doesn’t seem quite right. We haven’t, for example, guaranteed that the scaling is “fair”, in that grades retain their order. We’ll add one more postcondition.
;;; For all i,j, 0 <= i,j < (length grades) ;;; if (list-ref grades i) <= (list-ref grades j) then ;;; (list-ref scaled-grades i) <= (list-ref scaled-grades j)
That’s better. Will it stop the programmer from just giving everyone a grade of 105? Yes, since that would fail to meet the average postcondition.
There’s still more we might consider as we think about what to guarantee. For example, it could be useful to make the desired average a parameter to the procedure so that we can generalize it further. But we will leave such generalization for the future.
It took a bit of effort to get the documentation right, or close enough to right. We hope that it was useful effort. First, it required us to carefully think through what we wanted the procedure to do and to differentiate aspects of our current implementation from the more general goals. Second, it required us to think about special cases. We’ll find that many of the procedures we write work fine on many cases, but not on the more extreme cases, which we will often call “edge cases” or “corner cases”. In this instance, the procedure behaved differently on large components. Finally, we had to balance the needs of the client programmer and the implementing programmer. You’ll find that a lot of procedure design requires such a balancing act.
As noted above, the preconditions and postconditions form a contract with the client programmer: If the client programmer meets the type specified in the parameters section and the preconditions specified in the preconditions section, then the procedure must meet the postconditions specified in the postconditions section.
As with all contracts, there is therefore a bit of adversarial balance between the preconditions and postconditions. The implementer’s goal is to do as little as possible to meet the postconditions, which means that the client’s goal is to make the postconditions specify the goal in such a way that the implementer cannot make compromises. Similarly, one of the client’s goals may be to break the procedure (so that he may sue or reduce payment to the implementer), so the implementer needs to write the preconditions and parameter descriptions in such a way that she can ensure that any parameters that meet those descriptions can be used to compute a result.
Just in case you weren’t sure: The way we’ve described the adversarial relationship is clearly hyperbole. Nonetheless, it’s useful to think hyperbolically to ensure that we write the best possible preconditions and postconditions.
As the examples above suggest, the preconditions and postconditions help you think more carefully about exactly what you want the procedure to do and to help others understand that, too. But preconditions and postconditions can take a lot of thought. In many cases when you are writing “obvious” procedures or procedures whose primary audience is you, you may choose to write 4P’s, and do without preconditions and postconditions. We will try to indicate when only 4P’s are appropriate.
Although we typically suggest using the basic six P’s (procedure, parameters, purpose, produces, preconditions, and postconditions) to document your procedures, there are a few other optional sections that you may wish to include. For the sake of alliteration, we also begin those sections with the letter P.
In a Package section, you might name the group of code the procedure
is associated with. For example,
list-ref is in the package
In a Problems section, you might note special cases or issues that are not sufficiently covered. Typically, all the problems are handled by eliminating invalid inputs in the preconditions section, but until you have a carefully written preconditions section, the problems section provides additional help (e.g., “the behavior of this procedure is undefined if the parameter is 0”).
In a Practicum section, you can give some sample interactions with your procedure. We find that many programmers best understand the purpose of programs through examples, and the Practicum section gives you the opportunity to give clarifying examples.
In a Process section, you can discuss a bit about the algorithm you use to go from parameters to product. In general, the client should not know your algorithm, but there are times when it is helpful to reveal a bit about the algorithm.
In a Philosophy section, you can discuss the broader role of this procedure. Often, procedures are written as part of a larger collection. This section gives you an opportunity to specify these relationships.
At least one of the faculty who uses the six-P notation often adds a Ponderings section for assorted notes about the procedure, its implementation, or its use.
In an overly-ambitious attempt to stick within the constraints of this notation, at least one faculty member adds a Props section to provide citations and other crediting of work.
You may find other useful P’s as your program. Feel free to introduce them into the documentation. (Feel free to use terms that do not begin with P, too.)
Write the 6P documentation for the
bound-grade procedure you recently
We tend to focus on 6Ps. But there are others. How many P’s total? And what are they? Can you think of even more that might be useful?