What is wrong with this code? (If you’re a lazy reader like I am, don’t bother reading in detail — just briefly glance at the “shape” of the code.)
int main()
{
int rv = 0;
FILE * fin = fopen("foo", "r");
if (fin) {
FILE * fout = fopen("bar", "w");
if (fout) {
int count;
for (count = 0; count < 4; ++count) {
int i, j;
int nread = fscanf(fin, "%d%d", &i, &j);
if (nread == 2) {
if (j < 0) {
fprintf(stderr, "Negative divisor unsupportedn");
rv = 1;
}
else if (j == 0)
fprintf(fout, "%dn", i);
else // j > 0
if (i < 0)
fprintf(fout, "%dn", -(-i % j));
else
fprintf(fout, "%dn", i % j);
}
else {
fprintf(stderr, "Scanf errorn");
rv = 2;
break;
}
}
fclose(fout);
}
else {
fprintf(stderr, "Output errorn");
rv = 3;
}
fclose(fin);
}
else {
fprintf(stderr, "Input errorn");
rv = 4;
}
return rv;
}
It’s well structured. Its locals are tightly scoped with resources appropriately recovered. It skimps on curly braces probably too much in the middle to save vertical space but is otherwise correct. It appears to cover all error cases. It is about the right size such that factoring out a subroutine doesn’t really gain you much…. By all accounts it is pretty well-written code. And yet, it seems rather difficult to read and comprehend. This difficulty can be attributed to the diagonal nature of the code.
Consider, instead, this verticalized version of the same code:
int main()
{
FILE * fin = fopen("foo", "r");
if (!fin) {
fprintf(stderr, "Input errorn");
return 4;
}
FILE * fout = fopen("bar", "w");
if (!fout) {
fprintf(stderr, "Output errorn");
fclose(fin);
return 3;
}
int rv = 0;
int count;
for (count = 0; count < 4; ++count) {
int i, j;
int nread = fscanf(fin, "%d%d", &i, &j);
if (nread != 2) {
fprintf(stderr, "Scanf errorn");
rv = 2;
break;
}
if (j < 0) {
fprintf(stderr, "Negative divisor unsupportedn");
rv = 1;
continue;
}
if (j == 0) {
fprintf(fout, "%dn", i);
continue;
}
// j > 0
if (i < 0)
fprintf(fout, "%dn", -(-i % j));
else
fprintf(fout, "%dn", i % j);
}
fclose(fout);
fclose(fin);
return rv;
}
The linear layout of this version reads more naturally like a narrative, and the code is also more compact, with fewer levels of indentation to track, and occupying fewer columns of text. Over time I have found that coding in this vertical style significantly improves the understandability of code, and believe it should be encouraged as a “best practice” for coding.
Note that the linear nature of this style lines up with the logic of many programming situations, where the important work occurs along a single “track” surrounded by smaller offshoots, most of which are some form of error checking. The goal of vertical programming, then, is to align this single “track” down the vertical axis to make it easier for someone else to internalize it. Remember, an important and too-often-neglected aspect of coding is communicating with future programmers who have to maintain your code in your absence, so the simpler it is to read, the more maintainable the code. In that respect, vertical programming can even encourage one to simplify the problem to a single “track” and code to that track.
There are no hard and fast rules about how to code vertically (which is why I call it an “art”), but some guidelines that I’ve found useful (for structured imperative languages like C, Java, Python, /bin/sh) include:
- Avoid else clauses whenever possible. In particular, if the predicate is just filtering out some condition, have the if clause escape the scope via a break, continue, return statement. I’ll occasionally use a do-while(0) block to let me break out of a scope easily.
- Try not to indent a big block of code unless it is a loop. In many situations you can avoid indenting a big if block by flipping the condition and breaking out of the scope (per guideline 1).
- When you really need if-then-else blocks that are substantial in size, factor out the blocks into subroutines. It is easier to understand this:
if (condition)
do_something();
else
do_something_else();
than the same with the implementations of do_something and do_something_else expanded inline, particularly if the names of the factored subroutines are descriptive and accurate. (This guideline actually applies to any large code block.)
So why don’t more people code this way? In large part I believe it is due to programming language design. For example, the guidelines listed above don’t work with (purely) functional languages, where diagonal code is not only the norm, it is often unavoidable (e.g., there’s no such thing as an if without an else). The programming languages community, to which GrammaTech belongs (after all, the “Gramma” comes from “Grammar”), has a very strong affinity for trees (parse trees, syntax trees, computation trees), and trees tend to render diagonally rather than vertically.
Although trees have very nice properties that make them great for organization and analysis, they’re surprisingly difficult to render in a way that is easily comprehended by a human reader. In fact, GrammaTech’s first product from the early 80’s, the Synthesizer Generator, which may well be the first IDE, tried to address this difficulty, as have later IDEs like Visual Studio and Eclipse.
And in spite of modern GUI effects like collapsible blocks and floating tool tips, trees with more than a couple levels of nesting depth continue to be difficult to internalize. Ultimately, it boils down to the fact that the human field of view – our most efficient input channel (until we get brain implants) – can only fit a limited number of horizontal columns of characters (probably not much more than the 80 column limit common among terminals and coding conventions), and it doesn’t take many indentations to run out of horizontal real estate.
In summary, as long as we continue to code in text-based renderings of tree-based languages, the ability to minimize indentation levels can improve code readability and maintainability. Programmers should try to code vertically, while language designers should improve support for doing so (like the vertically-aligned sequential let-clauses in Lisp and ML). Or perhaps I’m just being naive and short-sighted: perhaps the future of programming will involve touch-screen swipes and pinches and as-yet-uninvented finger gestures, and all these “text file indentation” issues will become a relic of history. Given the continued pervasiveness of legacy code out there, however, I suspect this will not be the case.