I've met lots of people who complain about Lisp and lots of people (especially Lisp folks) who complain about Python. Lisp is very elegant. There's something nice about its syntax (don't laugh!). The uniformity lets you do all sorts of neat things once you have macros. The basic syntactic construct in Lisp is the list, (a b c …), and it can mean lots of things:

  1. Sometimes (f x) is a function call, and f is the name of the function, and x is evaluated as an argument.
  2. Sometimes (f x) is a macro invocation, and f is the name of the macro, and x may be treated specially (it's up to the macro to decide).
  3. Sometimes (f x) is a binding. For example, (let ((f x)) …) binds a new variable f to the value x.
  4. Sometimes (f x) is a list of names. For example, (lambda (f x) …) creates a function that has parameters named f and x.
  5. Sometimes (f x) is a literal list. For example, (quote (f x)).
  6. Sometimes (f x) is interpreted in some other way because it's enclosed inside a macro. How it's interpreted depends on the macro definition.

The ability to use the same syntactic form for so many different things give you great power. You can define all sorts of cool things this way. I'm writing a pattern matcher that uses list expressions to define patterns and macros to interpret those list expressions. Macros are great for writing elegant, concise code.

The trouble is that you can't easily tell just by looking at (f x) how to interpret it. It could do anything. You'd think maybe a text editor like Emacs (which uses Lisp as its native language) would be able to help you in some way. But no. Emacs can't tell either. So how can you, the person reading the code, figure it out? Well, you can, but it takes a lot of effort. You can't determine the syntactic meaning of code (e.g., whether it's a definition or an expression) by looking at the code locally; you have to know a lot more of the program to figure it out. Lisp's syntactic strength is at the same time a weakness.

Python on the other hand has no macros and doesn't give you much to write concise, abstract, elegant code. There's a lot of repetition and many times it's downright verbose. But where Lisp is nice to write and hard to read, Python makes the opposite tradeoff. It's easy to read. You can determine how to interpret something—a string, a list, a function call, a definition—just by looking at the code locally. You never have to worry that somewhere in some other module someone defined a macro that changes the meaning of everything you're reading. By restricting what people can write, the job of the reader becomes easier.

Lisp seems to be optimized for writing code; Python seems to be optimized for reading it. Which you prefer may depend on how often you write new code vs. read unfamiliar code; I'm not entirely sure. What bothers me the most though is not that these two languages do different things, but that the people who argue about it seem to think that there is one “best” answer, and don't see that this is a tradeoff. When I'm writing code I prefer Lisp; when I'm reading code I prefer Python. I think this is an inherent tradeoff—any added flexibility for the writer means an added burden for the reader, and there is no answer that will be right for everyone.

–Amit

P.S. When I read debates online, I have a bias towards the people who view these things as tradeoffs and a bias against the people who say there's only one right answer and everyone else is stupid or clueless. This has sadly pushed me away from Lisp, the Mac, and other systems that I think are really good but have fanatical communities. When you're in a debate, consider that the other person might not be stupid, and there might be good reasons for his or her choices. You'll not only learn something about their position, but you'll be more likely get people to listen to you and adopt your point of view.

Labels:

19 comments:

willyh wrote at April 23, 2007 11:58 AM

Lisp code is understood in context and a lisp aware editor (like emacs/slime) will highlight code appropriately based on the context. It is confusing at first but even in a plain text editor the context tends to leap out at me now.

Arto Bendiken wrote at April 23, 2007 12:00 PM

While I wouldn't entirely disagree with your conclusion per se, the comparison might have been better qualified. After all, it's perfectly possible to write Lisp restricting yourself to the well-established syntactic subset that Python supports, too.

(Nothing in your article points out parentheses as a readability problem, merely the possibly unknown syntactic meaning and evaluation rules in Lisp code you didn't yourself write.)

The comparison, as it stands, is somewhat analogous to an argument juxtaposing manifest static typing ("readability") vs dynamic typing ("writability"), without considering the possibility for Hindley–Milner type inference (readability + writability).

Anonymous wrote at April 23, 2007 12:12 PM

How long have you have been programming in Lisp? If you do it some time, you will learn to read it as well as Python code.

First rule is to always use Emacs or some other editor that uses proper indention rules to indent Lisp code right. Indention is extremely important to Lisp people. Second rule is to ignore 99% of the parentheses. Only the innermost parens are meaningful (you don't need to look ohters if code is well indented). Then you read Lisp code as easily as Python.

Peter wrote at April 23, 2007 1:46 PM

Interesting idea. Certainly there are languages which are "easy to write, hard to read", (Perl is the canonical example). On the other hand, being easy to write does not translate to being harder to read in an absolute sense, like C vs. Python (Python is much easier to both write and read).

I think there's a distinction lacking here:

Code that's easy to read, and code that's easy to understand. Elegance--to me--is simplicity of understanding, and readability comes from the discipline of things like correct naming which Python implicitly and partially does for you (by adding information via syntax):

(iterate-folder foldername repair-file)
vs.
iterate-folder(foldername, repair-file)

are both clear, while:

(x y z)
vs.
x(y, z)

Yes, the Python version gives you more information (x is callable, y and z are preexisting variables), but you still have no idea what it's doing.

In the first example, you don't know if iterate-folder is a macro or function, but is that bad? Why do you need to know?

Amit wrote at April 23, 2007 5:32 PM

willyh: does Emacs/SLIME highlight differently than regular Emacs Lisp-mode? Lisp-mode doesn't address the various uses of s-exprs I listed, although someone on the #emacs IRC channel was working on something better (I'm looking forward to trying it out).

Arto: yes, you can write a subset of Lisp, but when reading someone else's code, you don't have that luxury. It's when reading someone else's code that I appreciate Python more.

I don't consider the parentheses much of a readability problem. You get used to them. The problem is not the parentheses themselves but that the same form is used for so many different things (which is also a strength, as I mention).

I agree with Peter that it's possible to make something hard to read and hard to write, but what I'm really interested in is whether there's a way to make something both easy to read and easy to write. :)

I also agree with Peter that being able to figure out syntactic constructs isn't sufficient for understanding the code. However, I think it's a necessary step. You need to know whether (f x) is a function call or a list of names, do you not? How do you trace your code without knowing where f is being bound? Emacs doesn't help you with this. :(

Anonymous wrote at April 23, 2007 6:20 PM

"You need to know whether (f x) is a function call or a list of names, do you not?"

It's a function call unless its quoted, in which case its a literal list.

John Nowak wrote at April 23, 2007 6:20 PM

The supposed ambiguity of the syntax simply isn't a problem in practice. You're bet off putting in some serious work with a language like Scheme before making what is an admittedly intuitive judgement. If you use an editor with automatic indenting (I use DrScheme -- you don't need emacs-level complexity), that alone is usually plenty to get you by. DrScheme has many other features as well that help out. As for the differences between macros/functions/etc, it generally doesn't matter. And, of course, that's the point.

Amit wrote at April 23, 2007 6:33 PM

Anonymous: in (cond ... (f x) ...), (f x) is neither quoted nor is it a function call. Look at my list at the top of the post to see other examples.

John Nowak: Yes, it's entirely possible that with more use I'll get used to it. But getting used to something doesn't mean that something is good. Being used to something makes me less likely to spot a problem, because my brain has worked around it.

Amit wrote at April 23, 2007 7:30 PM

I enjoyed the Reddit comments, although some people misinterpreted my post as giving reasons to pick one language over another instead of about the tradeoff in syntactic uniformity, and maybe I shouldn't have included my P.S., which was unrelated to the main post.

Anton wrote at April 23, 2007 9:33 PM

Take a look at the DrScheme IDE. It analyzes your code, including macros, and knows all the things that emacs can't figure out.

reeses wrote at April 23, 2007 10:19 PM

I happened to be thinking about this recently because I'm finding I have to work a lot more with "syntactical" languages with [] and {} in addition to (), so it takes me a lot longer to read OCaml, Oz, or even Java and C. I realised that at this point in life, I can read Lisp more easily than anything else.

That is, until I can't. To your point, all it takes is a LOOP in the middle of code to act as a speed hump and break my stride.

Anonymous wrote at April 24, 2007 6:21 PM

"in (cond ... (f x) ...), (f x) is neither quoted nor is it a function call"

Cond is a special form, and so indicates how to interpret (f x).

As for your other examples:

There's really no distinction between macro and function invocations when reading code. Sure, you don't know what a macro is going to do with x just by looking at it, but the same is true for functions.

Let and lambda (and defun and defmacro, etc) are also special forms, just like cond.

As for (f x) inside a macro: that's the exact same distinction I made before: you make the distinction of whether the list is evaluated or returned literally based on whether or not it is quoted. Just because macro definitions have some sugar to make quoting and unquoting easier doesn't mean this changes.

I also have an issue with your definition of local. While it's strictly true that we can't make these distinctions from just (f x), that fact isn't useful in any way, because there will be contextual information that clarifies the meaning, except in the case of function vs. macro invocations, which shouldn't matter anyways when reading.

And it's not like Python is free from ambiguity when looking at such a small scale. Is f(x) invoking a simple function or an instantiating a new object? Is a + b adding two numbers or concatenating two strings? Is foo.bar() looking up a method or a function?

Anonymous wrote at April 25, 2007 3:15 AM

I don't know any language where some fragment of code isn't interpreted based on its lexical context. Take python for example. "foo" might be the name of a class being defined as in "class foo: ..", a variable being read or written, a function being defined or called, etc etc. Likewise, a comma can separate the arguments to a function call, or the elements of a list literal. And so on, and on and on.

That lisp syntax is mostly based on parentheses, has really very little to do with anything here.

Alok wrote at April 25, 2007 5:48 AM

Trouble with your argument is that, once the code is written, it can be viewed in different ways. Like special editor environments, GUI constructs (? some day soon in the future, I hope), at different granularity and so forth. So if my code is very easy to write and parse, then I can view it in many ways, each convenient for the my current perspective.

But once you have written down the code in C++, Python or any other language, it can be difficult to view it as you want to. Because parsing Lisp is as simple as parsing lists, not so for anything else.

Kragen Sitaker wrote at May 29, 2007 8:48 AM

I mostly agree, particularly with respect to Lisp and Python; it's true what some commenters say that it's a difference of degree, not of kind, but it's still a difference.

But I don't agree that more freedom for the author necessarily equals more work for the reader.

For one thing, there's the small vs. large-scale distinction --- eliminating duplication from your code makes it easier to read on a larger scale, because it's shorter and you don't have to do vdiff on pieces of it to see what's different, but generally makes each individual instance of duplication a little harder to understand, because more abstract.

Another thing is that more freedom for the author can give them the ability to choose a more readable representation. For example, in Python, you generally have the choice between passing arguments by name or by position. This is additional freedom for the author, but it allows them to use named arguments when there are a large number of arguments or when most of them are null, and positional arguments where there are only one or two and the names would just be noise.

That kind of freedom can be abused, of course, and then there are choices that seem to me to add little expressive power --- like Common Lisp's case-insensitivity, MzScheme's option to use [] or {} for () when you like, and C's choices about which line to put your braces on. I'm not arguing that these are abominations upon readability, just that they're mildly bad, and it helps readability that, say, Python doesn't have the brace problem.

I don't think the problem is that Lisp allows the author too much freedom to represent their meaning in idiosyncratic ways in their code; I think it's that different things, by default, look the same, and you have to engage your higher cognitive functions to figure out what they are, instead of simple visual pattern-recognition that frees up your neocortex for more useful work.

As a side note, it's kind of frustrating (and typical of the Lisp community) that most of the commenters seem to assume that you've never typed (+ 1 1).

Anonymous wrote at June 12, 2007 12:27 PM

looking at (f x) and thinking that it can be thousand different things makes no sense. Lisp code does not look like that. Don't look at micro-syntax. Look at a larger code level.

When you read/write Lisp code you use a lots of patterns to structure the code. As a Lisp programmer you learn to read and write code by these visual patterns. Lisp has some standard patterns that you can use for your own code. If you write a macro that uses one or more of these patterns, then the reader gets lots of visual hints. You have to learn a certain flexibility, because there might be an amount of well-known patterns, but you and others are free to invent new.

* use descriptive naming.
Lisp is about symbolic expressions. So name your symbols. *foo* is a global variable. +foo+ is a global constant. def-foo is a macro. %foo is an internal function. graphics:draw-pattern is a function in a package. graphics:draw-pattern* is a function in a package with a different argument list. :foo is a keyword symbol. ?foo is a pattern variable, foo-p is a predicate, foo! is a function with side-effects. MAKE-FOO creates some data. CALL-WITH-FOO calls a function. And so on.
Some Lisp systems have tens of thousands functions, variables and classes. You can bet that good naming discipline is essential.

* Macros use patterns.
WITH-FOO, BIND-FOO, DEFINE-FOO,.. all these have familiar patterns which are easy to spot after some time. There exists some tradition how macros should look like when used in code.

* Typical constructs used are binding lists, property-lists, argument lists, case lists, pattern lists, ...

* Often you see code defined by templates in macros.

* use :keyword parameters to make argument lists descriptive

* Code is automatically indented to a standard.
Indentation shows also the scope of a construct. Good code keeps effects local to that scope.

* Code can be analyzed and experimented with in the editor. The editor might color-code the source code.

* Parentheses disappear. After some time you read the code by structure and by the symbols.

* The editor connects to a running Lisp system and gets information (arglists, type, ...) about all the constructs from there. You get completion over functions, classes, macros, variables, etc. Source locations are recorded, so that you can move around in the code easily.

All these techniques and more make Lisp code nicely readable. Well written Lisp code has lots of visual structure.

Andy Freeman wrote at January 28, 2008 4:18 PM

> You can determine how to interpret something—a string, a list, a function call, a definition—just by looking at the code locally

Nope. If f is a property of a's class, a.f is a function call (with a as its argument). If f is an attribute, it isn't.

Anonymous wrote at March 24, 2008 12:41 PM

Some of these seem downright strange to complain about. Sure, in Lisp (f x) can be a literal list or a parameter list -- but in Python (f, x) can be a tuple or a parameter list, too. It's exactly analogous. (In practice, neither is a problem: a parameter list only comes after a def/defun or lambda.)

What does [1,2] mean in Python? Trick question: it might be a list, or (if it's quoted) it might be a string. Quoting, in any language, completely changes the meaning of a construct.

As long as Python has __getattr__, __getitem__, and __call__, I don't see this as a place where Python has a particular advantage. Just looking at some code, both seem roughly equal in readability to me (thanks to conventions, mostly), but Lisp has built-in tools like macroepxand-1 that make it even easier.

Anonymous wrote at August 16, 2009 8:42 AM

Guys, guys guys. She is right.
Do take the view of the opposing party to see the inherent good that his/her choice of language have to offer.
Don't even talk about python if you don't know the Zen of python.
There is "There should be one-- and preferably only one --obvious way to do it.", this explains why Python code will always be easier to read than Lisp, without being a veteran in the language.
Lisp didnt go anywhere after all these years and it sure won't go anywhere. Python on the other hand...

Post a Comment