Commenting code

October 29, 2014

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:

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:

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.


Did you enjoy that? Try one of my books:
Nonfiction Fiction
Into the Sciences Monologue: A Comedy of Telepathy