All Lisp Indentation Schemes Are Ugly

By Artyom Bologov Pinky beige thumbnail with 'UGLY LISP INDENT' in huge letters.     Every word is indented with a return char '⮑'.     In the right top corner, 'Artyom aartaka.me Bologov' is written in likewise indented way.

Once you get used to Lisp, you stop noticing the parentheses and rely on indentation instead. That's partially why there are several alternative syntaxes based solely on indentation. Like Wisp, or sweet expressions. But then, the question stands: how to indent the code, actually? Especially so—in Lispy syntax.

No Indent: One Can Only Go So Far

A solution that will likely satisfy a proponent of any indentation style: "Just put it all on one line lol." Of course, lines are not infinitely readable and there's a column cap, (whether natural or enforced.) So this line (adapted from cl-blc,) while devoid of indentation problems, is unreadable:

(list (tree-transform-if predicate transformer (first tree) depth) (tree-transform-if predicate transformer (second tree) depth))
Absurdly long line of a Lisp code

That's the problem statement: some forms need multiple lines and indentation. But what kind of indentation?

(Dis)Functional Aligned Indent

There's an established style of indentation: align the function arguments on the same column:

(list (tree-transform-if predicate transformer (first tree) depth)
      (tree-transform-if predicate transformer (second tree) depth))
(inc! d (/ (* (mtx:get x i k)
              (mtx:get x j k))
           (1+ (* (vec:get dl l)
                  (vec:get eval k)))))
(let ((s (if (< j i) j i))
      (l (if (< j i) i j)))
  (+ (* s 1/2 (- (* 2 d-size) (1+ s)))
     l
     (- s)))
Examples of function-like indentation

This style if useful in reflecting the code structure: just look at what's indented and what's outdented. It works especially well for short function/macro/form names, like + or vec:get. Not so well for long ones:

(tree-transform-if predicate
                   transformer
                   (second tree)
                   depth)
A problematic function indentation

Nineteen! Nineteen spaces of indentation! It's getting unruly. Such an indent, when used in deeply nested code, makes it too wide and unreadable. If you add the strict one-per-line alignment of arguments, it's also painfully long line-wise. Let's handle the verticality first:

Space-filling Indent

No sane Lisper would write a loop with every keyword on its own line:

(loop for
      i
      below
      10
      collect
      i)
Ugly loop

(Some pretty-printers do that too (I'm looking at you, ECL!), but that's a topic for another day.)

We don't have to put every argument on its own line. That's the intuition behind the space-filling indent:

(loop for i below 10
      collect i)
Less ugly loop

One can go as far as splitting the argument list in arbitrary places. Regardless of semantics. (Looking at you, SBCL!) Putting some keyword arguments on the first line, and then some on the second/third/etc. This utilizes the space efficiently enough to be used. But what if one's stuck really deep in nesting levels?

Sick Macro Indent

Now what I'm about to suggest is likely not to your taste:

(tree-transform-if
 predicate transformer (second tree) depth)
My indentation style suited for deeply nested code

This style of indentation (putting function name on one line, and arguments on the other) was frowned upon more than once in my practice:

But what this style achieves is perfect indentation control. You only get one space of indentation per form. Complex algorithms are easier to read when written in this style.

And! This style also plays well with most indentation tools, even the simplest ones. I had this situation more than once:

Indenting all the arguments of the progn/begin-like macro in this sick style helps:

;; From
(mtx:with-column (uab-col uab index-ab)
                 (mtx:set!
                  ppab 0 index-ab
                  (blas:dot hi-hi-eval uab-col)))
;; To
(mtx:with-column
 (uab-col uab index-ab)
 (mtx:set!
  ppab 0 index-ab
  (blas:dot hi-hi-eval uab-col)))
Sick indent helps you to manage macros

Another Solution: Threading Macros

I worked on a deeply nested corporate codebase functions. In Clojure. And I realized why threading macros exist. To make the logic more sequential and readable, true. But also to tame excessive nesting!

Unfortunately, threading/arrow macros don't always work. Common Lisp in particular is extremely unfriendly to threading macros. Arrows imply a consistent thread-first or thread-last functions. But CL's standard lib is too inconsistent for that to work. So we're left with picking an indentation style we don't necessarily like.

What Is Your Favorite Style?

I often prefer the macro-like indentation, because I sometimes write deeply nested code. But I see the value in all the other indentation schemes!

Leave feedback! (via email)