Spot The Invariant
Writing correct code in itself is not the most difficult and important thing programmers have to be able to do. By far the more difficult and crucial part of my profession is understanding code.
Understanding code is a factor in all of the following activities:
- Finding bugs in existing code
- Enhancing existing code
- Using libraries from new code
- Keeping a mental model of a system
And all of these activities are distributed over larger parts of a code base, whereas writing correct code is essentially a very localised activity. (Note that designing the architecture for an application or system can be much more difficult, but only needs to be done once, and is therefore not as big a part of the programmers life as understanding is).
And understanding isn’t always easy…
static int calculate(int n)
int a = 0, b = 1;
int x = 0, y = 1;
while (n != 0)
if ((n & 1) == 0)
int temp = <span class="hiddenGrammarError" pre=""><span class="hiddenGrammarError" pre="a ">a;
a</span></span> = a * a + b * b;
b = temp * b * 2 + b * b;
n = n / 2;
int temp = x;
x = a * x + b * y;
y = b * temp + a * y + b * y;
In fact, this fragment goes out of its way to give as few clues about what it does as possible, and yet it implements a very simple and well-known calculation.
Every language makes trade-offs in its syntax between being terse and being understandable. Perl is a famously terse language, where randomly mashing the keyboard is almost guaranteed to result in a valid program, whereas Java is well known to be verbose but relatively easy to understand.
Terse syntax has many obvious benefits. The terser the syntax, the more compact the code, the more source will fit on a single screen. And the more source fits on a screen the broader the overview you can get at a single glance over a fragment of code.
This is the rationale of many modern languages like Boo and Python and Ruby for example.
It seems to me though that the danger of making languages terse is that it optimises for ease of local understanding over easy of remote understanding. No matter how brief the source of a method is, if the method signature doesn’t give (m)any clues about the encapsulated functionality then using the method from another location in the source becomes needlessly difficult.
Naming is the first line of defense against incomprehensible code, and the only one that is guaranteed to exist in all languages (as far as I am aware). Classes get names, methods get names, often parameters get names. If these names are chosen well, both local and the global understanding are improved. This is why naming is such a big deal, and everyone intuitively understands this.
But often, names alone do not make the full constraints on parameters clear. This is where strongly typed languages have a further benefit. And even more so if there is at least optional explicit typing. Every parameter and return value that has a type implicitly tells something further about the meaning about the method (we’re operating on a collection, it holds strings, the result is a number, etc). I concede that small applications may be able to get away with pervasive dynamic typing, but larger systems often compensate by adding unit tests whose implicit purpose is to make sure methods operate on the right type of arguments and return the right type of results. But really, moving the typing information out of the method signature and into tests is in my opinion not progress.
Beyond this there are many more mechanisms that variously are employed to make code more understandable (documentation comments, code contracts, unit tests, word documentation), but as you stray further and further away from the actual source code and method signatures itself, the connection between the code and it’s constraint becomes ever more tenuous. Often this requires intervention from the IDE and third party tools and build checks, which move ever further away from the point where a developer is trying to understand the code in front of them.
I don’t really have a one-size-fits-all answer or a ready-made recipe to make code better, but here are a few suggestions.
- Use good names – don’t just look at this from the local perspective; what would a method invocation look like? Is the invocation self-documenting? For methods with multiple parameters, try and use the parameter names at the invocation site if your language will allow you to. Consider using names to compensate for the lack of other mechanisms; if your language of choice has no explicit typing, maybe calling an argument “
stringsToEliminate” is a good idea? Maybe calling an argument “
positiveInteger” is a good idea? But don’t make anything more specific than it needs to be.
- Use good types – even when typing parameters, pick the broadest type that will work. In .NET the compiler will keep telling you to use collection interfaces rather than explicit collection types. And sometimes creating a new type just so you can make one or more signatures more explicit may be a good trade-off; if a method only should take a value generated from a specific collection of other methods, then making the type “
double” is probably not as good an idea as using a custom “
- Use good tools – if you use any other mechanisms outside the language per-se, such as code contracts or source documentation, then you must have supporting tools as well. Source documentation is useless unless you have an IDE or tool that can “transport” this documentation from the method it is on to the place where it is invoked. Once you’re forced to go to the source of the implementation, odds are that a terse method implementation could more concisely explain what is going on already.
But first and foremost, pick a language that is suited to the problem at hand. Python is not a bad language, but I’m not sure it’s a good language for systems programming; it probably is better suited to web systems that often have many small self-contained features. Brevity is not a bad thing, as long as it doesn’t force you to work around an inability to use good names, or good types, or good tools.
Back to the Example
So, what of the example at the start of my post? It would have been much less opaque had the signature been along the following lines…
/// Calculate the n-th Fibonacci number using an
/// O(log N) algorithm.
static int Fibonacci(int n)
Contract.Requires(n > 12, "Result will overflow int");
Making the implementation itself easy to understand requires a few pages of explanation, and I’ll leave that as an exercise to the reader.
- Apparently, Perl users are unable to write programs more accurately than those using a language designed by chance – source: Lambda-the-Ultimate
- As of .NET 4, there’s a very useful Code Contracts mechanism available to C# developers; there are even plug-ins for the VS2010 IDE that can make these contracts visible as an implied part of the pop-up method documentation