Mind Your Form
Abstractions allow us to focus on the immediate computation at hand while hiding its details. Organizing code into modules (or packages or classes) is a form of abstraction. So are functions.
This post is about abstracting syntax. Syntactical abstraction can vary between using functions to abstract away common operations and full fledged DSLs that allow us to express complex tasks with ease. This post is about Clojure’s language constructs that simplify forms. It falls somewhere between those two extremes.
Horizontal to Vertical
Consider this code:
(:baz (:bar (:foo :something) :another-thing))
Think of :foo, :bar & :baz as functions. :baz is applied last but you read it first. :another-thing is a parameter to :bar but it’s hard to see at first glance. Using threading macro both issues are solved:
(-> :something (:foo) (:bar :another-thing) (:baz))
Threading macro converts nested code into sequential code. It also change the shape of the code from horizontal to vertical. Shorter lines are easier to read and we are accustomed to interpret a top-to-bottom list as sequential. It is no more or less sequential than the nested version. They are essentially same, -> is a macro that turns the latter into former.
Long functions are annoying because they have too much control in one place. Consider this code:
(if :pred-1 :result-1 (if :pred-2 :result-2 (if :pred-3 :result-3 :result-4)))
It is hard to see predicates (there are three of them) and branches (there are four) but the first thing you notice is the shape. The shape tells you nothing about what this code does.
You can’t break the code example above into smaller functions. You can, but it doesn’t improve readability much. But, just like the first example you can change the shape:
(cond :pred-1 :result-1 :pred-2 :result-2 :pred-3 :result-3 :else :result-4)
cond executes its predicates in turn (:pred-1, :pred-2 ...) until one evaluates to something truthy, then it executes the corresponding result and returns it. Again this is exactly what the if version does, cond is a macro that turns the latter into former.
Compression of Repeated Forms
Creating a lexical variable and then using it as an if predicate can be compressed using if-let macro:
(let [foo :foo] (if foo :foo-is-bound :noo-foo)) (if-let [foo :foo] :foo-is-bound :noo-foo)
This doesn’t provide great compression, but it is still useful if there are many instances. Destructuring on the other hand is a lot better in compression:
(let [?list (list 3 4) x (clojure.core/nth ?list 0 nil) y (clojure.core/nth ?list 1 nil)] (+ x y)) (let [[x y] (list 3 4)] (+ x y))
Destructuring is like a DSL for assignments. Using similar techniques can improve code considerably. Ability to add syntax to the language, is a very powerful feature. Hiding the language constructs that are not necessary to communicate what’s being done means leaving only the constructs that communicate what’s being done.
This post focuses on form-level abstractions, small enhancements that make life a bit easier. Each example has a longer more verbose version that does exactly the same thing as the more abstract version. These small enhancements would compound and make life a lot easier for projects of considerable size. So I suggest learning these functions/macros and make a habit of using them.
|||Except for the first one where we used threading macro. A more complex, real life example would necessitate breaking up that call chain anyway.|
If you have any questions, suggestions or corrections feel free to drop me a line.