Skip to content

Instantly share code, notes, and snippets.

@redbar0n
Last active March 3, 2023 22:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save redbar0n/df8b3e59c74f6a005de3e7936a4bf303 to your computer and use it in GitHub Desktop.
Save redbar0n/df8b3e59c74f6a005de3e7936a4bf303 to your computer and use it in GitHub Desktop.
Clojure readability
;; The following example is:
"The program that prints the first 25 integers squared."
;; PS: The example was inspired by this article by C. Martin aka. "Uncle Bob": https://blog.cleancoder.com/uncle-bob/2019/08/22/WhyClojure.html
;; Some necessary background documentation first:
;;
;; range - "Returns a lazy seq of nums from start (inclusive) to end
;; (exclusive), by step, where start defaults to 0, step to 1, and end to
;; infinity." https://clojuredocs.org/clojure.core/range
;;
;; take - "Returns a lazy sequence of the first n items in coll, or all items if
;; there are fewer than n." https://clojuredocs.org/clojure.core/take
;; This would likely be how many Clojure programmers, like "Uncle Bob", would write it.
;; Lets call it the Compositional All-In-One Approach:
(println (take 25 (map #(* % %) (range))))
;; Simplified, since the % notation can be confusing:
(defn square [x] (* x x))
(println (take 25 (map square (range))))
;; How about a little more intermediary naming, so you don't have to take it all in at once.
;; Lets call this the Bottom-Up Approach:
(defn square [x] (* x x))
(defn squared_range (map square (range))
(defn 25_first_numbers_squared (take 25 squared_range))
(println 25_first_numbers_squared)
;; Problem: You have no idea of the significance of the first function `square` when you are reading from top to bottom,
;; so you'd have to _keep it in mind_, while reading to the bottom, to understand its ultimate purpose.
;; Likely, you'd then have to read from the bottom to the top again, to get a grasp of what happens (execution).
;; What if we reversed it? So that it could be read from top-to-bottom as _named_ top-down abstractions.
;; So the reader doesn't have to take everything in at once (a benefit that imperative programming brings).
;; We don't need to store state intermediately (like imperative programming encourages),
;; since we may get the same benefit by having intermediate function _names_. Effectively piecing up the logic into bite-sized chunks.
;; The benefit of abstractions is that you don't have to take everything in at once: you don't get exposed to the innermost lists.
;; I.e. you're not flooded with details at first, when you're just trying to get an overview.
;; For the following approach, you first need these declarations. But in an IDE you can toggle hide these declarations into 1 line. Maybe you could even automate creating them?
(declare
25_first_numbers_squared
squared_range
square) ;; to hoist the function declarations, so that we may use them before defining them
;; Lets call this the Top-Down Approach:
(println 25_first_numbers_squared)
(defn 25_first_numbers_squared (take 25 squared_range))
(defn squared_range (map square (range))
(defn square [x] (* x x))
;; The difference between this and the first is, when asked:
"What does it do?"
;; You can answer:
"It prints the 25 first numbers squared."
;; Like this:
(println 25_first_numbers_squared)
;; Instead of answering:
"It prints a take of 25 numbers from a mapped squaring over a range."
;; Like this:
(println (take 25 (map (* % %) (range))))
;; While both are equally correct, sometimes leaving out some detail doesn't hurt.
;; Compare: Have you ever heard someone retelling a story/experience whilst getting so fixated
;; on the details, that the listeners are lost to the overall point?
;; Incidentally: 25_first_numbers_squared would be very similar to an imaginary Ruby chaining:
;; `25.first.squared`, even though Ruby isn't always as nice in reality...: `(0..25).to_a.map! {|num| num ** 2}`
;; "But can't you just use a threading macro?" some Clojure programmers might reply.
;; So with the threading macro `->>` it would have been the following approach.
;; Here `,,,` refers to the result from the previous line.
;; The Chronological Approach (similarities to imperative programming, but without temporary storage of state):
(defn square [x] (* x x))
(println
(->>
(range)
(map square ,,,)
(take 25 ,,,)))
;; But when asked: "What does it do?"
;; That would translate to answering (reading top to bottom):
"It prints (lazily) a range of all numbers from 0 towards infinity, squared, but it only takes the 25 first."
;; Which would also be a decent reply. But this Chronological Approach is arguably not as intuitive to the newcomer,
;; as reading the Top-Down Approach, where the first line basically answers (at a very high-level):
"It prints the 25 first numbers, squared."
;; Which is really close to what we set out to make, namely:
"The program that prints the first 25 integers squared."
;; If that's only what the reader needed to know, or cared about, he could be spared from reading more.
;; But also, like a good story, the reader is enticed to read further on (by the new names),
;; to discover exactly _how_ that high-level description is implemented in practise.
;; With this in mind, consider our end result with the Top-Down Approach,
;; which reads like a story, from the overarching and vague, down to the more concrete, as you read on:
(println 25_first_numbers_squared)
(defn 25_first_numbers_squared (take 25 squared_range))
(defn squared_range (map square (range))
(defn square [x] (* x x))
;; Notice that each line expanded on the question raised in the previous line,
;; and gives just enough (i.e. a little bit more) detail than the previous line.
;; But that you could stop reading at any line, and still leave with a solid grasp of what it does.
;; (Imagine what this could to understandability of a large codebase with even more complex functions and layers..)
;; You don't have to take all in at once. It is decidedly foregoing the powerful ability
;; to express a huge amount of complexity at once. While such complexity is easily consumable by the writer
;; (due to The Curse of Knowledge; https://en.wikipedia.org/wiki/Curse_of_knowledge),
;; it is not easily consumable for the reader. Code is Communication, after all.
;;
;; Notice also that when you read, your eyes don't have to go from right to left (innermost to outermost function),
;; or jump back and forth between lines and/or unfamiliar names. But you can read strictly from left-to-right and top-to-bottom.
;; While at the same time gaining a little bit more understanding and detail than you previously had.
;; Just like how you would read a story: the next sentence building on the previous one.
;; So what have we traded?
;; Initial version (Compositional All-In-One Approach): 1 terse line of code: (println (take 25 (map (* % %) (range))))
;; Declared version (Top-Down Approach): 8 lines of code (4 declarations, 4 actual code), to get intermediary names and an abstracted top-down view.
;; Is it a good trade? Maybe not (considering lines spent). But then again, maybe?
;;
;; If we do spend 10 times more time reading than writing, readability is very important:
;;
;; “Indeed, the ratio of time spent reading versus writing is well over 10 to 1.
;; We are constantly reading old code as part of the effort to write new code.
;; [Therefore,] making it easy to read makes it easier to write.”
;; ― Robert C. Martin (aka. 'Uncle Bob'), in his book Clean Code
;;
;; So maybe it is worth it to spend a little bit more time writing, for everyone later to spend a little less time reading?
;;
;; I'm interested in hearing thoughts and experiences around if this Top-Down Approach could work for a full codebase.
;; How would it look? How would it feel?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment