Previous: Where are Python macros?
Let me say few words about unstructured programming.
First of all, almost nobody can do it any more. You don't believe me? Just try writing you next project in unstructured way, Instead of:
if(x) {
do_stuff1();
do_stuff2();
}
write:
if(!x) goto stuffed;
do_stuff1();
do_stuff2();
stuffed:
Very soon you'll feel that creeping sense of insecurity: Am I doing it right? If this thing going to be maintainable? And what are the best practices anyway?
To be fair, the above doesn't apply to programmers who are fluent in assembly, but everyone else is encouraged to actually do this as an exercise. If nothing else, it will broaden your mind. For extra fun have a look at the Duff's device before and after the exercise. What have seemed like a terrible hack before will turn into a reasonable solution, even to a thing of beauty.
Anyway, the real point I wanted to make today is that the programming languages called "structured" still have a lot of unstructured traits. And it's not because their authors sucked at programming language design and let the old, discredited constructs leak into modern languages, but because fully structured language is, for all practical purposes, unusable.
To be clear, structured language is a language where the control flow follows the syntactic structure of the code, where there are no random cross-jumps from one branch of the parse tree to another.
And yes, this time it's not the vilified goto who's the main culprit, but rather the humble and innocent 'return', along with 'break' and 'continue'.
Consider this program:
while(1) {
do_stuff1();
if(condition())
break;
do_stuff2();
}
And here's its fully structured counterpart:
int done = 0;
while(!done) {
do_stuff1();
if(condition())
done = 1;
if(!done) {
do_stuff2();
}
}
And while the latter program may be only slightly ugly, trying to return from inside three nested loops in a structured way turns into an actual coding horror.
You may have already observed that the reason why unstructured traits aren't still showing any signs of demise from the modern programming languages is readability. Dijkstra argued that unstructured programming hurts readability (and thus maintainability) but now it turns out that even the structured way, if applied over-zealously, will do the same thing.
This argument looks like a straw man. In the end, nobody sane would write the fully structured program above. So what am I ranting about?
Well, it turns out that some programmers don't take full advantage of the unstructured traits in their language, resulting in some pretty ugly code:
void foo(void) {
if(condition1()) {
do_stuff1();
}
else {
if(condition2()) {
do_stuff2();
}
else {
if(condition3()) {
do_stuff3();
}
else {
do_stuff4();
}
}
}
}
Why not write it like this:
void foo(void) {
if(condition1()) {
do_stuff1();
return;
}
if(condition2()) {
do_stuff2();
return;
}
if(condition3()) {
do_stuff3();
return;
}
do_stuff4();
}
It's good to understand why the latter example is more readable than the former one: It is not only a matter of the fact that large number of nested blocks tend to push the code out of the right side of the screen. More importantly, reading nested blocks means you have to maintain more context in your mind. If there are 5 nested blocks and you are reading the innermost one you are effectively keeping a 5-element parse stack in your mind, for the single purpose of being able to continue reading when you enconter the next 'else' statement.
Here's my advice: When creating a new nested block, spend a second considering whether the same thing cannot be achieved by simply by using 'return', 'break' or 'continue'. It may require moving some bits around but the resulting flat and readable code is definitely worth the hassle.
Martin Sústrik, July 18th, 2015
Previous: Where are Python macros?
One of the codebases I had been working on pushed this style even further:
Well, the way you presented your "ugly" code is a bit convoluted. You would probably write it like this:
I've showed the simplest possible example. If you look at real-world examples you'll find out that lot of stuff out there cannot be nicified in this way.
In particular I often have code that looks like this:
i.e. the fact that additional calculations are needed between ifs means that else-if cannot be used, and the performance hit of putting all calculations up front is unacceptable when most calls will (intentionally) fail.
I've used this style for 20 years for efficiency and clarity. It is much more clear than 'only return once' style code. When I wrote a textbook with an example using this style it garnered a bad Amazon review from someone who complained it showed I was a terrible programmer, because I didn't follow 'best practice' as taught in their Java course. Not that I'm bitter, or anything…
The complaint is that, if I need to do something for every failure (like adding a log, say), it is easy to forget a place to add it. I'm increasingly thinking that goto is actually a pretty good solution, and labels are a form of documentation. Though my need to be seen as non heretical probably stops me from using it in anger:
Most of serious C code uses goto for error handling. I wouldn't worry much about being seen as a heretic by C++/Java people. Just tell them to check the C best practice.
Beautiful reference!
I often use early return as illustrated following the principal of avoiding deep nesting. One of my favorite refactorings is adding return to a the first branch and removing the matching else with an outdent of its block.
Yeah, I often do the same.
However, more often flattening the structure is harder to achieve and one must actually think and experiment with moving pieces around.
if do_stuff*() require a lot of surrounding context so cannot easily be factored out into a seperate function, would you advocate for using break over nested if()s?
Sorry about the broken formatting, I didn't realize the indentation will collapse and I can't edit the post now. INDENT(1) is your friend… ;-)
Indentation fixed.
As for the question, I would use goto. It's much more readble than do/break/while(0) construct.
Structured counterpart for the unstructured program
is not necessary that ugly.
If you consider that do_stuff1 is called once unconditionally, then do_stuff2 and do_stuff1 are called until condition is satisfied, you can obtain better structured program:
Except you copy-pasted do_stuff1(), which may be a problem if it's not a single line but rather a sizable chunk code.
You can try to put such code into a function but then you end up with a lot of little functions which have no clear semantics of their own and exist only for structural reasons. That's not good for readability either.
Post preview:
Close preview