18 August 2021

Manual Vertical Code Alignment in Clojure

...if you're into that sort of thing.

I had an interesting idea recently. It's quite possible I'm the only person[1] that will think this is kinda cool/useful, but maybe if you go read this other post I just wrote you'll at least know what you're dealing with.

Go read this and come back:

https://michaelwhatcott.com/manual-vertical-code-alignment/

Background

Did you read it? Ok, here's a similar idea for Clojure code, but not using strings (but there's nothing wrong with that approach in Clojure...).

I recently performed the Prime Factors Kata so it's on my mind. Here's how I implemented the production code:

(defn prime-factors-of [n]
  (loop [n n, div 2, factors []]
    (cond (= n 1) factors
          (zero? (mod n div)) (recur (/ n div) div (conj factors div))
          :else (recur n (inc div) factors))))

The cond is not the easiest thing to read. We could probably extract variables or introduce let blocks, but maybe this is all it needs:

(defn prime-factors-of [n]
  (loop [n n, div 2, factors []]
    (cond (= n 1)             factors
          (zero? (mod n div)) (recur (/ n div) div (conj factors div))
          :else               (recur n (inc div) factors))))

See the difference? Now the conditions expressions are spaced from their corresponding expressions such that the corresponding expressions are vertically aligned with each other. I don't know about you, but I find the second version much easier to read. (Robert Martin used this technique on code very simliar to this in order to make a point. You'll have to scroll down the last code sample to see it.)

I know what you're thinking:

Aren't beginners adorable?

Ok, ok, but I've been doing the same thing in Go (you did read the background article, right?), and I've been using that language since, well, practically forevor ago.

Anyway, the above example is problematic because the it goes against the grain of established formatting tools (ie. whatever ships with Intellij + Cursive).

And now, for something different, and maybe somewhat silly, but it gets the job done:

(defn prime-factors-of [n]
  (loop [n n, div 2, factors []]
    (cond (= n 1),,,,,,,,,,,, factors
          (zero? (mod n div)) (recur (/ n div) div (conj factors div))
          :else,,,,,,,,,,,,,, (recur n (inc div) factors))))

Commas to the rescue!

In Clojure, , is treated as whitespace, exactly the same as spaces, tabs, or newlines. Commas are thus never required...but are often used to enhance readability.

The intended use-case for commas in clojure code was undoubtedly as a separator in collections, like the key/value pairs in maps:

{1 [1], 2 [2], 3 [3]}

But I think they can enhance readability elsewhere when used judiciously. The use-case I outline above is somewhat problematic as it introduces a potential maintainence burden. I've always been willing to shoulder that burden because I think it makes the code easier to read, and it's not that hard to adjust when needed.

What do you think?







[1] Hey, I'm not alone!