Ask HN: How do you “get” Lisp?

18 points by khaledh ↗ HN
Over the years I've read countless articles and comments praising Lisp. Yet, there's something that keeps pushing me away from it every time I try to give it a chance. Maybe it's the nested prefix syntax, all those parentheses, something else?

I read most of SICP, and believe me I like the concepts the book puts forward. But I still don't get Lisp. I understand some of the merits: code as data, functional, minimal uniform syntax (s-expressions), macros (meta-programming), etc. But I fail to _appreciate_ those features in the same way I appreciate features in other languages. For example, I can instantly appreciate Elixir's pattern matching capability because it makes control flow fades into the background (in addition to simultaneously destructuring variables), removing a lot of noise; I can appreciate its pipe operator because it makes data flow fades into the background as well, letting me focus on logic composition.

What's the best way to develop an appreciation of Lisp, given that I learn best through examples that clearly demonstrate somethings's value, not by reading a laundry list of features?

20 comments

[ 2.7 ms ] story [ 64.2 ms ] thread
I would recommend implementing a pattern-matching capability in Lisp. With Lisp you don't need to wait for a language designer to invent a new language with better pattern matching; you can add pattern matching to the language any time you want it.
Those things you like in Elixir? Make them in Lisp. Think of Lisp as the language you use to build the language you need to implement your solution. See Paul Graham's essay Programming Bottom-Up (http://www.paulgraham.com/progbot.html).
But the thing is, I'm willing to take an opinionated language that gets most things right over a "god language" that is malleable enough to bend to your will. I'm pretty sure others will disagree, but I don't feel like writing my own language using Lisp.
But no language, no matter however opinionated, does get most things right, does it? People complain about C, C++, Java, Go, you name it.

I know many people are averse to the idea of macros that makes Lisp so malleable (you can add syntactic abstractions like in-fix expressions etc.) but the same people are not averse to adding new types with classes or new "operators" with functions. I find it curious that many are comfortable at stopping the level of abstraction at just that (types and operations). For some reason, the idea of abstracting away syntax itself or dynamic function definitions (macros) is unpalatable. Why should the comfort for abstraction stop just there? Just like one can make a mess with types and functions, one can make a mess with macros. Just like one can write beautiful and elegant types and functions, one can write beautiful and elegant macros. Does the aversion to macros come due to the traumatic experience people relate to when they think of C/C++ macros?

Lisp/Scheme have almost the same number of parenthesis+curly brackets than C. The difference is than in C the closing curly bracket has it's own line, but in Lisp/Scheme they are put all together. (If you hate parenthesis, try Python.)

The main advantage of Lisp/Scheme are the macros. The s-expressions are good because they make writing macros much easier. You don't have to mentally translate from the real language to the internal representation while writing macros.

I made a quick search and Elixir has macros. Do you write macros in Elixir?

The short answer is that Lisp/Scheme had real macros before it was cool.

> The main advantage of Lisp/Scheme are the macros.

Where can I find a practical example/tutorial/guide that demonstrates this in the context of building an actual application? I'm sincerely curious.

> I made a quick search and Elixir has macros. Do you write macros in Elixir?

Not much. It's something not encouraged by the language designer and community (since it can result in a lot of "magic" that is hard to understand), and I rarely found myself needing them.

The idea is that some repeated patterns can be encoded in a functions, but other repeated patters need more abstraction. Try to find some repeated pattern.

Some real example from a internal site for 500 users in the university:

The idea is that I have a parameter with the data of the current user, a parameter is like a global variable but each tread has it's own copy so it's "thread safe". Sometimes I want to iterate over all the users inside one of the administrations form. So I have to write in racket something like:

  (for ([u (in-list all-users)])
    (parameterize ([current-user u])
      (do-something-interesting-here)
    )
  )
(The last three parenthesis should be in the same line, but if you like c-like formatting, let's put them in their own line.)

I had to repeat this pattern a few times. It is boring and error prone. Perhaps I have to add a lock for the user profile. Perhaps I need an special case if the user in the iteration is the administrator. But let's keep the example simple.

Now I can use a macro to define the pattern:

  (define-syntax-rule (for/users body ...)
    (for ([u (in-list all-users)])
      (parameterize ([current-user u])
        body ...
      )
    )
  )

This is the simplest way to define a macro in scheme and racket. It has no error checks. It has no room for variants. There are more advanced methods to write good macros. One important point is that there is some magic to ensure that the variable `u` used inside the macro don't mess with a variable `u` defined outside the macro.

The nice part is that the macro looks almost like the code I want to repeat. I only need to use `body ...` in the definition in the first line and then replace the interesting part with `body ...`. Here is the magic! In the easy case, the macros look almost like normal code!

Now I can use it as any normal thing in the language, for example

  (for/users
    (writeln (user->name (current-user))
  )

  (define count 0)
  (define sum 0)
  (for/users
    (set! count (+ count 1))
    (set! sum (+ sum (user->age (current-user))))
  )
  (define average (/ sum count))
  (writeln average)
I hope the examples are self descriptive.

The idea inside the lispy word is that users should write macros when it is necessary to write macros. Try to use them sparsely. Try to make them blend with the language. Try to make them intuitive to use.

A bad example of a macro is:

  (define-syntax-rule (show x y)
    (writeln (list y x y))
  )
It's a bad macro because it looks like a normal function but the expressions are evaluated out of order and one of them is evaluated twice. So

  (define n 7)
  (show n (set! n (+ n 1))
  (writeln n)
will show

  (#void 8 #void)
  9
but the user probably expect

  (7 #void)
  8
So write macros, but write them sparsely and wisely.

My recommendation is to enjoy Elixir, try to find repeated patters, try to enclose the patters in functions. When that fails try to write good macros. When you understand why you want more macros and more macros try racket (or scheme/lisp/clojure/...).

One of the most striking things about Lisp for me is how concise it is. The excellent Lisp Tutor Jr [0] will give you several interactive examples that make you think in terms of shorter and simpler functions, which are the heart of Lisp programming. As PG has said, if you find that your function is more than a handful of lines long, you're probably not doing it right. The tutor app is a good training ground for thinking in Lisp this way and it might be the breakthrough you need...

[0] https://alarm.cti.depaul.edu/lisptutor/login

What worked for me was writing business logic for a customer-facing app. The amount of unavoidable boilerplate made me understand what Paul Graham had been on about.
The ability to abstract everything in the language, is what I got from learning Lisp. You can just keep abstracting and abstracting until you're working on an entirely different kind of plane where you're now measuring programs themselves in their entirety and assigning a type to their 'behavior'. Anyway, that's what I got from it.

As for pattern matching, try something like this https://cs.uwaterloo.ca/~plragde/flaneries/FDS/index.html you'll see how patterns you find by yourself can just be specified and matched, making it much easier to take apart things like suffix trees.

The answer for how to find out whether Lisp has practical benefits for you rather than only theoretical benefits is to... force yourself to write some practical projects (non-toy)!

[...and additionally maybe expend the effort to absorb a sizeable practical codebase that others have written]

Maybe the answer to the question will ultimately be "It's not for me", but an experiential approach is the only way to reach an answer that is satisfying.

This is generally a good way to learn things, as I learn best by doing. But, when approaching a new language or framework, I need some practical guidance on building applications using that language/framework.

Ideally I'm looking for the equivalent of Michael Hartl's Ruby on Rails Tutorial. I'm not a ruby/rails developer, but I did follow his tutorial and it was one of the best guides I've followed (pedagogically speaking).

The first step to learning LISP the right way is to understand what LISP stands for: Lets Insert Some Parentheses :-)

I would be curios if there are a set of guided lessons in Lisp with guided projects. I would like to revisit this language as it is truly an elegant language.

> I would be curios if there are a set of guided lessons in Lisp with guided projects.

That is exactly what I'm looking for. Practical guidance for building real world applications. If the answer is "Lisp is not for business applications", then fine, at least I have my answer.

The greatest feature of Lisp is that everything is an expression. Almost everything else is secondary as it’s already present in other modern languages.

If you ever encountered a syntactic limitation in any language, you could go to Lisp and work your way around it either with macros or even simple control flow.

I would recommend to skim through Peter Norvig’s PAIP to get a taste of it. Then, as another user suggested, writing your own pattern matching library would be a great exercise.

To me, SICP is not really a book about Lisp. Scheme makes the underlying concepts easier to explain. Pedagogical power was one of the motivations for developing Scheme.

The Lisp that's more like Erlang(Elixir). The one that was developed for commercial applications programming is Common Lisp. For me, the book that shows the power of Lisp for applications programming is Norvig's Paradigms of Artificial Intelligence: Case Studies in Common Lisp https://github.com/norvig/paip-lisp

Common Lisp is the result of consensus among industry stakeholders. There is meaningful consistency but no commitment to purity or theory or dogma. More about what people are (were) doing than what "people should do".

YMMV.

Thanks for the tip about that book. I'm collecting a number of resources from the answers here and I'll try to go through them. Right now Paul Graham's "On Lisp" seems promising.
It's been a few years since I read On Lisp. I kind of walked away with the impression that the main characters are macros and the star is anaphoric macros. There are nuggets in the buildup to their introduction and anaphoric macros are interesting. But my brain doesn't wrap very far around macros and my sense of understanding dissipated pretty quickly each time I stopped reading. For me, Graham's On Lisp was fun to read but less applicable than his ANSI Common Lisp. On Lisp was a rabbit hole for me. A fun rabbit hole. But still a rabbit hole.

---

Changing gears back to your original question, Racket has pattern matching. [1] [2]

[1]: Racket Guide: https://docs.racket-lang.org/guide/match.html

[2]: Racket Reference: https://docs.racket-lang.org/reference/match.html

Initially I avoided macros and simply learnt to write "flow-oriented" small programs in Scheme. It took a while to get used to not using many intermediate variables and simply feeding the output of one function into the next. Once I started seeing patterns I found myself writing small general purpose functions from which I could assemble larger more complex functions.

BTW it is important realise that Common Lisp and Scheme have significant differences. Personally I prefer gnuScheme. I hear that many business applications prefer to use SBCL.

> For example, I can instantly appreciate Elixir's pattern matching capability because it makes control flow fades into the background (in addition to simultaneously destructuring variables), removing a lot of noise ...

But where does that stuff come from? Structural pattern matching appeared in Lisp.

See papers like:

Format-directed List Processing in LISP [1966]

https://apps.dtic.mil/dtic/tr/fulltext/u2/633242.pdf

METEOR: A LISP Interpreter for String Transformations [1963]

https://dspace.mit.edu/handle/1721.1/6106

I'm pretty sure that the word "destructuring" comes from the Lisp culture. Fifteen, twenty years ago, if someone used "destructuring", it had to be a Lisp programmer.