Skip to content(if available)orjump to list(if available)

All Lisp indentation schemes are ugly

All Lisp indentation schemes are ugly

22 comments

·January 19, 2025

lihaoyi

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!

n8henrie

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.

rayiner

That is blasphemy.

DonHopkins

Nicely indented easy to read blasphemy is the most dangerous kind!

null

[deleted]

maiar

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.

fn-mote

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

phoe-krk

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

stevebmark

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

bcrosby95

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.

cb321

Well, there is also https://nim-lang.org as well as a pretty big list at https://en.wikipedia.org/wiki/Off-side_rule . Just expanding, not trying to invalidate your valid question!

null

[deleted]

agumonkey

never had any real issue using emacs indentation engine (not even sure lisp modes use smie..)

behnamoh

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

UltraSane

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

phoe-krk

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

behnamoh

Yes, I think for whatever reason, Lisp languages tend to use long names for funcs and vars whereas Haskell and other FP languages prefer symbols like >>=, <$>, etc. Personally, I prefer somewhere in the middle.

> The TREE-TRANSFORM-IF is such a case and you can't solve it by just "going step by step".

Yes, this is a good point.

martin-t

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.

behnamoh

So the "fluent design" approach? I like that too, and many OO languages allow for that, but I haven't seen that approach in functional programming.

3836293648

Really?

Iterator methods are the only part of Rust/C# that feel like normal decent function composition. Except Haskell is backwards.

swayvil

Surely one could do it with different background colors instead. Any examples of that?

el_memorioso

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.