Commenting code
Status: Done
Confidence: Likely
Young programmers are told that it is good to comment their code, and that is where the guidance on comments generally ends. This is equivalent to telling a student to write better without concrete guidance about what about their writing is poor and how to fix it. To remedy that, here are the five concrete uses of comments that I know of:
Stating non-technical information
The most generic use of comments is adding non-technical information to code. These are the license and copyright blocks at the top of many source files, and comments structuring a source file into sections. If you are using hunks of code from elsewhere or algorithms from a paper, you need to provide the attribution and references for these as well. Tagging bits of code that were fixed in non-obvious ways with a reference to an issue in a bug tracker lets a later reader track why the change was made.
At a minimum, each source file should have a header comment specifying the author, the copyright and who holds it, and the license. For example, in the Linux kernel we see headers such as
/* auditsc.c -- System-call auditing support
* Handles all system-call specific auditing features.
*
* Copyright 2003-2004 Red Hat Inc., Durham, North Carolina.
* Copyright 2005 Hewlett-Packard Development Company, L.P.
* Copyright (C) 2005, 2006 IBM Corporation
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
If you are using an open source license, it will generally provide verbiage to put in the header. In code that is not released publicly there won’t be a license header, but you should stil have a copyright notice stating ownership. This is important in case the code is taken out of context.
Documenting intended use
The following are some function and method names taken from real
systems: getRefresh
, parse
,
bfs_readdir
, dlm_dump_lkb_callbacks
. They are
followed by blocks of code. What are they for? When and how should are
they used? These are the first things a programmer needs to know when
reading code.
A comment documenting intended use implicitly takes the form of a set
of user stories: a programmer working on class X calls this function
when he needs to do Y because Z. A programmer calls this function with
these arguments under these conditions because blah. For example, in the
Splunk SDK for Python, there is a function localname
. It
takes one argument, called xname
. The user stories that
make its use clear are:
- A programmer parsing XML from the Splunk server calls
localname
on XML tags to get tag names without namespaces. - A programmer with a string containing XML tag name that may begin
with a namespace (contained in curly braces) can pass that string to
localname
to get a name with the namespace and curly braces removed.
The same thing applies to other entities in a program such as
classes. In the same project we find a class
_NoAuthenticationToken
. The user stories for it are:
- A programmer representing a connection to the Splunk server without
an authentication token uses the class
_NoAuthenticationToken
as the value of the token. - A programmer uses the class directly as a singleton value, and never constructs instances of it.
- A programmer testing for the presence of an authentication token
writes
token is _NoAuthenticationToken
.
Both lists are quite short. Something complicated, such as a class with instances and conditions about how its lifecycle must be managed might have more, but we wouldn’t expect really large numbers to be necessary. If they are, it’s a good indication that you should redesign this piece of code.
There are lots of good references on writing user stories, most of which apply here. Generally, for a function you should have stories around when a programmer would invoke this function (including what other parts there are of the code where those conditions apply), and stories around each distinct way of invoking the function that produces different behavior. For a class you would add stories around who initializes instances and when, and, in languages without garbage collected, how they are destroyed.
Unenforced type annotations
Functions in dynamically typed languages still expect arguments of certain types. Even in many statically typed languages there are cases where the type system has very little information about a value. This is particularly common when dealing with values in strings, such as different paths to files, and C has a longstanding convention of having integer valued functions return -1 for errors, which is invisible to its type system.
In all cases where you cannot statically assert the possible values of a type from a brief inspection of the code around the variable, you should comment what the set of possible values are. So this would not require a comment:
if condition:
a = "boris"
else:
a = "hilda"
print a
but if there were not that if statement immediately there enumerating
all possible values of a
that can end up in the
print
statement, then you should have a comment stating
what those values could be, such as:
print a # a is "boris" or "hilda"
If the values are paths to files to open but won’t change the behavior of how the program operates on the file, then there is no need to say anything further. If the behavior of the program will alter depending on the value, you should also comment on what values lead to what behaviors.
In languages with strong support for contracts, these comments may instead become code that is checked by the runtime.
Pre- and post-conditions
When faced with an inscrutable block of code, a comment on what it’s doing makes it much easier to understand. This is a particular case of something called a Hoare triple, which is the mathematical structure that underlies imperative programming languages. A Hoare triple associates to each statement or block of statements a pre-condition and a post-condition. The pre-condition states what must be true before the code is run in order for it to behave correctly. The post-condition states what the code has accomplished if it ran correctly.
Typically a block of code that works at a lower level of abstraction than those around it or executes a self contained step of an algorithm should be annotated with pre- and post-conditions.
For example, the following block of code requires several moments to decipher.
float an_array[N];
float threshold = 1.5;
int idx;
for (int i = 1; i < N; i++) {
if (arr[i] > last_max) {
idx = i;
break;
}
}
If we add pre- and post-conditions it becomes
float an_array[N];
float threshold = 1.5;
/* Precondition: an_array is sorted, least to greatest. */
int idx;
for (int i = 1; i < N; i++) {
if (arr[i] >= threshold) {
idx = i;
break;
}
}
/* Postcondition: idx = position of the first element of an_array
that is >= threshold */
Instead of deciphering the author’s intention by simulating the code, the reader can immediately know the intention and either pass on to the next block of code or check that the implementation satisfies the conditions, which is much easier.
Unidiomatic code
Every community of programmers has a collection of idioms that they recognize. For example, any competent C programmer can figure out the intention of
int i = 0;
while ((to[i] = from[i]) != '\0') {
++i;
}
almost effortlessly. To someone with no background in C, this seems like black magic. Haskell programmers have idioms for combining functions that seem very strange to non-Haskell programmers.
Sometimes you have to do something that breaks an idiom. It could be to work around a bug in an other part of your system. It could be to work with a lower level abstraction than is customary in your community. These cases should be commented so that when the reader jars to a halt on them there is some guidance to get them moving again.
Summary
These are the five times to comment code that I am aware of. You can treat them as a checklist to go over at each hunk of your code.
- Is there non-technical information that needs to be attached to this code?
- If this is an entity that can be referred to elsewhere in the program (a function, a class, a method, a type, etc.), what are its user stories?
- If a variable will contain only a subset of possible values of its type, or certain values have special meaning, what are they? How would you annotate this variable in an arbitrarily powerful type system?
- Would inserting pre- and post-conditions around a block of code make it easier to read?
- Is this code unidiomatic for members of my programming community?