How to make clojure program constructs easier to identify?

139 Views Asked by At

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?

1

There are 1 best solutions below

0
On

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:

(for [a b]
  [a a])

(some-func [a b] [a a])

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 to reduce take the form:

(reduce (fn [a b] ...)
        starting-acc
        coll)

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 and let (actually let*). These are fundamental constructs of the language, so you will be exposed to them constantly.

    • I don't think these should pose a problem. Your brain should immediately know what's going on when you see 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.

    • This is remedied though by just getting some practice with the macro. Learning a new macro extends your capabilities when writing Clojure anyway, so this should always be considered. As with special forms, there really aren't that many mind-bending macros, so memorizing the main ones (the basic threading macros, and conditional macros) is simple.
  • Functions. If it's not either of the above, it must be a function and follow typical function calling syntax.