10 bad programming habits we secretly love
We’ve all done it: snagged a cookie when mom wasn’t looking, had a little too much wine for dinner, let the car sit in a parking spot after the meter expired. We’ve even gone around Deadman’s Curve a bit too fast. And yes, we’ve all violated any number of the cardinal rules of programming, the ones that everyone agrees are bad. And we secretly liked it.
We’ve thumbed our nose at the rules of good programming, typed out code that is totally bad and we’ve lived. There were no lightning bolts from the programming gods. Our desktops didn’t explode. In fact, our code compiled and shipped, and the customers seemed happy enough.
That’s because bad programming isn’t in the same league as, say, licking an electric fence or pulling the tail of a tiger. Most of the time, it works out. The rules are more often guidelines or stylistic suggestions, not hard-and-fast directives that must be obeyed or code death will follow. Sure, your code might be ridiculed, possibly even publicly. But the fact that you’re bucking conventions adds a little bit of the thrill to subverting, even inadvertently, what amounts to (more often than not) the social mores of pleasant code.
To make matters more complex, sometimes it’s better to break the rules. (Shhhh!) The code comes out cleaner. It may even be faster and simpler. The rules are usually a bit too broad, and an artful programmer can improve the code by breaking them. Don’t tell your boss, but sometimes it makes sense to code your own way.
What follows is a list of nine rules that some may consider unimpeachable, but many of us break often, with both success and pleasure.
1: Copying
It’s wrong to do it in school. On the job, the rules are not so clear. There are certainly some blocks of code that shouldn’t be stolen. If it comes from proprietary code, don’t fold it into your stack, especially if it’s marked with a copyright message. Write your own version. It’s what they’re paying you to do.
The trickier question comes when the original creator wants to share. Perhaps it’s on one of those online programming fora. Perhaps it’s open source code with a license (BSD, MIT) that permits snagging a function or three. There’s no legal reason stopping you. And you’re paid to solve problems, not reinvent the wheel.
Most of the time, the advantages of copying are compelling and the disadvantages can be limited with a bit of care. The code you get from a reputable source has already had at least one round of thought applied to it. The original author searched for a solution and found something. The loop invariants and the data flow has been worked out.
The tricky questions are whether there are some unfound bugs or some different assumptions about the role or the underlying data. Perhaps your code mixes in null pointers while the original code never checked them. If you can fix the problems, it’s like your boss is getting the input from two programmers. It’s pair programming without the fancy desks.
2: Non-functional code
For the last decade or so, the functional paradigm has been ascending. The acolytes for building your program out of nested function calls love to cite studies showing how the code is safer and more bug-free than the older style of variables and loops, all strung together in whatever way makes the programmer happy. The devotees speak with the zeal of true believers, chastising non-functional approaches in code reviews and pull requests. They may even be right about the advantages.
But sometimes you just need to get out a roll of duct tape. Wonderfully engineered and gracefully planned code takes time, not just to imagine but also to construct and later to navigate. All of those layers add complexity, and complexity is expensive. Developers of beautiful functional code need to plan ahead and ensure that all data is passed along proper pathways. Sometimes it’s just easier to reach out and change a variable. Maybe put in a comment to explain it. Even adding a long, groveling apology to future generations in the comment is faster than re-architecting the entire system to do it the right way.
3: Non-standard spacing
Most spaces in software don’t have an effect on how the program performs. Except for a few languages like Python that use the spacing to indicate blocks of code, most spaces have zero effect on how the program behaves. Still, there are obsessive programmers who count them and insist that they matter. One of them once told my boss in the most serious tone that I was writing “Non Standard Code” and he could see it immediately. My sin? Violating the ESLint space-infix-ops rule by failing to put a space on both sides of an equal sign.
Sometimes you just have to think about something deeper than the placement of spaces. Maybe you’re worrying about the database getting overloaded. Maybe you’re worrying about some way that a null pointer could crash your code. Pretty much any part of the code is more important than the spaces, even if neb-nosed, bossy standards committees have filled pages of rules about the placement of these spaces or tabs.
The amazing thing is that there are several good tools that will automatically reformat your code to adhere to any well-defined linting rules. Humans don’t need to spend time thinking about this. If it’s so important, they can run it through the tool to clean up the problem.
4: Using goto
The prohibition on using goto
dates to the era before many of the tools of structured programming even existed. If programmers wanted to create a loop or jump to another routine, they would need to type GOTO
followed by a line number. After a few years, compiler teams let programmers use a string label instead of a line number. That was considered a hot new feature back then.
Some called the result “spaghetti code.” It was impossible for anyone to read your code later and follow the path of execution. It was a jumble of threads, forever tangled. Edsger Dijkstra banned the command with a manuscript drolly titled “Goto Statement Considered Harmful.”
But absolute branching isn’t the problem. It’s the tangle that results. Often an artful break
or return
will offer a very clean statement about what the code is doing at that location. Sometimes adding goto
to a case statement will produce something that’s simpler to understand than a more properly structured list of cascading if-then-else blocks.
There are counterexamples. The “goto fail” security hole in Apple’s SSL stack is one of the best instances. But if we’re careful to avoid some of the gnarly issues of case statements and loops, we can insert good, absolute jumps that make it easier for the reader to understand what’s going on. We can put in a break
or a return
that is cleaner and more pleasing for everyone except perhaps the goto
haters.
5: Not declaring types
The folks who love typed languages have a point. We write better, more bug-free code when we add clear declarations of the data type of each variable. Pausing a moment to spell out the type helps the compiler flag stupid errors before the code starts to run. It may be a pain, but it helps. It’s a belts-and-suspenders approach to programming that stops bugs.
Times have changed. Many of the newer compilers are smart enough to infer the type by looking at the code. They can work backward and forward through the code until they can be sure that the variable must be a string
or an int
or something else. And if these inferred types don’t line up, then the compilers can raise an error flag. They don’t need us to type the variables any more.
This means it’s now easier to save a few bits by leaving off some of the simplest declarations. The code becomes a bit cleaner, and the reader is usually quite able to guess that the variable named i
in a for loop is an integer.
6: Yo-yo code
Programmers like to call it “yo-yo code.” First the values are stored as strings. Then they’re parsed into integers. Then they’re converted back to strings. It’s terribly inefficient. You can almost feel the CPU struggle under all the additional load. Smart programmers who write fast code design their architectures to minimize the conversions. Their code runs faster because of their planning.
But believe it or not, sometimes it makes sense. Sometimes you have a whiz-bang library that does a bazillion intelligent things inside its proprietary black box. Sometimes the boss wrote a seven-figure check to license all of the genius inside that black box. If the library wants the data in strings, you give it to the library in strings even if you recently converted it to integers.
Sure, you could rewrite all of your code to minimize the conversion, but that would take time. Sometimes it’s OK for the code to run an extra minute, hour, day, or even week because rewriting the code would take even more time. Sometimes running up the technical debt is cheaper than building it right in the first place.
Sometimes the library isn’t proprietary code, but code you wrote yourself long ago. Sometimes it’s faster to convert the data one more time than rewrite everything in that library. So you go along and you write yo-yo code. It’s OK we’ve all been there.
7: Writing your own data structures
One of the standard rules is that a programmer should never write code for storing data after completing the data structures course in their sophomore year. Someone else has already written all of the data structures we’ll ever need, and their code has been tested and retested over the years. It’s bundled with the language and it’s probably free. Your code could only have bugs.
But sometimes the data structure libraries are a bit slow. Sometimes they force us into a structure that may be standard but wrong for our code. Sometimes the libraries push us into re-configuring our data before we use the structure. Sometimes the libraries include belts-and-suspender protections with features like thread locking, and our code doesn’t need them.
When that happens, it’s time to write our own data structures. Sometimes it’s much, much faster. And sometimes it makes our code much cleaner because we don’t include all of the extra code for reformatting the data exactly so.
8: Old-fashioned loops
Long ago, someone creating the C language wanted to encapsulate all of the abstract possibilities in one simple construct. There were some things to be done at the start, some things to do each time through the loop, and some way to tell when it was all done. At the time, it seemed like a perfectly clean syntax for capturing infinite possibilities.
That was then. Now some modern scolds see only trouble. There are too many things going on. All of those possibilities for goodness are also equally capable of badness. It makes reading and grokking that much harder. They love the more functional paradigm where there are no loops, just functions applied to lists, computational templates mapped to some data.
There are times when the loopless way is cleaner, especially when there’s just one neat function and an array. But there are times when the old-fashioned loop is much simpler because it can do much more. Searching for the first match, for instance, is simpler when you can stop as soon as it’s found.
Furthermore, mapping functions encourages sloppier coding when there are multiple things to be done to the data. Imagine you want to take the absolute value and then the square root of each number. The quickest solution is to map the first function and then the second, looping over the data twice.
9: Breaking out of loops in the middle
Somewhere along the line, a rule-making group declared that every loop should have an “invariant,” which is to say a logical statement that is true throughout the loop. When the invariant is no longer true, the loop ends. It’s a good way to think about complex loops, but it leads to crazy prohibitions like forbidding us from using a return
or a break
in the middle of the loop. This is a subset of the rule forbidding goto
statements.