In my previous post, The Elements of Programming Style: still worth reading, I argued that the book is still worth reading. Going back through the rules made me think about which ones I would give to a developer today.
Most need no help. Clear names, small modules, testing boundary values and measuring before optimising are as useful now as they were when Brian Kernighan and P. J. Plauger wrote the book. A few rules, though, speak in the syntax of Fortran and PL/I. Fortran is still used in scientific and high-performance computing, but many developers won’t recognise an arithmetic IF or a DATA statement. In those cases, I’ve kept the original rule and written a version that carries its advice into other languages.
These are the 72 rules I took from my reading notes, in order. Where I’ve changed one, I’ve included the original, the updated rule and my reason.
1. Write clearly - Don’t be too clever
2. Say what you mean, simply and directly.
3. Use library functions.
4. Use temporary variables only when they make an expression clearer; avoid unnecessary state.
Original: Avoid temporary variables.
Reason: A well-named intermediate value can explain a calculation. The problem is a temporary state that makes the calculation harder to follow.
5. Write clearly - don’t sacrifice clarity for “efficiency”.
6. Let the machine do the dirty work.
7. Replace repetitive expressions by calls to a common function.
8. Parenthesise to avoid ambiguity.
9. Choose variable names that won’t be confused.
10. Updated: Express arithmetic tests as explicit conditions and clear branches.
Original: Avoid the Fortran arithmetic IF.
Reason: The arithmetic IF is a Fortran construct. The lasting advice is to make the possible paths through the code obvious.
11. Avoid unnecessary branches.
12. Use the good features of a language; avoid the bad ones.
13. Use the language’s block structure and indentation to show which statements belong together.
Original: Use DO-END and indenting to delimit groups of statements.
Reason: The delimiters vary by language; the reader’s need to see the structure does not.
14. Make two-way choices explicit when only one action should occur.
Original: Use IF-ELSE to emphasise that only one of two actions is to be performed.
Reason: The point is the relationship between the actions, rather than a particular spelling of IF-ELSE.
15. Use a clear multiway decision structure for mutually exclusive alternatives.
Original: Use IF…ELSE IF…ELSE IF…ELSE… to implement multiway branches.
Reason: Depending on the language and the problem, that might be a conditional chain, a switch, pattern matching or something else.
16. Use the fundamental control flow constructs
17. Write first in an easy to understand pseudo language and then translate into whatever language you have.
18. Avoid unnecessary nesting and empty alternatives.
Original: Avoid THEN-IF and null ELSE.
Reason: The original names particular arrangements of statements. The issue is the extra work they give a reader trying to understand the decision.
19. Use early returns when they clarify a function, and remove else branches that no longer serve a purpose.
Original: Avoid ELSE GOTO and ELSE RETURN
Reason: A return is not automatically a problem. An exit hidden inside a tangle of branches is.
20. Follow each decision as closely as possible with its associate action.
21. Use appropriate data structures to replace repetitive control logic.
Original: Use data arrays to avoid repetitive control sequences.
Reason: An array may be right, but a map, table or other structure may express the relationship more clearly.
22. Choose a data representation that makes the program simple.
23. Don’t stop with your first draft.
24. Organise code into focused functions and modules.
Original: Modularise. Use subroutines.
Reason: “Subroutine” sounds dated to many readers. The useful idea is to give each part of the program a manageable responsibility.
25. Make coupling between modules visible.
26. Each module should do one thing well.
27. Make sure every module hides something.
28. Let the data structure the program.
29. Don’t patch bad code - rewrite it.
30. Write and test a big program in small pieces.
31. Use recursive procedures for recursively-defined data structures.
32. Test input for validity and plausibility.
33. Make sure inputs cannot violate the limits of the program.
34. Terminate input by end of file or marker, not by count.
35. Identify bad input; recover if possible.
36. Treat end of file conditions in a uniform manner.
37. Make input easy to prepare and output self-explanatory.
38. Use uniform input formats
39. Make input easy to proofread.
40. Use free-form input when possible.
41. Use self-identifying input. Allow defaults. Echo both on output.
42. Keep input and output at clear boundaries in dedicated functions or modules.
Original: Localise input and output in subroutines.
Reason: This keeps the original separation of concerns without prescribing subroutines as the mechanism.
43. Make sure all variables are initialised before use.
44. Use the diagnostics and debugging tools available for your language.
Original: Use debugging compilers.
Reason: Compilers are only one source of help. Warnings, linters, type checkers, debuggers and runtime checks can all catch mistakes.
45. Give constants explicit values where they are declared, and initialise changing state before it is used.
Original: Initialise constants with DATA statements or INITIAL attributes; initialise variables with executable code.
Reason: DATA statements and INITIAL attributes belong to particular languages. The underlying concern is knowing where a value comes from.
46. Watch out for off-by-one errors.
47. Take care to branch the right way on equality.
48. Make every exit from a loop clear and deliberate.
Original: Avoid multiple exits from loops.
Reason: An early exit can make a loop simpler. Several exits become a problem when a reader cannot readily find them or understand when each one runs.
49. Make sure your code “does nothing” gracefully.
50. Test programs at their boundary values.
51. Program defensively.
52. 10.0 times 0.1 is hardly ever 1.0.
I’ve left this rule as written because it neatly captures the danger the original authors were warning about. The following rule is where I’d apply a little more nuance today.
53. Use exact floating point equality only when exactness is part of the problem; otherwise use a tolerance appropriate to that problem.
Original: Don’t compare floating point numbers for equality.
Reason: The warning remains valuable, but a blanket ban hides the question a developer needs to answer: what does “equal” mean here?
54. Make it right before you make it faster.
55. Keep it right when you make it faster.
56. Make it clear before you make it faster.
57. Don’t sacrifice clarity for small gains in “efficiency”.
58. Let the language implementation handle routine optimisations.
Original: Let the compiler do the simple optimisations.
Reason: A program may be compiled ahead of time, compiled while it runs or interpreted. The advice remains to measure before trying to outsmart the tools.
59. Don’t strain to reuse code; reorganise instead.
60. Make sure special cases are truly special.
61. Keep it simple to make it faster.
62. Don’t tinker to make it faster - find a better algorithm.
63. Instrument your programs. Measure before making efficiency gains.
64. Make sure comments and code agree.
65. Don’t just echo the code with comments - make every comment count.
66. Don’t comment bad code - rewrite it.
67. Use variable names that mean something.
68. Give meaningful names to functions, blocks and other code elements.
Original: Use statement labels that mean something.
Reason: Statement labels are uncommon in much modern code, but names still help a reader understand what they are looking at.
69. Format a program to help the reader understand it.
70. Indent to show the logical structure of a program.
71. Document your data layouts.
72. Don’t over comment.
Only a small number of these rules needed new wording. A few others needed a little less certainty: temporary variables, early exits and floating point comparisons all call for judgement rather than an absolute prohibition. What surprised me most was how little needed to change. The languages, tools and syntax have moved on considerably since 1974, but most of the advice hasn’t. Good programming style, it seems, ages rather better than programming languages.
Could we enforce these rules?
Of course, having a list of rules and actually following them are two different things. So I wondered how these rules could be implemented in a modern development environment.
The first problem is that most of them cannot simply be handed to a linter. “Don’t be too clever”, “let the data structure the program” and “don’t patch bad code, rewrite it” all require judgement. There is no sensible automated rule for deciding whether a developer is being too clever.
One approach is to split the rules into two groups: those that can be checked automatically and those that need to be considered during code review.
Some rules lend themselves well to automation. Formatting and indentation can be handled by a formatter. Compilers and linters can detect some uses of uninitialised variables, enforce aspects of naming and identify particularly complex code. Static analysis can also highlight suspicious floating point comparisons. These are useful because they can be checked consistently without relying on somebody remembering all 72 rules.
I’m less convinced by turning some of the other advice into hard limits. A maximum function length of 40 lines, a maximum cyclomatic complexity of 10 or a maximum nesting depth of three might be useful warnings, but they aren't what the original rules say. “Each module should do one thing well” does not mean that every function must fit within 40 lines. Adding arbitrary numbers risks replacing one set of judgement calls with another.
The more interesting approach is to turn the rules that require judgement into questions for code review. Rather than expecting a reviewer to remember all 72 rules, some of them could become questions such as:
- Does every early return make the code clearer, rather than hiding one exit among several confusing ones?
- Could this chain of conditions be expressed more clearly using a map, table or another data structure?
- Is this change patching around bad code when rewriting it would leave the code clearer?
- Has this code been shared because it really represents the same operation, or because two pieces of code happen to look similar?
- Does every comment explain something useful, rather than simply repeating what the code already says?
- Are the names meaningful enough to explain what the variables, functions and other code elements represent?
- Have the boundary values and invalid inputs been considered?
- Has performance actually been measured before trying to improve it?
I wouldn't necessarily put all of those questions on every pull request. That could quickly become another checklist people click through without thinking. They are more useful as prompts, selected where they are relevant to the change being reviewed.
There is an obvious role for an AI assistant here too. The automated rules can be checked by the usual development tools, while the questions that require judgement could form part of the instructions given to an AI assistant when it writes or reviews code. It cannot turn a judgement call into an objective rule, but it can at least ask the question.
Perhaps that is another reason these rules have lasted so well. Some can be automated, but many still require judgement. And that judgement might be somewhere an AI assistant can help.
Add-on: instructions for an AI assistant
As an experiment, I asked an AI assistant to turn the rules into a set of instructions that could be added to a project. This isn't intended to replace the original 72 rules, and I don't agree that everything below should necessarily be enforced as written. However, it's an interesting example of how advice written in 1974 might be put to practical use more than 50 years later.
The resulting instructions divide the advice into hard constraints, warnings and questions requiring judgement. Where numbers are used, they are deliberately prompts to look more closely, not limits that must be satisfied.
Style rules
Adapted from Kernighan & Plauger, The Elements of Programming Style. Three tiers: rules that are genuinely bright-line, numbers that are only useful as a prompt to look closer, and questions that need real judgement every time.
Hard constraints — do not violate
These are bright-line rules: the original advice itself draws the line, rather than an arbitrary number bolted on afterwards.
- Floating point: use exact floating point equality only when exactness is part of the problem. Otherwise use an explicit tolerance appropriate to the problem, and say what that tolerance means.
- Initialisation: never use a variable before it is initialised. Give constants explicit values where they're declared; initialise changing state immediately before first use, not “eventually”.
- Formatting: match the project's existing formatter and indentation exactly. Never hand-format against the project's own tool.
- Naming: no single-letter names except loop counters or short lambda/comprehension variables. Avoid data, temp, val, foo and other names that don't say what the thing is.
- Boundaries: every new function that takes input must explicitly consider empty input, boundary values and invalid input.
- Loop exits: every exit from a loop must be easy for a reader to find and understand. If a loop has more than one exit, each needs a reason a reader can see at a glance.
- Comments: a comment that simply restates the code gets deleted, not written. If the code needs explaining, first ask whether it could be rewritten more clearly.
Warnings — flag, don't block
Length, complexity and nesting are symptoms, not the disease. A number here is a prompt to look more closely, not a rule to satisfy by mechanically splitting code until the counter is happy.
- A function pushing past roughly 40 lines, or cyclomatic complexity past roughly 10, is worth another look. Is it actually doing one thing well, or has it grown a second job? A long function that's still one clear thing is not necessarily a problem; a short function doing two unrelated things is.
- Nesting past three levels deep is worth another look. Would an early return, guard clause or restructuring the data make the decision easier to follow? If the nesting genuinely reflects the structure of the problem, leave it.
- Don't “fix” either of these by mechanically extracting functions simply to make a number smaller.
Before finishing any change, check
Treat these as questions rather than proverbs:
- Clarity over cleverness: is there a more obvious way to write this?
- Boundaries: if this function takes input, have empty input, boundary values and invalid input been considered where they are relevant?
- Loop exits: is every exit from a loop easy for a reader to find and understand? If there is more than one exit, is the reason for each clear?
- Control flow → data: could this chain of conditions or repeated branches be replaced by a map, table, lookup or polymorphic dispatch?
- Patch vs rewrite: is this a genuine fix, or a patch on code I don't actually understand?
- Coupling: does this change make the connection between modules more visible, or introduce a hidden dependency?
- Single responsibility: does this module or function still do one thing?
- Reuse pressure: am I abstracting this because it's genuinely the same operation, or forcing two similar but different things together?
- Temporary variables: does every intermediate variable make the calculation easier to follow, or just add more state to trace?
- Special cases: is every special case actually special, or is it covering for the wrong data structure or design?
- Multiway branches: is the branching structure actually the clearest way to express the alternatives?
- Diagnostics: have I used the language's available diagnostic tools rather than simply assuming the logic is right?
- Efficiency: have I optimised before measuring? If there's no evidence this is a bottleneck, would the clearer version be better?
- Modularity: does this module hide an implementation detail, data format or algorithm choice that the rest of the program doesn't need to know?
- Second draft: is this actually the best version, or just the first thing that worked?
Principles behind the rules
For context rather than something to repeat back on every code review:
- Say what you mean, simply and directly, and use library functions rather than reinventing them.
- Let the data structure the program. The right representation often simplifies the surrounding logic more effectively than clever control flow.
- Test at boundary values, not just the typical case.
- Make it right, keep it right, make it clear and only then make it fast.
- Good structure ages; specific syntax doesn't. When in doubt, prefer the version of a rule that survives a change of language.

I've long had an issue with item 51 (program defensively) because people have interpreted in opposite ways. Some treat it as "fail fast and loud", while others do the complete opposite and allow code to mask errors and try and limp along only to fail later in other weird ways.
ReplyDeleteThis post of mine from way back in 2012 was triggered by yet another occasion where defensive code masked a bug and looming performance problem:
https://chrisoldwood.blogspot.com/2012/11/the-cost-of-defensive-programming.html