back
98 comments
This post misses the IMO best indentation scheme for lisp, which I used for my college class where we had to use MIT scheme:

    (define (match:element variable restrictions)
        (define (ok? datum)
            (every 
                (lambda (restriction)
                    (restriction datum)
                )
                restrictions
            )
        )
        (define (element-match data dictionary succeed)
            (and 
                (ok? data)
                (let ((vcell (match:lookup variable dictionary)))
                    (if vcell
                        (and 
                            (equal? (match:value vcell) data)
                            (succeed dictionary)
                        )
                        (succeed (match:bind variable data dictionary))
                    )
                )
            )
        )
        element-match
    )
It may not be densest or most compact indentation scheme, but damn is it readable for someone without a lisp/scheme background!
> This post misses the IMO best indentation scheme for lisp [...]

Likewise, a lispier(?) and more compact compromise?:

    (define (match:element variable restrictions)
        (define (ok? datum)
            (every 
                (lambda (restriction)
                    (restriction datum))
                restrictions))
        (define (element-match data dictionary succeed)
            (and 
                (ok? data)
                (let ((vcell (match:lookup variable dictionary)))
                    (if vcell
                        (and 
                            (equal? (match:value vcell) data)
                            (succeed dictionary))
                        (succeed (match:bind variable data dictionary))))))
        element-match)
It becomes much harder to notice if you have closed all the right matching parens, and hence where a token after the closing paren belongs. When the closing paren is exactly below the opening paren, on the same column, it becomes much easier to see the structure. It's a very good thing for students who just learn Lisp.
For Lisp users this is 35% extra vertical space for zero readability benefit. Or negative readability rather. When I see this formatting style I have to fix it before I can read it. It's like trying to read C code with an extra blank line after every single closing brace.
Lisp users are a diverse crowd. I won't be so bold as to sign off for all of them.
The main reason to use it is not for reading it is to work with version control which is all line orientated.

The reading is not that bad as the indentation tells you where the brackets are and you don't really notice them.

As a hobbyist that keeps trying to break into lisp, I think the absence of an obviously good formatter for lisps has made things much harder.

This looks like how I've naively tried to format things, and having started with python kind of makes sense.

But trying to read / edit code with 5 parentheses bunched together, I have the hardest time telling if they are matched or where. Thank goodness for vim / helix auto-matching.

Admit you posted this only to trigger lispers.
That is blasphemy.
More of an allman Lisp guy myself

  (
  define (match:element variable restrictions)
    (
    define (ok? datum)
      (
      every 
        (
        lambda (restriction)
          (restriction datum)
        )
        restrictions
      )
    )
  (
    define (element-match data dictionary succeed)
      (
      and 
        (ok? data)
        (
        let ((vcell (match:lookup variable dictionary)))
          (
          if vcell
            (
            and 
             (equal? (match:value vcell) data)
             (succeed dictionary)
            )
            (succeed (match:bind variable data dictionary))
          )
        )
      )
    )
    element-match
  )
I’ve been in too many arguments about Lisp indentation but that actually comes close to decent with a couple of changes: move the singleton closing parens up, and reduce to, say, 2 spaces.
I am sure there are some Emacs modes or plugins for other editors that could go back and forth between this and the more commonly-seen style.
I don't use Lisp but this is very readable to me, actually way more than the examples in the article by a lot. I actually understand this code, just because of how it's formatted. Maybe I'd just use 2 spaces instead of 4 but otherwise, looks good.

Granted I'm not the one to optimize for since I don't use Lisp, but this just seems sane and easy to automate, and it looks like other non-Lisp languages.

Those poor lonely parens!
The only thing that's missing is moving most of nested logic into `let` assignments.
Yeah, I'm surprised the article doesn't address this obvious case. It's pretty much my default indentation scheme for any language, and while I see a lot of lispers here complain about the wasted vertical space, it's undeniably easy to see which parentheses match and what the block structure is.

I'll happily tolerate some wasted space in that trade-off.

Honestly that's the indentation format I use for every language. Doing it in Lisp just makes sense.

My "OTBS all the things" rules:

- Never horizontal align. Indentation is a simple tree, with each set of children indented 1 layer deeper, which can be 4 spaces or 1 tab.

    ThisKindOfThing(
                    foo,
                    bar,
                    baz);
is generally dumb.

- If a block is worth indenting for, the end of the block deserves its own line. Just do OTBS but also for parentheses if you've a long function call.

  ThisKindOfThing (
      foo,
      bar,
      baz
  );
Is just more consistent and easier to manage while still being legible.

- if you're ever splitting something across multiple lines there must be an indent.

  myObj
      .Foo()
      .Bar()
      .Baz();
- you're allowed to double-dip on tree depth - that is indent only one level for like 3 levels of tree, as long as the opening of that 3-deep jump is a clear one-liner and the end of it is a line that says ")))".

  Foo(Bar(Baz(
      someVal
  )));
- when splitting lines on infix operators, anything but commas go at the start of the line (also, bedmas counts as tree depth)

  foo
      + bar
      + baz
      + (
          var1
          * var2
          * var3
      )
      + quux;
Once you have even a small amount of that background it's complete shit. Or else if it isn't, nobody wants to work with you. But at that point that probably suits you fine. If you want to work completely alone, yet be maximally productive, you've landed in a good language family.
Haha, good one!

(I hope?!)

This is one of the most disgusting things I read on HN.
I love a Lisp-themed article, but this is such a yawn in 2025.

It seems pretty clear that a threading macro which allows the user to place the "hole" being filled wherever they want is the answer.

Example: cl-arrows

https://github.com/nightfly19/cl-arrows?tab=readme-ov-file

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

> Nineteen! Nineteen spaces of indentation! It's getting unruly.

Why, is there a shortage of whitespace these days??

> Such an indent, when used in deeply nested code, makes it too wide and unreadable.

Ah, I see! Well, I would argue the problem is not the indentation style. What makes your code unreadable is that it's too deply nested, and I would bet that no indentation style can help with that.

That's a point too!
Are there any formatters that detect related things and put them in the same column? Here's a Pie example:

  (claim  Tuple3 (->                U       U     U    U))
  (define Tuple3 (lambda          ( a       b     c   )
                   (Pair            a (Pair b     c)  )))
  (claim  tuple3Of (Pi (           (A U)   (B U) (C U))
                    (->             A       B     C
                      (Tuple3       A       B     C   ))))
  (define tuple3Of (lambda  (       A       B     C   )
                     (lambda      ( a       b     c   )
                       (the (Tuple3 A       B     C   )
                         (cons      a (cons b     c)) ))))

  (claim  Tuple5 (->                U       U       U       U     U    U))
  (define Tuple5 (lambda          ( a       b       c       d     e   )
                   (Pair            a (Pair b (Pair c (Pair d     e))))))
  (claim  tuple5Of (Pi            ((A U)   (B U)   (C U)   (D U) (E U))
                     (->            A       B       C       D     E
                       (Tuple5      A       B       C       D     E   ))))
  (define tuple5Of (lambda        ( A       B       C       D     E   )
                     (lambda      ( a       b       c       d     e   )
                       (the (Tuple5 A       B       C       D     E   )
                         (cons      a (cons b (cons c (cons d     e))))))))
> 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.

Diamond arrows exist, you know.

    (-<> foo
      bar
      (baz)
      (quux 1 2 3)
      (fred 4 5 6 <>)
      (frob 7 <> 8 <> 9))
All in all, this post reads like a rant, and I realized that upon reading "now what I'm about to suggest is likely not to your taste". That style of indentation is something I use often when writing calls to long-named functions like COMPUTE-APPLICABLE-METHODS and I haven't ever thought of it being not to my taste, or even of it being ugly as the author suggests.
Well, it's all based on my experience. In two Lisp companies I worked in, colleagues complained or even were trying to fix how I indent things. So it's not just some anxiety whatever, it's experience that needs explanation and justification.

I'm aware of arrow-macros/diamond wands, and am using them myself. But I often have to write dependency-less libraries for moderately complicated algos, and that's where indentation style is the only thing I can rely on.

    > (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.

I think this is more a problem with languages that encourage (or don't discourage) deeply nested function calls. I face the same issue in Python, although Python kinda discourages me to do:

    foo(
        bar(
            baz(
                far(
                    faz(...)))))

Instead, Python and Pythonic idioms encourage a "step by step" approach:

    a = faz(...)
    b = far(a)
    ...

I don't know which is "better"—the first approach requires thinking ahead about the chain of functions we're about to call while the second one is more like a gradual development of the solution, albeit being more verbose than the former.
The second Python method that uses intermediary variable is what I use because it works much better with a debugger

I also like this trick to avoid nested for loops:

  import itertools 
  names = ['Bob', 'Sam']
  nums = [1,2]
  letters = ['A','B']
  for name,num,letter in itertools.product(names,nums,letters):
      print(f"{name} {num} {letter}")

  Bob 1 A
  Bob 1 B
  Bob 2 A
  Bob 2 B
  Sam 1 A
  Sam 1 B
  Sam 2 A
  Sam 2 B
You can have the same step-by-step in Lisp via LET*. The problem outlined by the author happens when you have a function call where 1) the operator has a long name, 2) there are many arguments. The TREE-TRANSFORM-IF is such a case and you can't solve it by just "going step by step".
That deep nest of function calls is why I love languages that have bash-style pipelining as a built in feature, either with explicit shell piping features, or where everything Function(myObj) is also implicitly myObj.Method()

Then it becomes (eg PowerShell, a language I hate but use constantly)

  Faz | 
      Far |
      Baz |
      Bar |
      Foo
Or (more C++/Java/C#-ish)

  Faz()
      .Far()
      .Baz()
      .Bar()
      .Foo()
In this way the data and execution flows forwards instead of backwards and you don't have a pyramid of indentation.

I don't know why we're so stuck on prefix notation for function calls. Postfix is just easier to read in long chains.

I prefer a third approach - chaining method calls via `.method()`. C#'s LINQ extension methods and Rust's iterator methods do it and I find it to be the best of both worlds most of the time.
It's clear that the blogger is a wide-eyed newbie, who is enamored of Lisp and trying to attract any kind of attention and clicks to it. Obviously, it worked; he got on the front page of HN.

I predict that in another five years, he will write an article disparaging Lisp and purge it from his CV, and that will be that.

I literally program in Lisp for five years now, and I ain't going anywhere.
I just skipped to the bottom of this crap to find the real gem: it's produced with the help of ed.

Yeah, someone working with ed its not going to like 19 repeated leading spaces across lines for vertical alignment. They will like that less than even somebody working with Notepad.

...the author is using ed scripts as a static site generator, in a way completely unrelated to the post content. People are allowed to do oddball things.
> Once you get used to Lisp, you stop noticing the parentheses and rely on indentation instead.

Who does that? LISP has a datastructured syntax, textformat has no meaning in it.

Humans read code. I'd like to become a machine someday, but alas, I still am human and I need to read mine/others' code. Lisp code too.
never had any real issue using emacs indentation engine (not even sure lisp modes use smie..)
idk i always just put a newline before every single open parens and that worked for me
Surely one could do it with different background colors instead. Any examples of that?
For matching parentheses, Emacs users can use a package called rainbow-delimiters that colors matching delimiters. Different levels of enclosure use different colors. I've found this to be too much visual noise -- I am usually just interested on the delimiters for the current s-expressions. In this vein, having multiple different colored backgrounds would be garish and distracting for me.
I have seen it done with foreground colours: https://github.com/alphapapa/prism.el

I think I once saw a project turning Scheme into a visual block-based representation, but I can't find it again.

Lisp is an objectively goofy language,* I appreciate someone pointing out the pain points around trying to cobble together readability via indentation.

* Not a bad language - well, maybe bad by today's standards, but undeniably a very important language - just goofy

What languages do you use are readable without indentation? I guess other than Python, where indentation actually contributes towards correctness rather than it just being optional.