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:
- Sometimes
(f x)
is a function call, andf
is the name of the function, andx
is evaluated as an argument. - Sometimes
(f x)
is a macro invocation, andf
is the name of the macro, andx
may be treated specially (it's up to the macro to decide). - Sometimes
(f x)
is a binding. For example,(let ((f x)) …)
binds a new variablef
to the valuex
. - Sometimes
(f x)
is a list of names. For example,(lambda (f x) …)
creates a function that has parameters namedf
andx
. - Sometimes
(f x)
is a literal list. For example,(quote (f x))
. - 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.
Update: Also see reddit comments; [2012-06-29] Rich Hickey calls multiple uses of parentheses “overloading” in this talk; [2018-10-12] Also see Heinrich Hartmann's blog post.
Labels: programming
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.
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).
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.
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?
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. :(
"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.
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.
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.
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.
Take a look at the DrScheme IDE. It analyzes your code, including macros, and knows all the things that emacs can't figure out.
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.
"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?
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.
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.
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).
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.
> 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.
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.
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...
A lot of comments saying brackets aren't really that bad, and that you get used to reading Lisp after you've used it for a while.
Both of these arguments ignore what the author is actually saying. Python is far more readable than Lisp. Ask anybody who's never written in Python or Lisp to read code from either language and comprehend it. Remove all the brackets from the Lisp code even. I bet you that, without exception, the Python code is far more readable. Does that make Python better? No, it just makes it more readable. Readability is, however, a key feature for many programmers.
I've discovered that if I use some of Python's higher-level constructs in creative ways (that seem to be natural for the problem I'm doing), Python can get ugly and unreadable pretty quickly.
I've used Python mostly, and I'm in the process of learning Lisp, so I can't quite make the comparisons between either (except to say that, so far, I like Lisp's expressiveness more); nonetheless, both are more readable than, say Perl, or C :-).
There are at least three more items you could add to your list, it seems to me. (I'm just learning the language, so I don't vouch for the absolute accuracy of everything I say here.)
7. Sometimes (f x) means that f is a parameter and x is a default value within a method definition.
8. Sometimes (f x) means that f is a formal parameter and x is a specific type within a defmethod expression (which defines a polymorphic method).
9. Sometimes (f x) means that f is a field of x (equivalent to x.f in Python or C++).
I fully agree with your main point. I would elaborate on it as follows: By dispensing with most syntactic markers (keywords, punctuation), Lisp achieves conciseness and probably ease of compilability but at a cost: it makes the code harder to read. Loss of readability is always a drawback, whether it translates into an increased learning curve for the beginner, an increased cognitive/memory load for the reader, or an increased necessity for the writer to add comments to the code. One could argue that the loss of readability is justified because it results directly in some other gain (less typing, fewer lines of code to take in at once), or that it's not as dire as it would seem at first glance, but that's not the same as saying that it doesn't exist, or that once you've "arrived" it doesn't matter how long or steep the learning curve to get there was.
It's basically like writing a novel without vowels or punctuation. It can be done, and the book can be read, but if the reader has to turn to a sentence at random, it will take him/her far longer to understand what's going on than if those supposedly redundant elements were included.
One more point: it's true that a language like Python or C++ does not remove all ambiguity, and indeed a parameter list looks like a tuple when you ignore everything around it. But Python/C++ reduces the level of ambiguity because (a) there are fewer possible interpretations for a fragment and (b) the amount of context needed to resolve the ambiguity is smaller, so you generally only need to go back one or two tokens instead of one or two or three lines.
There are at least three more items you could add to your list, it seems to me. (I'm just learning the language, so I don't vouch for the absolute accuracy of everything I say here.)
7. Sometimes (f x) means that f is a parameter and x is a default value within a method definition.
8. Sometimes (f x) means that f is a formal parameter and x is a specific type within a defmethod expression (which defines a polymorphic method).
9. Sometimes (f x) means that f is a field of x (equivalent to x.f in Python or C++).
I fully agree with your main point. I would elaborate on it as follows: By dispensing with most syntactic markers (keywords, punctuation), Lisp achieves conciseness and probably ease of compilability but at a cost: it makes the code harder to read. Loss of readability is always a drawback, whether it translates into an increased learning curve for the beginner, an increased cognitive/memory load for the reader, or an increased necessity for the writer to add comments to the code. One could argue that the loss of readability is justified because it results directly in some other gain (less typing, fewer lines of code to take in at once), or that it's not as dire as it would seem at first glance, but that's not the same as saying that it doesn't exist, or that once you've "arrived" it doesn't matter how long or steep the learning curve to get there was.
It's basically like writing a novel without vowels or punctuation. It can be done, and the book can be read, but if the reader has to turn to a sentence at random, it will take him/her far longer to understand what's going on than if those supposedly redundant elements were included.
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).
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).
This is one of the little things that I like about Clojure. Clojure adds to Lisp's literal lists syntax for literal vectors, maps and sets. Function arguments and let bindings are written as vectors, with square brackets instead of rounds. This little bit of added syntax makes a huge bit of difference.
To use your reasoning, in Python f, x could be:
- The parameters of a function: def foo(f, x)
- The parameters of a lambda: lambda (f, x)
- Arguments to a function: foo(f, x)
- Some characters: "f, x"
- A tuple construction: f, x
- The left hand of a variable binding: f, x = (1, 2)
Anonymous: I agree, Clojure's added syntax is nice at times. I was also involved with the development of e7, another variant of Lisp with a few extra syntactic constructs, so I guess my desire for a little more syntax in Lisp has been around for a while.
rayiner: Yes, this is true. However, it's a question of how far up the parse tree you have to go to figure out which of these it is. In most mainstream languages it's local (and easy for automated tools to do); in Lisp it's global (and not easy to do without executing code).
Consider for example modifying someone else's code, where you don't know every definition in the program, but are only looking at some small part of the program. You see (defun (lookup x) ... (f x) ...) and decide to clean it up into (defun (lookup name-map) ... (f name-map) ...). Is this a valid change? Well, it depends on what f does, right? If f is caddr then you're fine but if f is quote then you don't want to rename that x. That's a local check but it requires global knowledge of all macros in play, including any macros defined in other modules that are imported into this one. It also depends on what's surrounding it. Suppose x occurs in the context (g ((f x) ...) ... x ...). If g is destructing-let then x is defining a name, and you want to change neither of those x's. If f is quoting x then you don't want to change that one, but you do want to rename the other x (probably). Or maybe you want to rename both of these. Determining which x's should be renamed depends on all macro definitions. Alpha-renaming is easy in lambda calculus but hard in Lisp.
Or consider seeing (progn (f some-long-name) ... (f some-long-name)) and you want to introduce a local name. You change the code to (let ((y some-long-name)) (progn (f y) ... (f y))). Is this inverse eta-reduction a valid change? If f is prin1 then it's fine but if f is incf then it's not. That's also a local check, but requires global knowledge. In addition, you have to know how progn works (it may be a macro as well), and all the way up to the top level. If the (progn ...) is enclosed by (destructing-let ((progn ...) expr) ...) then hey, it means something completely different, and the substitution isn't valid. The code change to add a local name y to alias some-long-name has a dependency on the entire module, and possibly the entire program including all imported modules. Yes, the farther you go up the tree the less likely there will be a special form that makes this matter, but it's always a risk, and it gets in the way of automated tools like Emacs syntax-highlighting or refactoring tools. In most mainstream languages you look at the statement or the function — it's local. Python's locals() and triple quoted strings make this a little harder.
In retrospect, I shouldn't have named Python specifically in my blog post. Almost everywhere I wrote Python I could've said “a conventional language”. At the time (nearly four years ago) I was using Python as my day-to-day language, so that's what was on my mind.
If you ever come across this post, you should know that Amit is well aware of DrScheme, now known as DrRacket. -- Matthias
Clojure uses Vectors instead of Lists for binding forms, and forces keywords to start with a colon. Lisp IDEs *COULD* tell you the difference between macros, pure functions, procedures, binding forms, special forms using fonts, font styling and colors for the keywords, based on run time or static time information, but they don't because thats how Lisp has always been and thats how Lisp will always so screwed be you Lisp newbies : (. Also Lisp IDEs could also style parenthesis differently based for cond/if/when, macros, etc..., but nope.
Post a Comment