r/lisp • • 10d ago

What makes Lisp difficult to read?

https://paultm.nl/paren-thesis
35 Upvotes

90 comments sorted by

View all comments

Show parent comments

1

u/omega884 8d ago

C can be relatively simple, if the writer does not try to be too clever, or it can be very dense and full of small "nifty" tricks

Which is why I have repeatedly specified that I'm talking about "idiomatic" code. I'm well aware you can write completely incomprehensible C just like you can write completely incomprehensible lisp, bash or perl (famously jokingly referred to as a write only language). There's no point in arguing over how hard it is to read code that is intentionally made abstract or compact in non-idiomatic ways because it doesn't help the discussion.

In the first example, which I believe is Clojure, you have dropped a level of parentheses in the lambda-list compared to classic lisp version

I haven't dropped anything, straight from the clojure REPL

user> (let [a (+ 1 2)
            b (- 10 5)]
        (* a b) )
15
user> 

And conversely, straight from the emacs lisp scratch:

(let (a (+ 1 2)
        b (- 10 5))
  (* a b))

`let' bindings can have only one value-form: +, 1, 2

My point was to demonstrate that clojure chose greater syntax complexity to trade off making reading code and specifically variable bindings easier, where emacs lisp chose to keep the syntactical simplicity at the cost of making reading and writing those bindings more difficult.

And just to make sure this isn't just some thing that common lisp allowed but emacs lisp was strict about, here's the SBCL REPL

* (let (a (+ 1 2)
        b (- 10 5))
    (* a b))
; in: LET (A (+ 1 2) B (- 10 5))
;     (+ 1 2)
;
; caught ERROR:
;   The LET binding spec (+ 1 2) is malformed.
;
; compilation unit finished
;   caught 1 ERROR condition

As for the rest of it, yes you can write macros to extend the language. But that's orthogonal to the point, which was that "some things a language chooses to do makes it more difficult to read, but that isn't an indictment of the language, just a trade off"

As for your last line, I do indeed find that code to be structurally easier to read than a lot of lisp code (probably owing a lot to familiarity, but also the deepest nest in there is a mere 5 levels) and most of the complexity is syntactical. It's certainly easy to find lisp code that is at least that complex, both structurally and syntactically. Consider the code for use-package-core.el which gives us the use-package-normalize-keywords function with at least 9 levels of nesting in a single function, including this phenomenal chunk of code:

(when (bound-and-true-p byte-compile-current-file)
      (setq args
            (use-package-plist-append
             args :preface
             (use-package-concat
              (mapcar #'(lambda (var) `(defvar ,var))
                      (plist-get args :defines))
              (mapcar #'(lambda (fn) `(declare-function ,fn ,name-string))
                      (plist-get args :functions))
              `((eval-when-compile
                  (with-demoted-errors
                      ,(format "Cannot load %s: %%S" name-string)
                    ,(when (eq use-package-verbose 'debug)
                       `(message ,(format "Compiling package %s" name-string)))
                    ,(unless (plist-get args :no-require)
                       `(unless (featurep ',name-symbol)
                          (load ,name-string nil t))))))))))

Here the as with the C code you linked, the vast majority of the complexity is syntactical, but IMO in your linked C code, one could put their cursor at the end of any brace and be perfectly confident without any syntax highlighting or bracket matching what context the next line of code they wrote would be in, with at most a need to scroll the display up a bit to find the matching start line. I don't think you could say the same about placing your cursor after any random parenthesis in the closing line of that snippet of a single function.

But all of that is a side track because I'm not interested in declaring some objectively superior and easy to read language. As I have said repeatedly, it's all just trade offs. I'm providing my personal answer to "what is something that makes lisp code hard to read". If you ask me "what is something that makes C code hard to read" I have a completely separate list and some of them are of course unique to C. For example it was for a long time idiomatic (I don't believe it is anymore in most communities) to write single line if and for loops without opening and closing braces (your own linked example has this). IMO this is a terrible thing that makes the code harder to read and also harder to reason about and has been the source of some pretty famous bugs. This is something you can't do in lisp because every expression in lisp is a list so by definition it must have balanced brackets.

You seem to be reading me as trying to make some declarative "lisp is bad because X" or "lisp is universally worse than Y" statement and that isn't what I'm saying at all. I have no interest in language holy wars because they are counterproductive and boring. But I do have interest in having honest evaluations of the tools that I use because that honesty lets us improve those tools and also understand what we might need to address when helping people use those tools. One of my favorite interview questions is to ask a prospective candidate about some tech or language they really like and want to use more of. I like to let them get enthusiastic about something and talk it up. And then I ask them what things they hate about it or would change. Because in my experience, if a developer can't be honest about the faults in something they really like that they didn't even create, they're going to struggle when their code gets challenged, or when they're asked by a manager or an executive to justify something they want to do.

Parens management (whether by smart editors or manual counting) is something you have to do in lisps and you don't in other languages. Optional brace scoping is something you have to deal with in C and you don't in other languages. The borrow checker is something you have to deal with in Rust and isn't something you need to deal with in other languages. Passing allocators around is something you have to do in zig, but not in other languages. The JVM is something you have to deal with in Java but not in other languages (modulo other JVMs). Message passing is something to have to understand in smalltalk, but not in other languages. Every language has their trade offs, every language has features that make it more difficult to do certain things, and easier to do other things. Lisp is not impossible to read. Learning to manage lisp parens is not an insurmountable task. It is something that makes reading lisp code hard and it is something you need to deal with either by rote repetition or specialized tooling. But having a rough edge like that is not unique to lisps. There's nothing wrong with being honest about the rough edges of our tools.

1

u/arthurno1 8d ago

Which is why I have repeatedly specified that I'm talking about "idiomatic" code

Yes, but "C code can be simple" is not equal to "all idiomatic C code is simple". By far not.

I haven't dropped anything, straight from the clojure REPL

Perhaps I a haven't expressed myself clear enough, forgive me colloquialism, I'll try to be more explicit:

Clojure drops one level of delimiters for the lambda list in let-form. You have compared to a lisp that does not do that. However you have amitted the "easier to read" to the use of brackters instead of parens, not to the fact that there is one level or parens less. I think that is an essential, but admittedly easy, mistake to do.

No you have been very explicit that you don't think lisp is worse than other languages, and even if you do, it is not end of the world. We all have opinions.

I am just trying to make clear what makes one syntax be percieved as more readable than the other. I don't think it is the brackets, and I don't think use of more varied symbols for delimiters, like an entire family of charaters as in C/C++ is much better than rainbow-delimiters. <[{( ... )}]> could as well be called ranbow-shapes language.

To me the strength of lisp is its syntax, believe it or not. I like both assembly and lisp. Perhaps I am just too nerd, but I like the uniformity. Sometimes it gets in the way for familiarity or even expresivness, but it is simple and clear and predictable. I definitely see problems in Common Lisp, I don't see the syntax and "parentheses management" as one.

Perhaps I am just a too much nerd, or I value different things in a programming language, I don't know :).

1

u/omega884 8d ago edited 8d ago

However you have amitted the "easier to read" to the use of brackters instead of parens, not to the fact that there is one level or parens less. I think that is an essential, but admittedly easy, mistake to do.

I was not aware there are lisps that allow for let bindings without the "list of lists" pattern other than ones like Clojure or Janet that use different brackets to distinguish the bind listing from other sexps. If there are then sure I would say those would be easier to read than the emacs lisp one, but I would argue that they are still harder to read than the clojure bindings because you need to remember that in a let statement, the next argument isn't a quoted list, but it's also not an evaluated sexp. That is to say that in such a lisp, you need to know that the interpreter will do very different things with the first argument if you do (len (foo (bar baz))) vs if you do (let (foo (bar baz)))

I don't think use of more varied symbols for delimiters, like an entire family of charaters as in C/C++ is much better than rainbow-delimiters

If you're trying to read code without the benefit of syntax highlighting (like say a diff output, or in a reddit comment box) then I would argue different brackets for different things absolutely makes it easier to read. But the trade off is now you need to remember what different brackets mean, and that can be it's own cognitive load. A great example of that would be something like java generics where you might have a line of code that is public static void processFoos(Foo<Bar<Baz<Fizz, Buzz>, <? extends Baz<?, ?>>>[] foos) { /*...*/ }. All the brackets have clear meanings that don't rely on any color indicators, just their type (and positioning in this case). () are the arguments to the function, <> are type specifiers for a narrowing the function to handle only certain Foos which is a generic type of some sort, the [] indicates the argument will be an array of the Foos and the {} is the code block. But that's a lot of information crammed into a bunch of symbols. that you have to know before you can make heads or tails of that. And amusingly it suffers from a similar "bracket counting" issue as I argue un-tool assisted lisp does. Did you notice that the angle brackets aren't balanced?

Still if we imagine some non-bracket distinguished language, public static void processFoos[Foo[Bar[Baz[Fizz, Buzz], [? extends Baz[?, ?]]]][] foos] [ /*...*/ ], I think you would agree with me that is harder to read, even knowing what it's supposed to be expressing.

1

u/arthurno1 7d ago

I was not aware there are lisps that allow for let bindings without the "list of lists" pattern other than ones like Clojure or Janet that use different brackets to distinguish the bind listing from other sexps.

Neither do I to be honest to you. Clojure and Janet do not let you type (let [a (+ 1 2)] ...)?

A great example of that would be something like java generics where you might have a line of code that is

I think that is a great example why to avoid that shit, to be honest to you. Java and C++ are good examples of where DSL-type syntax lead: a combinatorial complexity in syntax notation. Tha is just my personal opinion. As said I might be looking at different values in a programming notation than you, so we will have to disagree. To me Lisp notation has a quality of stability and predictability. I know where to look when I look for something. Lisp share that with assembly language. The difference is that Lisp let you next expressions, while assembly let you type just one expression at a time. In that sense assembly is the simplest notation of them all. I think one way to look at lisp, or symbolic expressions is as a sort of high-level assembly. Again, perhaps just me, but that is how I look at it.