Code Quality: Power
There are many ways to solve a problem. Choosing the optimal approach is important to write good code and maintain its quality over time. One concept we can use to help us reason though figuring out this solution is power: the flexibility and dynamism of the constructs we use to model solutions. At a minimum, a program needs enough "power" to meet its immediate requirements; but how much more than that?
For code quality, an awareness of power:
- Helps to frame discussions around simplicity and flexibility
- Encourages thinking about the growth and limitations of a system over time
- Discourages over-engineering and unnecessary premature optimization
Programming languages expose features with different power levels. Before we talk about proper use of power, we should talk about the common options.
Data is the least powerful thing. It is inert. These are most languages' primitive types such as integers and strings, but also include more complex data structures. These things lack dynamism and ought to be immutable.
Conditionals are where things start to get dynamic. If-else, switch, case-when and other decision points form the bedrock of actually executing code.
Flow control operators are worth calling out because they introduce the idea that code may never finish running. While loops can get caught in infinite loops if misused, or may be intentional for long running tasks. In this vein, functions also fit as recursion can be used to this effect as well. (And without tail-call optimization, recursive functions can cause stack overflows much more readily). Non-recursive, acyclic functions ought not be considered powerful; these are simply a means for organizing code since they don't introduce these issues.
Interfaces and dynamic dispatch (often "methods") introduces indirection between the caller and receiver. It is at this point where we can start to lose a sense of what will happen when we call something as methods meeting a common interface may do very different things. In another sense this is where we start to think of functions as values.
Goto statements have fallen out of favor but do deserve a shout out for just how crazy powerful they are. "Pause whatever we are doing and start executing code at line X" blows through so much one might intend to have happened before the code is run. We also have to understand the "context" of the call because unlike a function we can't pass or return values so this guarantees some shared global state.
Notice goto operates at the code-as-text level since it maintains the relation to a line of code or label whereas everything above can be reasoned about as non-textual concepts.
Beyond these programming level features, we should also talk about some facets of power that can differ based on language.
async/awaitannotations, what is blocking execution and what is async is fairly arbitrary. Asynchronous work is incredibly powerful because it can take a long time and other things can execute in the meantime. Other things happening means things can change. Careful attention must be paid to when and where assumptions are made because they may be far removed from when a resulting action is taken.
Meta-programming is implemented wildly differently in languages, if it is at all. To even begin to discuss this, let's say meta-programming in the general case is more powerful than dynamic dispatch. This ought not be true for an executing program because meta-programming can elide dynamic dispatch. For example, Rust supports generic types which when compiled generate monomorphic function code. You pay the cost of that indirection as the difference between the written code and what executes.
Rust also supports hygienic macros which generate more arbitrary code. These macros are more robust and maintainable than text-based macros more common in C/C++. When we talk about power though, these are roughly equivalent in the amount of indirection they introduce.
For text based languages there's always the avenue of writing programs to write files which will then be compiled/interpreted. In which case you're not getting the benefits of the language's built-in meta-programming facilities, which may not have enough power.
Remote procedure calls (RPC) represent power beyond a single computer. All bets are off making networked connections to services. They don't use the same programming language, change dramatically over time, may become unreachable, and with the pace of technology are unconstricted by any current notion of power. Entering this realm of power invites understanding topics in distributed systems.
With all of the above power levels and features laid out, there's really not much else to say other than to make the case for simplicity in code. Lots of notable quotes in this area:
Everything should be made as simple as possible, but no simpler.
Always implement things when you actually need them, never when you just foresee that you need them.
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.
And so it is with power: picking the least powerful construct to solve a problem aids the most simple solution. This is bundled up nicely as the Rule of least power. And there are some complimentary acronyms to say the same thing in a rush: Keep it simple stupid (KISS) and "You ain't gonna need it" (YAGNI).
There are legitimate use cases for each of these levels of power and appropriate times to use them. Deciding what is unnecessary power often boils down to "can we make this work with this less powerful thing?" and iterating on that until there's no reasonable weaker choice.
We also need to be cognizant and articulate exactly what is necessary to make this decision. Getting these explicit requirements figured out is the harder problem. Lacking those requirements, airing on the side of least power initially is easier to recover from than something more powerful.