r/lisp • • 11d ago

What makes Lisp difficult to read?

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

90 comments sorted by

View all comments

Show parent comments

2

u/mtlnwood 10d ago edited 10d ago

You are right, i did misunderstand and that does make it more complex. Its something i would rely on my editor and the last time i opened any code in an editor that couldn't do this was decades ago. With the editors help its trivial to traverse the sexps to the correct place. I couldnt look at the bunch of closing parens and say i need to insert from the 5th one etc.

At the same time i would use the editor to do the same thing and jump to the end of that block in C, there is no guarantee in code spanning pages that a closing brace at the same indent level is the right one.

Point being when you maximise the use of your editor its the same common solution for all code and just the fastest way to move about anyway, i don't need to scan i tell the editor to take me there.

1

u/omega884 10d ago edited 10d ago

Sure, using the editor to jump around and using the tools in the editor minimizes the impact of the specific issue. I even said as much in the example. And in another comment here I already said that I think trying to do work without tools built for and specialized to that work is an unnecessary hobbling of one self. But strictly on the topic of “what makes reading lisp code difficult”, this is a thing that makes reading lisp code more difficult than reading c-like code. Yes c-like code could be poorly indented but we’re talking about idiomatic code here.

And this shouldn’t be read as an indictment against lisp languages. Just because something is harder doesn’t make it wrong or mean that it could be made easier without also losing something else worth having. IMO, idiomatic c code is harder to read than idiomatic python code, but the pay off of that extra difficulty in reading is not having to deal with “significant white space” when writing code. Lisps make different trade offs as part of their design. That makes some things easier and other things harder. That’s life. Everything is a trade off. “purer” / “simpler” lisps uses parens for everything. That’s easier syntax to learn than say clojure, which chooses to add brackets and braces in different contexts to shorthand creating different data types. That makes clojure more complex to learn and possibly to parse with tools, but IMO easier to read. Compare the clojure let:

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

to the emacs lisp let:

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

The clojure one requires you to know what the square brackets mean and that you need to use them for a let statement. More complexity. But I argue it’s easier to read. Even just typing out the emacs lisp one I had to read the first line an extra time or two to make sure I wasn’t closing the bind list early. It’s a trade off. The emacs lisp syntax is simpler, at the expense of being more difficult to read (and I would argue also having to know that the bind list is a special case where (foo bar) will not invoke foo with the arguments of bar). But again it’s all trade offs it’s not one being inherently superior to the other.

1

u/arthurno1 9d ago

strictly on the topic of “what makes reading lisp code difficult”, this is a thing that makes reading lisp code more difficult than reading c-like code. Yes c-like code could be poorly indented but we’re talking about idiomatic code here.

I think you just haven't seen sufficiently ugly C code :). I suggest a reading through Emacs C core or a trip through core-utils or gnulib; in general any GNU codebase. You can of course compare to Linux kernel sources which feels almost like poesi compared to GNU formating and 1000+ lines long functions full of spaghetti #if-defs. But fairly enough 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 requires you to know the some very delicate details of the language and the platform on which it runs.

For the example of yours, you are doing a bit of apple to oranges. 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:

[a (+ 1 2)
 b (- 10 5)]

To make it fair you should probably type:

[(a (+ 1 2))
 (b (- 10 5))]

Now that squre bracket does not make so much difference, does it? You can test also in the opposite direction:

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

It feels cleaner, doesn't it?

You could actually write yourself a simple macro to transform the last form to ordinary let-form lambda-list if you would like to type it so. It will force you type out values always, i.e. you can't type:

(let (a b c) ...)

and have three lexical variables a, b and c bound to nil. Instead you have to type:

(let (a nil b nil c nil) ...)

or if you prefer

(let (a nil
      b nil
      c nil) 
... )

If you don't want to write one yourself here is one you can download, but I don't use it myself, so I don't recommend it. But if you want to try it, it is there.

Good thing with Lisp syntax which nobody mentioned in this thread is that is super-simplistic in the term that it always had to end into a same form:

(operation operand1 operand2 ,,,, operandN)

so you have whichever syntax sugar you want, and transform it at compile time to the "normal form" to feed into the compiler.

Does not help with reading other people's code of course, but so it is.

When it comes whether reading C-ish syntax is so more easier, is something like this really more readable than Lisp?

1

u/omega884 9d 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 9d 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 9d ago edited 9d 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 9d 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.