Clojure, being a Lisp dialect, inherited Lisp's homoiconicity. Homoiconicity makes metaprogramming easier, since code can be treated as data: reflection in the language (examining the program's entities at runtime) depends on a single, homogeneous structure, and it does not have to handle several different structures that would appear in a complex syntax [1].
The downside of a more homogeneous language structure is that language constructs, such as loops, nested ifs, function calls or switches, etc, are more similar to each other.
In clojure:
;; if:
(if (chunked-seq? s)
(chunk-cons (chunk-first s) (concat (chunk-rest s) y))
(cons (first s) (concat (rest s) y)))
;; function call:
(repaint (chunked-seq? s)
(chunk-cons (chunk-first s) (concat (chunk-rest s) y))
(cons (first s) (concat (rest s) y)))
The difference between the two constructs is just a word. In a non homoiconic language:
// if:
if (chunked-seq?(s))
chunk-cons(chunk-first(s), concat(chunk-rest(s), y));
else
cons(first(s), concat(rest(s), y));
// function call:
repaint(chunked-seq?(s),
chunk-cons(chunk-first(s), concat(chunk-rest(s), y)),
cons(first(s), concat(rest(s), y));
Is there a way to make these program constructs easier to identify (more conspicuous) in Clojure? Maybe some recommended code format or best practice?
Besides using an IDE that supports syntax highlighting for the different cases, no, there isn't really a way to differentiate between them in the code itself.
You could try and use formatting to differentiate between function calls and macros:
But then that prevents you from using a one-line list comprehension using
for
; sometimes they can neatly fit on one line. This also prevents you from breaking up large function calls into several lines. Unless the reducing function is pre-defined, most of my calls toreduce
take the form:There are just too many scenarios to try to limit how calls are formatted. What about more complicated macros like
cond
?I think a key thing to understand is that the operation of a form depends entirely on the first symbol in the form. Instead of relying on special syntaxes to differentiate between them, train your eyes to snap to the first symbol in the form, and do a quick "lookup" in your head.
And really, there are only a few cases that need to be considered:
Special forms like
if
andlet
(actuallylet*
). These are fundamental constructs of the language, so you will be exposed to them constantly.if
. There are so few special forms that plain memorization is the best route.Macros with "unusual" behavior like threading macros and
cond
. There are still some instances where I'll be looking over someone's code, and because they're using a macro that I don't have a lot of familiarity with, it'll take me a second to figure out the flow of the code.Functions. If it's not either of the above, it must be a function and follow typical function calling syntax.