Welcome! Please see the About page for a little more info on how this works.

0 votes
ago in Macros by
edited ago by

Hello and thank you for creating such an expressive and interesting language!

I've learned Lisp/Scheme-like languages before (in addition to experimenting with dozens of other both widely known and less known languages over the years, from multiple paradigms: concatenative, logical, low-level imperative, OOP, etc), but lately I've been learning Clojure and enjoying it!

Anyway though, the other day I was reflecting upon the fact that in programming languages (and in natural language) the order in which words appear in an expression has a very substantial effect on readability sometimes and on what parts of what is being communicated feel like they are being emphasized and brought to one's attention.

Consider, for example, the difference in visual/subjective emphasis between the following two equally valid Clojure expressions:

(assert (not (zero? 1)))
(-> 1 zero? not assert)

The former is the more traditional/direct way of writing it (and perhaps looks more natural to most people), whereas the later has stronger unidirectional flow of action and less visual distractions once you get used to it. However, unfortunately for the -> case, notice that though the -> expression is more "elegant" in a sense, it makes the visibility of the assertion worse by putting the assert keyword at the end.

It is harder to scan for the presence of assert at a glance in the -> case because we start reading each line at the left side, and the column position of whatever is on the left on each line is more consistent/predictable and hence easier to scan for at a glance.

Some people may also find "zero not" to sound too "Yoda-like" (akin to concatenative programming languages like Forth and Factor, like post-fix notation) whereas something like "not zero" may sound more natural to many people.

I also thought about how comp (compose functions) could be used here to keep the more natural or more expected "not zero" ordering, but then a function call cost may be imposed by doing so if the optimizer doesn't remove the function wrapper implied by use of comp.

So, that got me thinking that I should try writing a reversed version of the -> and ->> macros and I did and here are the resulting definitions:

(defmacro <- [& rest] 
  (let [rev-rest (reverse rest)] 
    `(-> ~@rev-rest)))

(defmacro <<- [& rest] 
  (let [rev-rest (reverse rest)] 
    `(->> ~@rev-rest)))

Both are actually very easy to write, as you can see.

This allows you to now do the following:

(<- assert not zero? 1)

As you can see, this enables you to reorder the "threading"/pipeline macro according to what feels most natural in any given context, which helps draw the attention of anyone reading your code to what comes first in each expression, much like how in English the first thing we start a sentence with is what is being emphasized or is assumed to be the subject (as in active vs passive voice, etc).

It is also very intuitive and easy to remember because of how the arrow operators visually guide the reader towards whichever direction the data is flowing (i.e. left to right for -> vs right to left for the proposed <- operator here).

This especially is well suited to assertions because what is most important about an assertion for someone trying to scan code quickly is that it is an assertion and hence emphasizing that clarifies it.

This is in contrast to data flow or transformation pipelines that use -> and such, where the opposite order often feels better.

One can also see, by applying macroexpand and/or macroexpand-1 that the cost of these macros in run-time will be zero, relative to traditional/direct nesting of the underlying expressions, given that the expressions (macroexpand '(<- assert not zero? 1)) and (macroexpand '(assert (not (zero? 1)))) return identical expansions.

Cognitive load tends to increase non-linearly with how many nesting levels something has, so using "threading"/pipeline macros flexibly and often tends to help readability and these new variants help fill those potential use cases out better.

Adding it also would naturally discourage unnecessary use of the computational overhead of comp when users prefer a right to left ordering of flow over a left to right one.

So, it seems clear to me that both orderings should be in every Clojure programmer's toolbox for legibility and I would argue that this should even be made part of the standard library since it is such a common case.

One can also of course just as easily add <-as too using the same strategy as above. Indeed, I already have added all these variants into my own code, though I more often use let sequences than <-as, since let is much more general in complex cases of course, for the obvious reasons of flexibility and clarity. So, <-as is maybe slightly fringe, but still good to add for symmetry purposes.

The reverse-direction <- and <<- threading macros though I am finding myself using quite often, and I likewise still use -> and ->> in data transformation code. Having both directions available feels analogous to having both active and passive voice (as in English) now available, whereas before with only one direction I was only writing in one voice whenever I used threading macros, which felt sometimes much more awkward and less clear (as the assertion example's legibility with -> vs <- clearly shows).

Interestingly, if you think about it, at least typically, -> corresponds with English passive voice because it begins with a noun (the initial data to be subsequently transformed), whereas <- corresponds with English active voice because it begins with a verb (the action ultimately being done to the whole). Well, at least, that analogy seems roughly true for "data as data" (not "data as code") cases anyway, in a sense.

Nice food for thought though, right? I really like these kinds of nuances. Truly great English writing takes very fluent advantage of such nuances and it makes sense that the same would apply to code if you gave a language enough mechanisms to actually do so fully.

As such, I am personally really happy with the resulting freedom of these macros!

With these new macros in hand one is much less likely to use the less legible deeply nested or oddly directed forms when you can now order things either way to fit each stylistic case.

Thanks for reading and for your time!

I hope to see this in the standard library one day and I think it'd make Clojure feel even more elegant, expressive, and conceptually symmetrical. I see only upside to that! Thinking flexibly and naturally after all is one of the most defining traits of Lisp/Scheme languages!

PS: These arrows operators look even nicer with a ligature font such as Fira Code, making them pop out even more clearly when reading through code. It results in a truly beautiful visual flow of code, seeing these arrows direct the eyes and the mind so intuitively.

Please log in or register to answer this question.

...