Software design principles for teams: The Principle of No WTFs.
Code is read far more than it is written: “clever code” ages like seafood in the sun.
Background
This is an elaboration of one of the principles I originally published in the following article…
Principle
Code is read far more than it is written: “clever code” ages like seafood in the sun.
Confession: I’m stealing the name from one of my fave XKCD comics:
The fly in the ointment of this principle is that what is “clever” (i.e., WTF inducing) to me… might be absolutely pedestrian to you. An example is called for: the following Clojure1 code finds the largest sets of anagrams in a file of words (source: https://rosettacode.org/wiki/Anagrams).
(->> (slurp "words.txt")
clojure.string/split-lines
(group-by sort)
vals
(sort-by count >)
(partition-by count)
first)
This is idiomatic Clojure: it chains together 7 top-level function calls where the result of each previous call is passed as an argument to the next, all via the magic of the Clojure threading macro (->>
). Other languages have similar features (Elixir’s pipe operator works in a similar fashion), but these powerful idiomatic constructs often hide the intent from people who are not used to parsing code written in this way. Highly threaded code like this also makes subsequent maintenance and inspection harder because you need to deconstruct the single expression to debug or update it.
There is a way to write this code in Clojure without the threading, but it is probably even less appealing to non-Clojurians. The second version leans heavily on the “inside out” nature of Lisp-based languages with deeply nested function calls. It is exactly this style of code that the threading macro is used to prevent.
(first
(partition-by count
(sort-by count >
(vals
(group-by sort
(clojure.string/split-lines
(slurp "words.txt")))))))
The third version of this solution is functionally identical to the previous ones, but written in a manner more accessible to non-Clojure developers:
(def word-file (slurp "words.txt")
(def words (clojure.string/split-lines word-file))
(def words-by-letters (group-by sort words))
(def anagrams (vals words-by-letters))
(def sorted-anagrams (sort-by count > anagrams))
(def anagrams-by-count (partition-by count sorted-anagrams))
(def result (first anagrams-by-count))
This final example does a far better job in revealing it’s intent, mainly through the use of meaningful “variable” names. These variables are explicitly named in subsequent lines, making the flow of data easier to see.
For teams with a broad range of experience with the language and/or needing to stay across a number of languages, I would favor this final example (or a variant of it) as an example of “how we write code here”. In effect, I’m preferring a dumber subset of a language when it is more inclusive to the needs of the developers. I would be wary of crippling the language by excluding too many of it’s unique features, but there is usually a happy middle ground to be found between this and Code Golf.
Note: I am not advocating a lowest common denominator approach where only the most ubiquitous constructs are supported, but rather a conscious effort to meet the developers where they are in terms of their ability to write intentional code. Allowing developers to write code at their own personal level is optimising for individual productivity rather than that of the team. Doing so will certainly make it harder to achieve The Principle of Single Authorship.
Clojure is a good language for these examples because the syntax is very simple, but it can appear impenetrable to people not comfortable with Lisp-y languages and their prefix notation.