I thought it would be a down with Lisp article, but it was like my experience with the language, omg, how cool. From that ancient introduction to Lisp all I learned was that Lisp was as close to perfection and beauty as I would get in a programming language.
Serious question: what is it about the "beauty" of Lisp that the HN community seems to like so much? To me, how Lisp looks is its worst quality--the number of parentheses is just mind boggling. I want to understand why it is so loved.
1. You write everything as a List. So there is syntax, but not exactly.
2. The data is also list, and your program is also a list.
3. Algorithm are basically List Manipulation(Stacks, Queues, Trees, Adjacency lists). So its easy to write complex algorithms in Lisp.
4. Tail call recursion. This part amplifies 3. further.
5. Functional programming features.
6. REPL. I mean like a real REPL.
7. Macros.
1 - 7 helps you to express problems and their solutions with code which represents exactly that. The boiler plate and other assisting code is largely non existent.
Another reason is if a thing has been there around for the longest, more people have thought about it. Both quality and quantity of literature of it are higher than others.
> Another reason is if a thing has been there around for the longest, more people have thought about it. Both quality and quantity of literature of it are higher than others.
Being older doesn’t necessarily mean either quantity or quality of literature will be greater. MUMPS has been around for much longer than Go, for example.
I would like to make the controversial claim that homoiconicity counts against adoption; the single representation carries little structural information and the meaning of a symbol is highly dependent on its containing context. Whereas the ALGOL-derived languages use different types of bracket or other means to indicate visually what the semantics are.
Ironically modern Javascript often replicates the bracket pileup, just with "})" instead. Python does away with it by having "nonindented newline" as an invisible semantic character that closes any number of scopes.
Hmm. I've been thinking for a while now that Lisp (and also FP) matches how some peoples' minds work, and doesn't match how other peoples' minds work. For those who match Lisp and/or FP, it's like a revelation, and it's very freeing. For others, not so much - it's a new way of programming, and you can do it that way, but why would you want to?
You said:
> ... the single representation carries little structural information and the meaning of a symbol is highly dependent on its containing context.
That makes me wonder if the difference is abstract vs. concrete thinking - those who by nature prefer abstract thinking will find Lisp more natural, and those who prefer concrete thinking will find it clumsy and unsettling.
Choosing the "right" programming language is not just finding the right language for the task (though it is that). It's also finding the right language that fits our minds - and our minds are not identical.
> the meaning of a symbol is highly dependent on its containing context
That being a problem is pretty much solved by not writing 1000 line top-level expressions with thirty nesting levels.
People who actually write Lisp are engaging their imagination for the program itself. Thus their imagination is too busy to come up with scary reasons how things could go wrong that would spook them out of continuing.
To be fair you can do that in most modern languages.
(I don't know lisp) but sometimes this approach just makes the code really difficult to understand afterwards. Does Lisp have something that makes it easier or better?
You can, but it's like picking your nose with boxing gloves on[1] - it's really clumsy. In Lisp, it's natural and easy. It's maybe the same in result, but it's really different in ease of use.
[1] Credit where due - I stole that phrase from my friend Michael Pavlinch.
The beauty of lisp-like languages isn't in the visual appearance of the code as text (the parenthesis and layout), but in the structure of the code as trees, the uniformity, and the homoiconicity.
- Different from what you are used to does not mean worse.
- Structural simplicity. Instead of having a huge mess of syntax to keep track of, it has only a very small number of constructs. Simplicity is beautiful.
I guess "ycombinator" (a LISP idiom/macro) as subdomain, and the fact that the site code was originally written in LISP, attracts lots of LISPers. I'm personally not a big fan of LISP, much less of the frequent hijacking of threads to degenerate into discussions about sexprs supremacy and other holier-than-though topics.
Presumably, this is because you've looked at Lisp code, but you probably haven't understood Lisp.
Criticizing it on the basis of parenthesis would be roughly equivalent to criticizing someone talking about the beauty of mathematics on the basis of the color of the piece of chalk they're using. It's true that it's criticism about aesthetics, but it's not criticism about the aesthetics that the mathematician was talking about.
Once you've understood in what sense people mean that Lisp is beautiful, you can disagree, but this disagreement will not concern parentheses.
As an aside: I have a similarly shallow aesthetic aversion to JS syntax for the same reason. When I encounter a 12-line ragged cascade of curly brackets, square brackets and parentheses, sprinkled with semicolons, I wonder how that can be considered OK.
Aside 2: with Lisp, you edit programs structurally, meaning that their position is essentially managed for you. This is what people refer to when they mean that "the brackets disappear after a while." You're not focusing on them; they're handled by something else.
In my editor, I have the closing brackets faded nearly into the background, which reflects how concerned I am about their existence.
>When I encounter a 12-line ragged cascade of curly brackets, square brackets and parentheses, sprinkled with semicolons, I wonder how that can be considered OK.
It's considered OK because the extra syntax carries semantic weight, and the semicolons disambiguate intent for the interpreter (because while semicolons are optional in javascript, leaving them out can lead to errors.)
And as someone just beginning to play around with (Arc) Lisp, I still can appreciate both paradigms. Neither is objectively wrong.
In many ways it is the parsimony that is beautiful. There is a ton of syntax in lisp, but there are not as many punctuation requirements. And you get to play with the grammar and many parts of syntax yourself.
Contrast this with most other languages, where there are many things the language does that you can not do. At least, not easily.
There aren't actually more parens in a Lisp program than a program written in a C-like syntax (which is really an algol-like syntax). They just stand out more for two reasons:
1. There is less punctuation in general, so the parens are more obvious. Instead of f(x, y, z) you write (f x y z). Without the commas, the parens stand out because that's all that is left.
2. There is only one kind of parens in Lisp whereas C-like languages use at least three: (), [], and {}, so that makes any particular kind of paren less prominent.
3. Lisps optimize tail calls, leading to easier and more efficient recursion. This does tend to increase depth of code.
4. The lisp punctuation style closes all levels on a single line, where most C code guidelines close one level per line. This leads to a "thick" chunk of close parens that students of lisp find harder to read at first.
> There is less punctuation in general, so the parens are more obvious. Instead of f(x, y, z) you write (f x y z). Without the commas, the parens stand out because that's all that is left.
There's also a historical componenent in all of this. Modern languages have adopted many features from the old Lisps that were quite unique back then. The same is true with the ML language family: between the two, ML and Lisp seeded a new generation of languages that are, on average, much more powerful than the languages in common use in the 1970s and 80s. (Smalltalk deserves a special mention, but it came later.)
I'm still fond of Lisp, but languages like Python, D, Nim (and increasingly, Rust) offer a lot of the Lisp affordances that I really valued -- in particular, easy compile-time metaprogramming. The biggest missing bits are Lisp's deeply integrated REPL-driven programming, and the use of program images -- which have many drawbacks but also some benefits.
IMO, everyone should write and maintain a medium-sized program in a modern Lisp (SBCL, CCL) at least once, just to get an appreciation for how different the programming / debugging experience is. So many well-integrated tools at your disposal, even at runtime.
Check also his other books: Algorithms and Data Structures, Programming in Oberon, Project Oberon, etc.
They are all available on the ETH website and his homepage (in various revisions). Basically everything you need to know about imperative programming on CS bachelor level for free.
I'm young too (21), I was taught programming in scheme. In UC Berkeley the very first CS class (called "SICP" lol) is in Python and Scheme. https://cs61a.org/
Lisp is a fun language to program in. I learned Lisp after already being familiar with Java / Python and some other languages, which maybe made it even more beautiful.
One of my favourite pieces of code is a Lisp REPL in Lisp:
as-> is great for when you have to mix thread-first and thread-last.
cond-> is great for building maps where some keys might not be needed:
(cond-> {:k1 v1}
v2 (assoc :k2 v2)
...)
some-> and some->> I don't use as often, but when mixing in some java code that could throw an NPE, these will avoid that and just return nil early (they short-circuit when getting a nil value).
I guess I just prefer Ruby for general legibility. The same type of bracket everywhere makes pairing them mentally an error-prone chore. At least for yours truly.
puts and gets are string functions. read and print are code deserializers and serializers.
eval is a function that takes a data structure representing code (as deserialized by read) and computes its value as a data structure, and serializes it.
While you might get the same effect as a user, the mechanics and metacircularity are lost.
If this presentation of Lisp and model of computation clicks with you, you owe it to yourself to read Structures and Interpretations of Computer Programs (SICP).
The Abelson from the OP's lecture is the co-author, and the presentation described exactly follows the structure of the book (including the derivative example).
It opened my eyes to a new way of thinking about coding when I first read it (and worked through the exercises) many years ago.
When I was in high school I had access to three programming tools: Commodore BASIC, 6502 assembly, and Turbo Pascal. I spent years hand coding 6502 assembly because BASIC was clearly for amateurs and I could only get at the Pascal system at school.
At one point I wrote my own Pascal interpreter for the C64. In BASIC, because I wasn't good enough to do it in assembly. It was a little slow.
I desperately wish someone had introduced me to lisp. The entire course of my life would have been different. The most frustrating thing is that I probably could have written a naive lisp interpreter in assembly.
I got a Forth for my C64, and it was the PERFECT language for the machine: fast, compact, structured, extensible. Generally much easier to understand the code than C64 Basic, but you could also easily call into assembly language if you needed a bit more speed.
I was impressed first by the fact that whoever designed this particular Lisp system cared about efficiency.
This isn't a particularly unusual approach. Common Lisp, a contemporary Lisp dialect, was designed with efficiency in mind. The approach of "Lisp = slow + inefficient + spending 80% of time on garbage collection" is a myth.
This comment was regarding TCO, which Common Lisp (unlike Scheme) doesn't guarantee. Many CL compilers today offer it, but IIRC that was rare in the 1980's.
I think it's that you can calculate the value, but you can't (or can't as easily) create a new function that is the derivative of another function, and pass that around.
In a language with higher-order procedures like Lisp or Julia, you can write code that actually returns the derivative as a function, as opposed to differentiate at a given point. This is harder to do (gracefully, anyway) in e.g. C. The beauty of the functional approach is that it corresponds much more closely to how we think about math. See for example Structure & Interpretation of Classical Mechanics (http://mitpress.mit.edu/sites/default/files/titles/content/s...). This is especially powerful when combined with automatic differentiation, as is done in the ScmUtils system that goes along with SICM. (Finite differencing has numerical stability issues when carried too far.)
It'd look slightly different in 1983 but the functionality would all be there. const->#define and maybe you'd need to change the function signatures to K&R style, but that's it.
Well, it was 1983. So seeing something like this in a programming language was more novel than it is now.
Also, up to that point the author has only programmed in a couple of different assembly languages, plus Basic. Coming at it from that perspective, there's plenty of novelty to be found in Lisp family languages.
Context is also helpful. “I’m not sure what’s novel about function composition” is a little bit of a smug take on a language and concept that was pretty cutting edge back in the early 80s. Now we Meat-and-Potatoes Programmers know the composition is useful, but I’d be hard pressed to find first-class composition in 1980 in any popular language that wasn’t {academic, Lisp}.
I don’t really agree that doing calculus refutes the idea that lisp programs can be hard to follow, the syntax is kind of a nightmare imo, but a fun read.
> Hal went on to explain how the substitution model of evaluation worked in this example and I carefully watched. It seemed simple. It couldn't be that simple, though, could it? Something must be missing. I had to find out.
Well, not that simple... as the author hasn't taken the potential problem of free variable capture into account.
For anyone who wants to feel what this might have felt like (and you've yet to watch them), MIT posted videos of Abelson and Sussman presenting the course on YouTube [0]. I felt a similar sense of magic the first time I saw the derivative section.
Scheme was my favorite language in school back in 2006, but the only class I ever used it in was a class where we learned search algorithms and the like (it was either AI or Robotics). I was dismayed to find out there isn't a lot of software written in LISP dialects, because I would love to write LISP code all day, it's so much fun.
All this could be done easily in C (function pointers, factorial for loop etc.). And sampling a function twice at 0.0001 dx's apart and calling it "doing calculus" is a stretch.
The author seems to be making the case that, contrary to his original skepticism, Lisp is indeed some magical higher-order language, but I honestly don't see it. Lisp's ratio of philosophizing to noteworthy projects seems to tend toward infinity.
Seen pretty elaborate loop constructs in large Common Lisp code bases I was working on, and my conclusion is that the only thing that should be banned is lack of willingness to spend 30 minutes at some point learning the loop syntax. Seriously, after you write few loops on your own e.g. collecting over several hash tables in parallel, you won't have much problem anymore.
I still feel current generation of programmers has a learning phobia.
92 comments
[ 4.9 ms ] story [ 130 ms ] thread2. The data is also list, and your program is also a list.
3. Algorithm are basically List Manipulation(Stacks, Queues, Trees, Adjacency lists). So its easy to write complex algorithms in Lisp.
4. Tail call recursion. This part amplifies 3. further.
5. Functional programming features.
6. REPL. I mean like a real REPL.
7. Macros.
1 - 7 helps you to express problems and their solutions with code which represents exactly that. The boiler plate and other assisting code is largely non existent.
Another reason is if a thing has been there around for the longest, more people have thought about it. Both quality and quantity of literature of it are higher than others.
Being older doesn’t necessarily mean either quantity or quality of literature will be greater. MUMPS has been around for much longer than Go, for example.
For example, it is a homoiconic language, so any piece of data can be evaluated as if it is code.
The only way to really understand it is probably to learn it :)
Ironically modern Javascript often replicates the bracket pileup, just with "})" instead. Python does away with it by having "nonindented newline" as an invisible semantic character that closes any number of scopes.
You said:
> ... the single representation carries little structural information and the meaning of a symbol is highly dependent on its containing context.
That makes me wonder if the difference is abstract vs. concrete thinking - those who by nature prefer abstract thinking will find Lisp more natural, and those who prefer concrete thinking will find it clumsy and unsettling.
Choosing the "right" programming language is not just finding the right language for the task (though it is that). It's also finding the right language that fits our minds - and our minds are not identical.
That being a problem is pretty much solved by not writing 1000 line top-level expressions with thirty nesting levels.
People who actually write Lisp are engaging their imagination for the program itself. Thus their imagination is too busy to come up with scary reasons how things could go wrong that would spook them out of continuing.
(I don't know lisp) but sometimes this approach just makes the code really difficult to understand afterwards. Does Lisp have something that makes it easier or better?
[1] Credit where due - I stole that phrase from my friend Michael Pavlinch.
- Structural simplicity. Instead of having a huge mess of syntax to keep track of, it has only a very small number of constructs. Simplicity is beautiful.
Criticizing it on the basis of parenthesis would be roughly equivalent to criticizing someone talking about the beauty of mathematics on the basis of the color of the piece of chalk they're using. It's true that it's criticism about aesthetics, but it's not criticism about the aesthetics that the mathematician was talking about.
Once you've understood in what sense people mean that Lisp is beautiful, you can disagree, but this disagreement will not concern parentheses.
As an aside: I have a similarly shallow aesthetic aversion to JS syntax for the same reason. When I encounter a 12-line ragged cascade of curly brackets, square brackets and parentheses, sprinkled with semicolons, I wonder how that can be considered OK.
Aside 2: with Lisp, you edit programs structurally, meaning that their position is essentially managed for you. This is what people refer to when they mean that "the brackets disappear after a while." You're not focusing on them; they're handled by something else.
In my editor, I have the closing brackets faded nearly into the background, which reflects how concerned I am about their existence.
It's considered OK because the extra syntax carries semantic weight, and the semicolons disambiguate intent for the interpreter (because while semicolons are optional in javascript, leaving them out can lead to errors.)
And as someone just beginning to play around with (Arc) Lisp, I still can appreciate both paradigms. Neither is objectively wrong.
Contrast this with most other languages, where there are many things the language does that you can not do. At least, not easily.
There aren't actually more parens in a Lisp program than a program written in a C-like syntax (which is really an algol-like syntax). They just stand out more for two reasons:
1. There is less punctuation in general, so the parens are more obvious. Instead of f(x, y, z) you write (f x y z). Without the commas, the parens stand out because that's all that is left.
2. There is only one kind of parens in Lisp whereas C-like languages use at least three: (), [], and {}, so that makes any particular kind of paren less prominent.
4. The lisp punctuation style closes all levels on a single line, where most C code guidelines close one level per line. This leads to a "thick" chunk of close parens that students of lisp find harder to read at first.
Well put, Madam/Sir.
I'm still fond of Lisp, but languages like Python, D, Nim (and increasingly, Rust) offer a lot of the Lisp affordances that I really valued -- in particular, easy compile-time metaprogramming. The biggest missing bits are Lisp's deeply integrated REPL-driven programming, and the use of program images -- which have many drawbacks but also some benefits.
IMO, everyone should write and maintain a medium-sized program in a modern Lisp (SBCL, CCL) at least once, just to get an appreciation for how different the programming / debugging experience is. So many well-integrated tools at your disposal, even at runtime.
However, I very well remember reading the Compilerbau book by N. Wirth and the awe I felt. It’s just a few pages but it was a revelation.
They are all available on the ETH website and his homepage (in various revisions). Basically everything you need to know about imperative programming on CS bachelor level for free.
One of my favourite pieces of code is a Lisp REPL in Lisp:
(->) inserts each form's value between the function name & the first argument in the next form.
(->>) inserts each form's value as the last argument in the next form.
The latter forms are much more niche and I haven't found need for them yet.
[0]: https://clojure.org/guides/threading_macros
cond-> is great for building maps where some keys might not be needed:
some-> and some->> I don't use as often, but when mixing in some java code that could throw an NPE, these will avoid that and just return nil early (they short-circuit when getting a nil value).The threading macro is great, but the GP one-liner tells me more about lisp.
Still one is the latter and the other, the former. Hence I think the first tells more of the lisp story, with just a few more parens. ;-)
"Why is it called REPL when the code says LPER? Well..."
puts and gets are string functions. read and print are code deserializers and serializers.
eval is a function that takes a data structure representing code (as deserialized by read) and computes its value as a data structure, and serializes it.
While you might get the same effect as a user, the mechanics and metacircularity are lost.
Though I guess that wouldn't be valid lisp if you tried to express it literally.
The Abelson from the OP's lecture is the co-author, and the presentation described exactly follows the structure of the book (including the derivative example).
It opened my eyes to a new way of thinking about coding when I first read it (and worked through the exercises) many years ago.
I'm self-taught and it's what made me fully "grasp" programming and the power of abstraction.
At one point I wrote my own Pascal interpreter for the C64. In BASIC, because I wasn't good enough to do it in assembly. It was a little slow.
I desperately wish someone had introduced me to lisp. The entire course of my life would have been different. The most frustrating thing is that I probably could have written a naive lisp interpreter in assembly.
This isn't a particularly unusual approach. Common Lisp, a contemporary Lisp dialect, was designed with efficiency in mind. The approach of "Lisp = slow + inefficient + spending 80% of time on garbage collection" is a myth.
(The ANSI standard was 1994. Still nowhere near 20 years).
If the `deriv` function instead returned a function instead of a number, you could apply the `deriv` function twice.
I find that it follows the functional spec quite closely, thanks to Go functions being first class citizens.
- you can't write a C macro which will do this symbolically. We really just want to be calculating 3 * x * x.
- having cube and deriv, you can't write an expression which combines these two, and is itself a function.
BTW, the & is unnecessary in &cube; a primary expression which names a function evaluates to a pointer to that function.
Also, up to that point the author has only programmed in a couple of different assembly languages, plus Basic. Coming at it from that perspective, there's plenty of novelty to be found in Lisp family languages.
Well, not that simple... as the author hasn't taken the potential problem of free variable capture into account.
[0] https://www.youtube.com/watch?v=2Op3QLzMgSY
2017: https://news.ycombinator.com/item?id=14247269 (261 comments)
2013: https://news.ycombinator.com/item?id=5375735 (176 comments)
2009: https://news.ycombinator.com/item?id=504667 (39 comments)
Joking aside, I think lisp is nearly fundamental in the same way that c is.
The author seems to be making the case that, contrary to his original skepticism, Lisp is indeed some magical higher-order language, but I honestly don't see it. Lisp's ratio of philosophizing to noteworthy projects seems to tend toward infinity.
I still feel current generation of programmers has a learning phobia.