Some things in the programming world are so easy to misuse that most people prefer to never use them at all. These are the programming equivalent of a flamethrower: You might rarely be in the position to really need one, but every once in a while it turns out that you need to take down a forest. In that case, there’s no easier way than going Rambo on your codebase.
“There is no programming language—no matter how structured—that will prevent programmers from making bad programs.” —Larry Flon
That's where a few of the old, forgotten code constructs come into play. Creative use of features such as goto, multiple inheritance, eval, and recursion may be just the right solution for experienced developers when used in the right situation.
In software development, one of the most important skills you can have is knowing how to evaluate tradeoffs. Once you get better at choosing the right way over the popular way, you'll gain a lot in code clarity, readability, and reduced development time.
Sometimes the measure of a tool’s power lies more in the programmer's ability to use it properly than on the qualities of the tool itself. Most of these forbidden ideas have special cases where they are not only useful, but they are the best possible solution available. That's what they were made for. In those cases, you are missing out on a lot of power by not using them.
Here are four forgotten code constructs that should be resurrected.
Goto
Old codebases used to be infested with goto statements. In those times, you could use it to jump to particular code lines directly, by number. This led people to write the most inscrutable code imaginable, filled with numerical line references, akin to assembly language.
Because it's so easy to misuse, it eventually started causing more problems than it solved. Then Edsger Dijikstra's famous paper: "Goto Considered Harmful" came out and the rest is history. These days, people look at you funny if you try to argue that it can be useful.
Despite goto's bad reputation, even today there are some popular projects using goto, including the Linux kernel itself. For example, look at this intro to kernel development where the audience has a collective gasp when they hear about it. And then there’s a classic Linus Torvalds rant speaking in its favor.
The appeal behind goto was being able to write very tight loops, but the perils of this construct were not universally understood. One common mistake was to jump backward in code which, although easy to write, made the logic almost incomprehensible.
Few modern languages support goto. Of the most popular ones, only C, PHP and C# have it. But there are a few practical cases where it leads to code that is maintainable and easy to read.
Goto the right way
The main place where it shines is in error handling code. Here's an example that can be a good alternative to having multiple consecutive if/else statements.
int result;
The reason why goto fits error handling well is that it gives you immediate visibility on the problem by flattening the code. The data handling and the clean-up code are separated and easy to see.
result = first_operation();
if(!result) {
goto first_error;
}
result = second_operation();
if(!result) {
goto second_error;
}
second_error:
// ...
first_error:
// ...
Bear in mind that this return value style with gotos is more suited for low-level code. In C++ you have the option of using RAII, and other high-level languages where return values are not common usually have exception support anyway.
Goto is also good for encapsulating repetitive return statements. Some methods have multiple exit paths that look very similar. In those cases, we can encapsulate the return statements and we’ll be able to easily change the output of the function without touching the data handling.
function isLeapYear($year) {
You could also encapsulate using method calls. That's fine if your compiler inlines the resulting code, but as an added bonus in this case you also get to have the related code inside the same function instead of somewhere else in the file.
if ($year % 400 == 0) {
goto leap_year;
}
if ($year % 4 > 0) {
goto common_year;
}
if ($year % 100 > 0) {
goto leap_year;
}
goto common_year;
leap_year:
return true;
common_year:
return false;
}
Multiple inheritance
This language feature lets you inherit a class from multiple parent classes to get functionality from all of them. Normally in OOP (Object-oriented programming) code you'll only inherit from one parent, but in some contexts it seems more natural to inherit from many.
The danger here is complexity. Since you might affect multiple modules in your app from the same parent classes, it's not that easy to reason about code changes. Any mistake could cause a chain reaction of bugs.
Unfortunately, misguided usage has made this once popular feature almost completely disappear. These days it's almost synonymous with complexity in OOP, and most of the new languages have dropped it altogether. It can only be found in C++, Python, Lisp, and some other functional languages.
My gripe with multiple inheritance is that it’s an easy way to break the single-responsibility principle. Each class should be responsible for one task only, and once you inherit from multiple parents, things start to get muddy. In my experience, this principle tends to be thrown out quickly when deadlines and hard times come knocking, so I wouldn't want to make that even easier.
Multiple inheritance the right way
On the other hand, sometimes you want to inherit from parents that are completely separate from each other. This is where multiple inheritance can become productive. Classes that have orthogonal behaviors are the best case scenario for multiple inheritance.
class Flyer:
pass
class Swimmer:
pass
class FlyingFish(Swimmer, Flyer):
pass # can swim and fly
In this example, Swimmer and Flyer are completely separate abstractions, so inheriting from both is not going to lead to a mix-up of responsibilities in the child class.
Another good case for multiple inheritance is when you inherit from multiple interfaces instead of classes. This case is less dangerous, as inheriting does not bring additional functionality to the base class—it just extends it's contract with the rest of the world.
I like to build interfaces that reflect a particular behavior. That works very naturally with multiple inheritance. For example, you can have a DatabaseDriver that is Queryable and Persistent, and maybe you're implementing it as a MysqlDriver which has both behaviors working together.
As long as you are careful in having your abstractions clearly defined, multiple inheritance can be another powerful tool at your disposal.
Eval
Eval is a tool some languages have that lets you run arbitrary code from a string. Most interpreted languages have it, such as JavaScript, Python, PHP, and Ruby.
It is by far the most dangerous tool here, and that's because you might accidentally give it a user's input, which will open your system up completely. A malicious user would be able to run code to delete files or otherwise take control of your machine.
Despite this, in some cases it can be a very productive tool. Some problems become much easier to solve with eval, such as parsing a dynamic number of input variables and parsing template languages.
Although wanting to apply simple solutions like that is laudable, the dangers are too great. It is close to impossible to properly sanitize random input so that the eval call is safe from abuse, so it's better to only evaluate code you've generated yourself.
Eval the right way
Evaluating your own code can still be very useful. Some examples include: interpreting JSON data, HTML templates, mathematical expressions, and detecting environment and language features.
Here's an example for detecting if your browser supports ES6-style generators.
try {
eval("(function *(){})");
} catch(err) {
console.log(err);
console.log("No generators");
}
Recursion
More forgotten than forbidden, recursion is still highly praised in academic settings, but ask your teacher about practical applications and you'll often get blank stares.
Functional programmers love recursion. Some functional programming languages such as Haskell even downright force you to use it, but the truth is that recursion is not natural outside of that world.
There are ways to make it work in the iterative world, though. One such way is through tail recursion. Simply put, you leave the recursive call at the end of your method, and the compiler will then be able to optimize your method to avoid stack overflows.
Unfortunately, some languages have decided to not support tail-call optimization. This is often true of web-based languages. From what I've gathered, both Python and PHP don't support it, and JavaScript only does on ES6+ engines. In these cases, if you really need it, you can use something like trampolines to get around your environment's limitations, but I'd recommend just sticking to iterative programming.
Recursion the right way
Recursion offers great simplification and code reduction in some algorithms. Particularly if you're dealing with tree-based data structures and sorted lists, you'll find that it's much easier to implement solutions using recursion. In those cases, if you need simple code that works, go for it.
Performance-wise, it's generally best to convert recursive functions to their iterative versions. The code will not be as easy to understand, but it'll be more appropriate in production environments, as it will have more predictable performance.
Rediscover your language's forgotten constructs
Knowing your language well is important. Even if you don't plan on using these features, be sure to know the use cases where they are a good choice. Not only to have more options, but also to have a better grasp on the language itself.
Be pragmatic. If you haven't read it yet, check out The Pragmatic Programmer. Knowing your tools by heart and all of their trade-offs is one of the marks of the experienced and battle-hardened developer.
Keep learning
Take a deep dive into the state of quality with TechBeacon's Guide. Plus: Download the free World Quality Report 2022-23.
Put performance engineering into practice with these top 10 performance engineering techniques that work.
Find to tools you need with TechBeacon's Buyer's Guide for Selecting Software Test Automation Tools.
Discover best practices for reducing software defects with TechBeacon's Guide.
- Take your testing career to the next level. TechBeacon's Careers Topic Center provides expert advice to prepare you for your next move.