Cond indentation/readabliity, continuation syntax proposal for expr

Hi! Wanted to get some feedback on an annoyance and possible fix for cond and similar forms…

I frequently encounter/write code that runs into line length/spacing issues. I think this impacts most of us, whether you prefer 80 chars line max or 120 or whatever. Example:

  ;; Typical nice case; alignment helps
  (cond
    (short1)       (short2)
    (still-short1) (still-short2)
    :else          (short3))

  ;; Problematic longer case; lines can get VERY long
  (cond
    (some-really-long-call :to "whatever")       (do-something-also-long)
    (some-other-thing :with "even" :more "args") (some-other-action)
    :else                                        (the-last-possible-thing))

  ;; Break into newlines, but hard to tell TEST from EXPR lines! (what I usually do)
  (cond
    (some-really-long-call :to "whatever")
    (do-something-also-long)
    (some-other-thing :with "even" :more "args")
    (some-other-action)
    :else
    (the-last-possible-thing))


  ;; Readable and typical, but just blew up from nice 3 lines to 9; big problem IMO
  (cond

    (some-really-long-call :to "whatever")
    (do-something-also-long)
    ;; Maybe a comment here to separate, or just another wasteful blank line
    (some-other-thing :with "even" :more "args")
    (some-other-action)

    :else
    (the-last-possible-thing))

  ;; How about this??
  (cond
    (some-really-long-call :to "whatever")
    #_> (do-something-also-long)
    (some-other-thing :with "even" :more "args")
    #_> (some-other-action)
    :else
    #_> (the-last-possible-thing))

Thoughts on the #_> comment/indicator? Any big problems you could see it causing? Any alternative you like better? It works well with Emacs’ default clojure formatting, and also cljfmt (should try with zprint and other editors). It also looks pretty good with a ligature/unicode substitution (though I don’t think these arrows are the ideal substitution), like:

Screen Shot 2021-07-06 at 11.12.39 AM

We could come up with some other macro like #> but maybe not worth it since #_ works for cases I can think of, and is still short.

This should also probably apply to case, condp, cond->, and maybe others. It’s even almost nicely congruent with condp's :>>.

Here’s the Emacs setup (thanks to Malabarba!) that makes the pretty arrow (I assume can be done similarly for other editors):

(global-prettify-symbols-mode)
(setq prettify-symbols-alist '(("#_>" . (?\s (Br . Bl) ?\s (Br . Bl) ?\s
					     (Bc . Br) #x21a6 (Bc . Bl) #x21a6))))

(Yes, we could change all our editors and tools to indent the EXPR line by a couple chars, but I can’t imagine getting teams to agree how to do that. This proposal alone may also be difficult to get people to agree on, but at least it seems like a harmless, optional convention that doesn’t break anything.)

If I have long conditions and need to have my cond formatted as alternating lines of condition/expression, I just add blank lines between each pair to add readability. I haven’t found anything else that is both simple enough and readable enough to be worth the effort of introducing a custom convention.

7 Likes

I agree that having to introduce a convention is not ideal. Though I’m considering trying it.

My first approach to this was trying to get my editor to do some sort of overlay (and not need any special comment addition) to make the TEST line stand out, but I’d need to do some research to get it to work (if even possible), like:

Screen Shot 2021-07-06 at 12.00.26 PM

Actually, I think a § (section sign) or (pilcrow) is a better/good symbol than the triangle.

2 Likes

I think in terms of code, it shouldn’t cause issues that I can think of.

Now as too if I like it better than proper indent or spaces not sure.

You could also try with a comma:

(cond
    (some-really-long-call :to "whatever")
    , (do-something-also-long)
    (some-other-thing :with "even" :more "args")
    , (some-other-action)
    :else
    , (the-last-possible-thing))

;; Or

(cond
    (some-really-long-call :to "whatever")
    (do-something-also-long)
    ,
    (some-other-thing :with "even" :more "args")
    (some-other-action)
    ,
    :else
    (the-last-possible-thing))

;; Or

(cond
    (some-really-long-call :to "whatever")
    (do-something-also-long),
    (some-other-thing :with "even" :more "args")
    (some-other-action),
    :else
    (the-last-possible-thing))
2 Likes

Thanks @didibus! The comma is pretty good and simple. I definitely prefer (and I think like) your first example. It’s a little trickier to create a “pretty symbol” for, but maybe it’s simple enough that it does the job and doesn’t need a glyph. Could even make it a double-comma.

Oh but alas, cljfmt is pretty particular about commas and does not like it (even though Emacs treats it as expected).

1 Like

What does it do with it?

1 Like

It treats commas as whitespace, so ignores them, and then complains that the EXPR lines should not be indented. Looks like using :remove-surrounding-whitespace? option does not affect this.

 (cond
   (some-really-long-call :to "whatever")
-  , (do-something-also-long)
+  (do-something-also-long)
   (some-other-thing :with "even" :more "args")
-  , (some-other-action)
+  (some-other-action)
   :else
-  , (the-last-possible-thing
-     :foo))
+  (the-last-possible-thing
+   :foo))

Ah, interesting thanks.

Cursive moves commas to the end of previous line when reformats code.

Feels like adding complexity to anyone reading the codebase. It could easily lead to questions like

Is this a new form of reader macro added onto the language…

Where is this macro defined, is it on the project or in a library that was imported.

Introducing additional ‘convention’ that can easily be mistaken for syntax seem to add complexity

A blank line does not suffer this complexity

4 Likes

Sorry for resurrecting an old thread, but I was excited to see @didibus mention using commas here. I do exactly this. These days I’m an engineering team of one at CollBox, so I don’t yet know how this will go over with other engineers, but I’ve been test-driving this for a few months now and I’m pretty happy with it.

I actually use commas whenever I want to violate the normal Clojure indentation standards. Typically that comes in three cases:

  1. The cond example described above:
(cond
  (extremely-long-condition with some lengthy args)
  , :foo
  (another-extremely-long-condition here)
  , :bar)

But that’s probably my rarest usage of it. More commonly I use this with…

  1. Datomic queries with multiple :where clauses that I want to align, but I like having them indented after the :where keyword like the rest of the key-value “pairs”:
(datomic.api/q '[:find ?user
                 :in $ ?email
                 :where [?user :user/email ?email]
                 ,      [?user :user/role :user.role/admin]]
               db email)

(And yes, I am aware of the map syntax for q.)

  1. Compactly-aligned let bindings where I have one or two lengthy names I’m binding and I don’t want to shift all of the values annoyingly far right:
(let [name       "Bilbo Baggins"
      email      "[email protected]"
      confirmed? false
      something-annoyingly-long
      ,          42]
  ; ...
  )

I’m sure somebody will say this is the worst thing ever, but I firmly believe that indentation impacts readability, and while the comma trick isn’t exactly beautiful, it lets me express the patterns in the code that I want a reader to notice, without making me fight my editor’s indentation features, and it calls out the non-standard indentation as being intentional.

1 Like

I think it’s cool to align things up in ways that makes the code easy to read.

However, different formatters will treat this differently. I know Calva, which uses clj-fmt will just delete those commas, as if they where whitespace.

1 Like