Last active
March 3, 2023 22:12
-
-
Save redbar0n/df8b3e59c74f6a005de3e7936a4bf303 to your computer and use it in GitHub Desktop.
Clojure readability
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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