What makes Lisp difficult to read?

59 points by veqq 17 hours ago on lobsters | 36 comments

tusharhero | 15 hours ago

Many people, myself included, find this syntax more difficult to read than the notation of languages like Python, Java or C.

And many people don't. I feel like the article works backwards to justify why it is difficult to read. Nested function calls are hard to read in general. That is why you avoid doing that. Or just have them go on newlines.

chinmay | 15 hours ago

Yeah, whenever someone asserts that it is inherently harder to read I feel like there should be studies into how people completely new to programming are able to read each of them once taught what it means

mlatu | 13 hours ago

i agree but feel the need to point out that having learned one of them might make it harder to understand the other. so.. you'd want to first test teaching them C/Java/Python, then Lisp, and with another group you'd do it the other way around...

technomancy | 6 hours ago

What makes lisp difficult to read?

Inexperience.

munificent | 2 hours ago

The article literally talks about this claim in the second paragraph.

I don't know to what degree familiarity matters more than the other points the author is making, but it's not like they are oblivious to this claim.

technomancy | 17 minutes ago

If it were true that there's a reason other than inexperience, then I would expect over the years to have encountered more than zero people who have spent several years working in lisp languages who still think it's inherently difficult to read. This article feels more they decided up-front that it's more difficult, and then from there went to hunt for rationalizations as to why.

It could be true that (for example) C-style languages take N months of experience to gain M "units of familiarity" while in lisp-like languages it takes 1.5xN months, and maybe the reasons in the article could go towards explaining the difference in the curve. But they're entirely unconvincing for a blanket "hard to read" claim.

munificent | 14 minutes ago

Three things can be true:

  • A system can be in aggregate more difficult to some degree compared to other systems.
  • People still prefer the system and feel content and productive in it.
  • Familiarity also has a large effective on the difficultly.

I think there is wide agreement that, say, Russian and Mandarin are harder to become fluent in than Spanish. Even so, there are happy fluent Russian and Mandarin speakers.

I suspect that with Lisp, most of the discomfort with the syntax is lack of familiarity, but that also there is some component of it actually being a somewhat more cumbersome syntax even for skilled readers.

sjamaan | 3 hours ago

And close-mindedness

kingmob | 11 hours ago

As a former attentional neuroscientist, I'm pleasantly surprised to see people get interested in things like the Stroop, pop-out, and gestalt effects.

However, I must caution against extending them like this. They evolved for the needs of our visual world, and aren't as general as you might imagine.

Saying stuff like

Due to the spacing created by the placement of parentheses and commas, C-style notation more often obeys the law of proximity than Lisp-style notation. C-style function calls with only one argument require no space characters. This allows such calls to be perceived as a single visual entity.

requires proof, not assertion. It's not even clear that "perceived as a single visual entity" is a good thing here. How do we know it doesn't impair the ability to scan for function names?


Worse is the "Mental Stack" section. First, they have working memory (WM) confused with short-term memory. They're not always the same thing.

Second, any code, even at trivial nesting levels, involves way more things than can be held in human WM. Recalling global definitions (your functions, the stdlib namespace, etc.) alone would blow through the WM budget. (Humans actually use a chunking strategy to recall larger pieces, but it's unlikely that we could store much of a stdlib in WM.)

Instead, we're much more likely to engage in things like priming/activation of non-WM memories. This fits with why many of us have to sort of "refresh" our brains on the context around a piece of code before we can begin productive work on it. By making relevant memories more accessible at the unconscious level, we're prepared for them to bias our actions, or even bubble up into awareness as needed.


About the only thing I truly agree with here is the value in having multiple bracket styles, though not for the reasons given.

They can't be explained purely as a gestalt effect, because that wouldn't work when the brackets are on different lines. (No larger visual object is formed.) Instead, I think the use of varying brackets allows us to speed up the search for the corresponding pair by cutting down on the number of shapes we have to examine.

I was a bit annoyed that the blur thing seemed to just be horizontal (and made the whitespace vanish) and not vertical (where both probably would be equally unreadable and ungrouped).

kingmob | 6 hours ago

Yeah, the horizontal examples don't help make a strong case, because most real world code just doesn't have that many things on one line.

Yogthos | 7 hours ago

Mostly preconceptions about how code should be written. I worked at a place where we used Clojure and hired students as interns every four months. We started by hiring students from third and fourth year, and what we noticed was that their main problem was unlearning the imperative style they internalized because they already had a lot of exposure to languages like Python and Java.

We then switched to hiring students from second year who had a good grasp of algorithms and data structures, but had little actual programming experience. And they could learn to write useful Clojure code after about a week or two. Very few had trouble using the language, and most found it completely natural.

yogsototh | 11 hours ago

diclamer: I use Clojure as first programming language.

First obligatory: https://wiki.haskell.org/Wadler's_Law

I think what makes LISP syntax a LOT better is that it exposes the AST more clearly. I agree with some of the claims being made here (even though, I find LISP / Scheme / Clojure a lot easier to read than Python, Java, C, Go, etc...). And I am wondering if we could find the best possible syntax to easily parse the AST in our minds just by looking at the ASCII text.

For example, use indentation à la Python (I know this approach has already been tested by some, and myself included). But to really makes that work, I think the prefix notation is incredibly more important than the argument about parenthesis that may difficult to parse from far away. Knowing that there is not infix notation at all in your language makes it a lot easier to immediately know what each "symbol" should do.

Example: a b c vs b a c you would need to KNOW that a is a function that takes two arguments. In Haskell the distinction between prefix and infix is "if the word contains a symbol it is infix otherwise this is prefix. So in haskell is either something like f b c or b <*> c (whatever operator you want). Now consider that both arguments b and c (and potentially the a or f`` or <*>`) could become bigger ; it means that you read the operation like:

<long description of first argument> <long description of operator> <long description of second argument>

Also, the most common place for the tension is about arithmetic operators. People tend to better read 2 + 2 than + 2 2. But as soon as you add recursive complexity it starts being worse and worse and more difficult to really "parse". So if your language uses infix notation ; well.

2*2+3/4-x*12 becomes quickly more and more difficult to parse as, thus you immediately naturally want to add parenthesis anyway (even if you use infix notation). I really prefer the fact that in LISP it is (almost ; thanks macros) always clear you use prefix notation.

And also if you continue in this direction about "the most important is for the reader to immediately see the AST" ; you end up with arguments about using ASCII text vs GUI to immediately show the AST...

Well if this was an easy problem, we wouldn't talk about syntax that much :)

All this wall of text to say... I think the best syntax is the one that reduce the cost of "parsing in my mind from raw text to AST".

nemin | 10 hours ago

For me it's the indentation. Lisps are in general very indentation heavy languages due to let for instance.

Example:

int x = 0;
int y = 5;

while (x < y) {
   if (x < 2) {
       x *= 2;
   } else {
       x++;
   }
}

vs

(let loop ((x 0)
           (y 5))
  (when (< x y)
    (loop (if (< x 2) 
              (* x 2) 
              (+ x 1))))

There is just so much whitespace everywhere. C(-likes) tend to the left and scopes are opened far less frequently than in Lisps, while Lisps tend towards "down and right" if that makes any sense.

And sure, we could probably merge the line with x and y's declarations and maybe even make the if single line, but then you start having very long lines, which is arguably just as hard to read.

I found myself ignoring the parens quickly enough, but the wildly jumping around line starts really slow me down. Python is similarly indentation heavy, but due to the fact, that adding new variables doesn't open new scopes and it also tends to the left like C makes it a lot more tenable for me.

reidrac | 4 hours ago

I agree. What I find hard to read is not the parentheses, is the whitespace.

But there are plenty of people that don't have that problem, so it is a "me" thing.

an_origamian | 5 hours ago

Hopefully slightly improved (Emacs Lisp):

(defvar x 0)
(defvar y 5)
(while (< x y)
  (if (< x 2)
      (setf x (* x 2))
      (incf x)))

(This code is infinite loop)

My own wishlist is to have assignment operators (and other operators) and an if form that is more normal: if {} else {}

nemin | 3 hours ago

(This code is infinite loop)

Ah damnit, I was trying to be so careful, but I ran right into what I wanted to avoid.

defvar

Yeah, I guess I should've specified that I find this most prevalent in Scheme, where using define inline is not really kosher (some implementations support it, but the spec doesn't require them to).

My own wishlist is to have assignment operators

I'm curious, what would that look like?

I'm personally super fond of what Clojure does. Having accessors makes things so much more pleasant

omidmash | 6 hours ago

The article is well written, but I don’t necessarily agree. I don’t think about any of this when writing LISP code. Emacs deals with my parens, as does paredit with structural editing. I don't read LISP by parens, but by the indentation. This discussion ignores the things that make LISP work: sexps, infix notation, "lack of syntax" and lists. By changing these properties , one destroys what makes LISP great in the first place.

Clojure uses different types of closures, like braces, brackets and parens. So it does exist, and one could use it if they so incline, but trying to say the things that inherently make a language work (uniquely) are not good, is not necessarily sensible. Threading macros also exist and would solve some of the author's problems, and implementing them is not difficult.

As a side note, it makes me wonder how this discussion would look like if it were comparing LISP with another functional or functional-first language, like Haskell or Elixir.

Edit: I also agree with Yogsototh, mentioning that

I think what makes LISP syntax a LOT better is that it exposes the AST more clearly.

and would put it on the list of the things that make LISP work the way it does.

david_chisnall | 6 hours ago

I've only ever edited Lisp in an editor that does rainbow parentheses (each nesting depth has a different colour). And that leads to some interesting problems where code is very easy to read in an editor and very hard somewhere else.

I think that's often a big problem for Lisp-family languages (a class in which I'd include Smalltalk for the purpose of this discussion): they are easy to develop and explore in an interactive environment and much harder serialised as plain text files. Unfortunately, the industry has standardised on text (mostly UTF-8, sometimes ASCII) files as the format.

I think the really big thing I'd consider with a new language is the ability to nicely syntax highlight it with no additional context. C++ code is the worst for this. Unless you parse an entire compilation unit, you often can't tell if an identifier is a type or a variable name (note for Haskell programmers: many people believe these are different things), so doing much more than highlighting keywords is hard. Something like Go has a very simple grammar and so it's trivial to do syntax highlighting in a little bit of Python or Ruby that plugs into your code hosting site's web UI.

An early version of D had explicit support for HTML tags in comments, on the assumption that you'd always edit code in a rich-text editor and so it could render styles in comments nicely. I saw a proposal inspired like this for a language to use a file format that explicitly stripped all XML-like tags as the first step of preprocessing, so you could store all of the required markup inline and just open it in a browser to see the fully marked up and cross-referenced code, with your editor filling in the bits that you didn't type after parsing the code.

cblake | 3 hours ago

I usually try to summarize your "rainbow parens" point as "humans are just not great at unary past 3..4 things and this shows up in many notations". Examples: the classic counting tally strike marks will be 4 bars and then a 5th diagonal slash, batches of 3 digits in "1,234,567.00" (or whatever radix marks you want), 0xFFAB_1234 or many, many other cases.

Lisp people will tell you, "if you, rather than a computer, is counting parens, then you're doing it wrong." Essentially that is an appeal to tooling, but if the tooling isn't there (as you said), then you can be stuck with a PITA notation. Honestly, if Lisp code formatters even did batches of 3 closing parens like "))) ))) )))" it would already be better, yet that is very rarely seen in Lisp code in the wild, at least in my experience.

rheaplex | 45 minutes ago

Different brace styles are mostly syntactic sugar for the developer, and I find the context switch from (list 1 2 3) to [vector 1 2 3] slows me down when visually scanning the code structure and searching for depth and context cues, without adding anything useful to me in return.

The regularity of Lisp is what makes me productive when reading and writing it. I feel I'm in a tiny minority on this, though, and it can't simply be relative familiarity that causes this. I hope.

kalkin | 9 hours ago

To preface: Not a lisp fan, but used enough of lispy languages to have some opionion

(((((((function aap) noot) mies) wim) zus) jet) teun)

No one writes it like this? Am I missing something? why would i write a function that returns a function, that returns…? If something like this is actually needed, then the right way is to write a macro!

(aap noot ((mies wim) zus) (jet teun vuur))
aap(noot, mies(wim)(zus), jet(teun, vuur))

I don't see any difference. Both are suboptimal, but the first variant provides at least some sensible visual grouping.

(sum
 (filter
   (split (read (open "input.txt")
                ",")
   (lambda (it) (> it 0)))

Maybe just format it in a readable way?

(sum (filter (split (read (open "input.txt") ",")
                    (lambda (it) (> it 0)))))

Looks good and readable to me.

I mean i get it, i hate the parenthesis obsession of lisp too, but if you are finding yourself nesting too much, you are probably doing it wrong. You probably want to use macros to generate your code or you need to do classic refactoring.

skami | 8 hours ago

I'm unconvinced by these arguments. But I would agree that addressing the issues — except for "prefix notation puts parentheses further apart" which is, in my opinion, essential and a net positive — raised by the author would improve Lisp readability further.

With the blurred example, for instance, I reached the opposite conclusion. My reasoning was something like: Lisp, ok clearly three arguments. Then, looking at the blurred C, I mistakenly thought that it was different and went back to the unblurred versions to compare. The only one that gave me pause was the C (blurred). There is clear identification of grouping in the blurred Lisp.

To share a few non-scientific experiences, I often program on monochrome E-ink displays. There, with next to no formatting in the editor, I find Lisp so much easier to work with than anything else. On a color display, with something like rainbow-delimiters-mode that highlights matching parentheses, the difference is even greater in favor of Lisp syntax, as whole expressions are grouped by color.

By the end of a long day of programming, I often increase text size in C++, but I feel no need to do so in my Lisp files.

thecloudlet | 7 hours ago

To me I thinks it’s just about how you are accustom to daily usage.

I started with C for years. C++ look really annoying syntactically and eventually I feel great skimming the code after 2 months.

The same thing happened I went to Haskell for 6 months. The first 2 months is a pain but eventually it flows.

I write some lisps Scheme, Racket, Elisps. Sometimes the same feeling will hit back and eventually get always.

I think it’s just your brain is doing its deep leaning process and rewriting where we should focus our attention.

By the way I feel lisps syntax is more easy to translate into English because the action is placed in the very front. Foldr filter map …

Claudius | 15 hours ago

This is why in LispE, I introduced the "." operator to reduce the number of parentheses. Hence: (f (g (v x))) becomes (f . g . v x). Of course I didn't invent this operator, I borrowed it from Haskell, but it simplifies a lot the way expressions can be written or modified. In the same way, I use the Java indentation scheme a lot as I find it more readable and more pleasant to read, which is as subjective as subjective goes.

I also allow "[]" to be used alongside "()", specially in pattern definition:

(defpat test( [integer_ x] ) ...) which is the same as (defpat test( (integer_ x) ) ...) but is more readable.

makishimu | 9 hours ago

One more alternative syntax is wisp, https://srfi.schemers.org/srfi-119/srfi-119.html

thecloudlet | 7 hours ago

No please, it just more pain. () are fine.

an_origamian | 5 hours ago

hawski | 5 hours ago

Is there something that would make this in reverse like a pipeline operator?

Like (f (g (v x))) becoming more like v x | g | f? That probably goes against the tide of Lisp.

Clojure has threading macros. There are a couple of packages for Common Lisp, like cl-arrows. Scheme and Racket have other packages, too, and a SRFI.

flockofbirbs | 7 hours ago

Lisp is easy to read.

alper | 5 hours ago

I do not find (- n 1) to be clearly inferior to (n - 1)

Oh I do. This is a machine artefact more than anything else?


I'd say stacks of parentheses are always hard to read in any language and Lisp intrinsically generates more of those.

donio | 3 hours ago

If you compare the factorial example the Lisp and C-style versions both have the exact same number of brackets: 14. The difference is that with Lisp you get nice consistent parens that gracefully melt away but with the C-style syntax your eyes have to stumble over a jungle of closing punctuation: ))))) vs ));}}. No wonder C-style has to split them into multiple lines and waste 42% of the vertical space. This gets even worse with languages that also abuse "angle brackets" and other punctuation.

Here is the full punctuation breakdown for the factorial example:

Lisp:
(()((=)(*((-)))))

C-style:
(){(==){;}{(*(-));}}

rheaplex | 50 minutes ago

I find it easier than infix notation because I don't have to think about operator precedence bugs.

xnacly | 13 hours ago

Im guessing the braces, but honestly, this is just a case of getting used to it, rust is also hard to read coming from c, same with c++, i can not understand all the symbols in c++ just because I am avoiding learning the language

toastal | 11 hours ago

Would have been nice to have seen a postfix language as comparison (such as many concatenative languages). These tend to left to right & without all the parentheses where function application & function composition are equivalent.