Let’s say you needed to calculate the triangular number of n. The triangular number of 4 is (1+2+3+4). Which of the following implementations would you consider the more readable?
(defn t1 [n]
(if (= n 0)
(+ n (t1 (- n 1)))))
(defn t2 [n]
(->> (inc n)
If you answered t1, I’m willing to bet that by “readable” you meant “requires less experience to read”. I’m going to argue that this is a bad way of evaluate readability. Let’s assume for the moment that your command of Clojure is perfect, what are the challenges to comprehension then?
- The first has a recursive step
- The first has a branch
- The first contains an expression that goes three deep.
Worse, each of these interact, meaning you have to hold them in your head all at once. If you’re trying to solve a problem significantly harder than computing triangular numbers, sticking to “basic” code results in significant more lines of code and significantly more of these things that you have to simultaneously track. Whilst each individual component is easy enough to parse, the overall effect is fatiguing. This is bad news for humans, because they’re bad at maintaining mental function whilst processing large numbers of minor items.
Favour High Level Code
Let’s now assume that everyone has excellent command of the language we’re using. What impedes readability in these circumstances?
- The longer you need to track the value of a variable, the harder it is to understand.*
- The more levels of control flow you need to track, the harder it is to understand.
- The less code you can see on your screen at once, the harder the code is to understand.
- The more times you see the same code expressions repeated with possible minor variations, the harder it is to understand.
Writing basic code in any language favours the comprehensibility of a single over the comprehensibility of the whole. Not only that, but since each construct contains the possibility of error, using a basic style is much more likely to result in bugs. A much better set of guidelines for writing readable code would be:
- Use values close to their definition. Make it clear that they are out of scope after that point.
- Favour standardised control flow constructs such as reduce in Clojure and LINQ in C# over writing everything in terms of branches, loops and recursion.
- Favour concise code over verbose code
- Aggressively eliminate common sub-expressions
And next time trying to evaluate code readability, take the effects of fatigue more seriously and don’t worry as much about trying to compensate for lack of experience.
*If it’s global and mutable, your chances of tracking it are nil unless you’re extremely disciplined. Action at a distance is very hard to read and extremely error prone.
One thought on “Code Fatigue”
I'd argue that both your examples are difficult to read. The first one does not use the "recur" special form, which makes the recursion more difficult to spot and will blow the stack because it is not TCO. I think it should be written as follows:(defn t3 ([n] (t3 n 0)) ([n result] (if (zero? n) result (recur (dec n) (+ result n)))))The use of the threading macro for calling only 2 functions is overkill, and the code would be much more readable without:(defn t4 [n] (reduce + (range (inc n))))Finally, as an alternative to the threading macro, I would suggest a point-free definition, which is a much better fit for this topic:(def t5 (comp (partial reduce +) range inc))I agree completely with your message, but I'm afraid you are not following your own advice! 😉