92 comments

[ 4.9 ms ] story [ 130 ms ] thread
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.
Lisp is basically writing a program as abstract syntax tree, with facilities to modify the abstract syntax tree.
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 think because it is a powerfull language - and once you learn to look past the parentheses it actually has some pretty cool features.

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 :)

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.

Code is data. So you can write a program to write parts of your program, on the fly
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.

(comment deleted)
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.

(comment deleted)
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.

> the number of parentheses is just mind boggling

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.

Well put, Madam/Sir.

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.

(comment deleted)
I’m too young (or studied in the wrong time or place) so my CS teachers failed to impress me to such an extent.

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.

Can you share the name of the book? Is it "Compiler Construction" by Niklaus Wirth?
Yes - "Compilerbau" from German is literally "building compilers".
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:

    (defun repl()
            (loop (print (eval (read)))))
looks even better with threading in imaginary lisp dialect:

    (-> read eval print loop)
I think clojure has that; I could be wrong, though, I've never used clojure.
Clojure does indeed have -> (thread-first), ->> (thread-last), some->, some->>, cond->, and as->. [0]

(->) 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

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).
Agree to disagree; to write lisp one must know how to read it.

The threading macro is great, but the GP one-liner tells me more about lisp.

Lisp is all about being able to compile the latter to the former in compile-time, if you want it. That's the major problem lisp solves.
Hard agree!

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..."

Why is it called "root mean square" when you square first, then take the mean, and then the root?

    (defmacro -> (&rest args)
      (loop for item in (reverse args)
            for result = (list item) then (list item result)
            finally (return result)))
Python 3 for comparison:

    while True:
        print(eval(input(">>> ")))
Not really:

   ovov@ovov ~> cat repl.py
   while True:
       print(eval(input(">>> ")))
   ovov@ovov ~> python3 repl.py
   >>> x = 1
   Traceback (most recent call last):
     File "repl.py", line 2, in <module>
       print(eval(input(">>> ")))
     File "<string>", line 1
       x = 1
      ^
   SyntaxError: invalid syntax
This is a fundamentally different function that works with strings and not symbolic data structures.
That is cool, but it's pretty similar in Ruby.

    def repl
      loop { puts( eval gets ) }
    end
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.
This is actually a very different piece of code.

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.

plus, it's a gepl, not a repl.
(comment deleted)
I've always thought (probably because I'm not that smart) that REPL really should have been coined as REPR instead.

Though I guess that wouldn't be valid lisp if you tried to express it literally.

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.

There are also SICP video lectures available, from the 80s.
There's also some newer SICP videos online but the 1980s one is the best and still highly relevant to a programmer from todays era (or any era).

I'm self-taught and it's what made me fully "grasp" programming and the power of abstraction.

That book is a miracle of clear presentation. Worth looking at even if you never write a line of Lisp/Scheme in your life.
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 article is set about 20 years prior to common lisp's standardization. It was a reasonable belief then.
It’s set in 1983. “Common LISP The Language” was published in 1984.

(The ANSI standard was 1994. Still nowhere near 20 years).

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.
Isn't the derivate relatively easy in C?

    #include <stdio.h>
  
    const double delta = 1.0e-6;
    double cube(double x) { return x * x * x; }
    double deriv(double (*f)(double), double x) { return (f(x+delta) - f(x)) / delta; }

    int main()
    {
        printf("%f", deriv(&cube, 2));
        return 0;
    }
Am I missing something?
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.
That makes sense, thanks. And as others have mentioned it's therefore not easy to compose higher order derivatives.
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.)
article is talking about 1983. your example relies heavily on Greenspun's 10th rule.
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.
With only the functions you've defined, how do you calculate the second derivative?

If the `deriv` function instead returned a function instead of a number, you could apply the `deriv` function twice.

While you can take the address of cube, you can't do this trick more than one level deep: you can't return a new function from "deriv".
Here's the generic C++ version (https://gcc.godbolt.org/z/mMln6L):

    constexpr double delta = 1.0e-6;
    
    constexpr auto deriv = [] (auto f) { 
        return [=] (double x) { 
            return (f(x+delta) - f(x)) / delta; 
        };
    };

    constexpr auto f¨ = [] (auto f) { return deriv(deriv(f)); }; 

    double res = f¨([] (double x) { return x*x*x; })(2.0);
(comment deleted)
You're missing that:

- 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.

So the author was excited about the concept of function composition? I'm a fan of lisp but I'm not seeing what's particularly novel about lisp here.
It was novel to you when you first saw it, which is what he's describing.
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}.
Not function composition, but higher-order functions.
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.

[0] https://www.youtube.com/watch?v=2Op3QLzMgSY

The classics are worth revisiting on a regular basis.
I don't think the grandparent is complaining about reposts, merely linking to previous discussions for the curious.
Its a matter of time before there is a "bot" that auto-posts this kind response for historical reference.
There used to be. Not sure what happened to it.
(comment deleted)
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.
my question is yes, lisp.. but what is beyond lisp?
more lisp :)

Joking aside, I think lisp is nearly fundamental in the same way that c is.

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.