Modern Comments
I recently read a blog post arguing that comments are an important part of code, and the people who claim otherwise are missing out. While I tend to fall on the opposite side of the argument, I would certainly agree that there is a time and a place where comments in code make sense. More interestingly, I realized that I haven’t seen many explanations of why comments are sub-optimal, and when they are still important. So, in an effort to improve that situation, here’s my take on the principles behind comments, and when those same principles are better served with different tools.
The Origin of Comments
In understanding why you might want comments, it’s useful to first understand the context in which comments were created. Early programming languages were concerned mainly with the mechanics of computing. They were also extremely verbose, with even simple operations requiring a number of different statements. These problems are compounded by the fact that the statements are not designed to convey meaning to a human, they are instructions for a (amazingly limited) machine.
To better make the point, let’s look at an example of well-written assembly code. Specifically, this is a small sample of the Apollo Command Module’s guidance computer code1. This is, quite literally, code good enough to put men on the moon. The engineers who wrote this code knew that a mistake could end up killing an Apollo crew, and so used the best tools available to them to ensure success. Those tools included code comments, which I have removed from the following sample.
BUTTONS TC LIGHTSET
CAF HI5
MASK ERESTORE
EXTEND
BZF +2
TCF NONAVKEY +1
CS ERESTORE
EXTEND
BZF ELRSKIP -1
AD SKEEP7
EXTEND
BZF +2
TCF NONAVKEY +1
CA SKEEP4
TS EBANK
EXTEND
DCA SKEEP5
INDEX SKEEP7
DXCH 0000
CA ZERO
TS ERESTORE
TC STARTSUB
Without the comments, assembly and its ilk quickly become unintelligible. Even after sophisticated compilers allowed the creation of higher-level programming languages, there were still constraints on the number of function calls and the length of variable names that limited the ability of programmers to express themselves clearly in the code.
Given these constraints, comments were “the worst form of context, except for all those others that have been tried"2. Comments still had all the same problems that we see today, but there wasn’t a better solution available. Indeed, there are still some programming environments that have constraints that limit our ability to include context with our code. Given those constraints, writing appropriate comments is an important skill for a developer.
Context Without Comments
The whole point of comments is to provide context to a fellow human being that is not present in the code. Since I believe that providing context is one of the most important duties of a software developer, you might think that I’d be a big advocate for comments. So why, instead, am I writing a blog post trying to convince you that you may not need any comments in your code at all?
The major problem I have with comments is that they divorce the context that they provide from the actual decision. In constrained environments, they may represent the best you can do. When you can write code that includes the context of why, though, many of the problems that come from having two independent representations of a solution disappear. There are probably as many ways to attempt this as there are people who write code, but two that I have found very powerful are expressive code and good unit tests.
Expressive Code
One of the biggest tools that helps embed context in code is the rise of expressive code features. In most environments, local function calls are close enough to free that the only time that we run into call stack depth issues is when recursion goes bad. The only effective limit on variable names is how much you’re willing to type, so we don’t have to include comments about what iLoc2
actually means.
Consider this example from Pluralsight.Maybe
, the library we discussed in an earlier post:
public static Maybe<T> FirstOrNone<T>(this IEnumerable<T> self) where T : class
{
return self.FirstOrDefault().ToMaybe();
}
In (effectively) two lines of code, we learn that FirstOrNone
is an extension method on IEnumerable<T>
that returns the first item in the collection or Maybe<T>.None
. There’s a lot going on behind the scenes to make that actually work, but the whole point of abstractions is to enable you to ignore some technical complexities and instead work on generalizations. And while this is an extreme example, I’ve written a lot of code that effectively can’t be usefully commented, because the comments would just duplicate what the code is actually doing.
GUTs
Good unit tests3 are a second, powerful way to embed context in your code. A lot of that power comes from expressive code, but the fact that you’ll never call your tests frees you from even the (relatively minor) constraints that we tend to put on production code. For example, let’s look at some of the tests from Pluralsight.Maybe
:
public class When_getting_the_value_and_there_is_a_value : ContextSpecification
{
Because of = () => result = maybe.Value;
It should_return_the_value = () => result.ShouldEqual("hi");
static Maybe<string> maybe = Maybe.Some("hi");
static string result;
}
public class When_getting_the_value_and_there_is_NOT_a_value : ContextSpecification
{
Because of = () => result = Catch.Exception(() => { var x = maybe.Value; });
It should_throw_an_invalid_operation_exception = () => result.ShouldBeType<InvalidOperationException>();
static Maybe<string> maybe = Maybe<string>.None;
static Exception result;
}
When_getting_the_value_and_there_is_NOT_a_value
and should_throw_an_invalid_operation_exception
are not the kind of names that I want to see in “normal” code. But they do a great job of describing, in English, what is being tested, and what I can expect when calling .Value
.
The power of unit tests as comments also comes from the fact that they can be tested by a computer (you are running your unit tests in an automated fashion, right?). It’s all too easy for comments to become accidentally divorced from the truth over time, as code moves around and context changes. As professionals, we try to stop that from happening, but the only effective way to do that requires human vigilance. By letting a computer do the hard work of double checking that your comments are still true, you only have to pay attention when something changes.
Conclusion
Comments provide the simplest and most universal way to include additional context around our code. In some situations, they are the only effective way provide that context. In many cases, however, there are ways to include that context that are integrated with the actual code, protecting us from the problems that arise when context and implementation are separated.
1 The full source code of the Apollo Guidance Computer is available on Github. This sample was taken from FRESH_START_AND_RESTART.agc
2 With apologies to Winston Churchill
3 For a much more in depth discussion of what makes a good unit test, I recommend this talk by Kevlin Henney