back
168 comments
> Lisp hackers have been effortlessly reshaping the language for decades using the powerful macro system and extending and bending the language to their will.

I've written a bit of Racket code (https://github.com/evdubs?tab=repositories&q=&type=&language...) and I still haven't written a macro. In only one case did I even think a macro would be useful: merging class member definitions to include both the type and the default value on the same line. It's sort of a shame that Racket, a Scheme with a much larger standard library and many great user-contributed libraries, has to deal with the Scheme/Lisp marketing of "you can build low level tools with macros" when it's more likely that Racket developers won't need to write macros since they're already written and part of the standard library.

> But the success of Parsec has filled Hackage with hundreds of bespoke DSLs for everything. One for parsing, one for XML, one for generating PDFs. Each is completely different, and each demands its own learning curve. Consider parsing XML, mutating it based on some JSON from a web API, and writing it to a PDF.

What a missed opportunity to preach another gospel of Lisp: s-expressions. XML and JSON are forms of data that are likely not native to the programming language you're using (the exception being JSON in JavaScript). What is better than XML or JSON? s-expressions. How do Lisp developers deal with XML and JSON? Convert it to s-expressions. What about defining data? Since you have s-expressions, you aren't limited to XML and JSON and you can instead use sorted maps for your data or use proper dates for your data; you don't need to fit everything into the array, hash, string, and float buckets as you would with JSON.

If you've been hearing about Lisp and you get turned off by all of this "you can build a DSL and use better macros" marketing, Racket has been a much more comfortable environment for a developer used to languages with large standard libraries like Java and C#.

How do Lisp developers deal with XML and JSON? Convert it to s-expressions.

As a common lisp developer, that is only very vaguely true for me.

The mapping I prefer for json<->Lisp is:

  true:  t
  false: nil
  null:  :null
  []     #()
  {}     (make-hash-table :test #'equal)
This falls out of my desire for the mapping to be bijective:

- The only built-in type that is unambiguously a mapping type is hash-tabe.

- nil is the only value that is falsy in CL

- () is the same as nil, so we can't use it as an empty list; vectors are the obvious alternative

- Not really any obvious values left to use for "null" so punt to a keyword.

Sometime back 15 years ago [0], I hit a bit of an existential crisis regarding my career and the kind of work I was doing.

I thought the particular technology I was working in was "part of the problem", as I felt pigeon-holed by .NET and C# to always be a corporate-monkey CRUD consultant. So, I went out in search of something better. Different programming languages. Different environments. Just something that wasn't working for asshole clients who thought it was okay to yell at people about an outage in a hotel on the complete opposite side of the country that was more due to local radio interference than anything I had done in the database code that configured things. Long story involving missing a holiday with my family over something completely outside of my control and yet I still got blamed for it. The problem wasn't the technology, it was the company I was working for, but at that time in my life, I didn't understand the difference.

Racket was a life preserver at that time.

It's really hard to explain, because I never actually ended up working in Racket full-time and I haven't even touched it in probably 10 years. But it still has this impact on my identity as a software developer. I learned Racket. I forced myself out of being a Glub programmer and into someone who saw the strings that underwrote The Universe. The beauty of S-Expressions and syntactic forms and code-is-data and all that. It had a permanent impact on my view of what this job could be.

I still work primarily in .NET. Most of the things that were technological issues about .NET Framework got absolved by what was first .NET Core and what is now .NET. So, I no longer feel like my tools are holding me back. And I'll forever be thankful to Racket (and the community! The Racket listserve was amazing back then. Probably still is, I just don't interact with it anymore) for being there for me.

Edit: Haskell was in fact another language I explored at that time, in addition to Ocaml and Ruby and Python (ugh! Don't get me started on Python!) and many other things. They were all "cool" in their own way, but nothing felt like Racket. They all had their own weird rules that felt like being bossed at again. Racket felt like art. Racket felt like it was there for me, not the other way around.

[0] I still think of this time as the "mid-point" in my career, but it's now been long enough ago that I've been more past the crisis than I was ever in it. Strange feelings.

For what it's worth, anytime I have written a macro it's usually not because it's needed, but just because I think it'll be fun :)
> Of course, to be completely fair about my toolkit, standard Scheme can sometimes lack the heavyweight, “batteries-included” ecosystem required for massive enterprise production compared to the JVM.

I was thinking the whole time, "this person would _love_ Clojure".

Kawa is a Scheme which runs on the JVM and is pretty great.

https://www.gnu.org/software/kawa/index.html

I am one of these people who cannot countenance a Lisp that doesn't have `syntax-case`.

as a part time schemer, I also love Clojure and reach for it more often than Scheme these days.
> You can pause, inspect objects, change values, and even redefine a broken function on the fly to test a fix in any environment (yes even in production, while running).

I see this mentioned often, and it sounds amazingly useful (especially the part about fixing in production!). But how truly widespread is it among the Lisp dialects to be able to connect to a running program, debug, and hotfix it? I understand Common Lisp has it, but I struggled to figure out how to do it in, say, Racket. Admittedly I'm am relatively inexperienced Lisp programmer, so maybe I wasn't looking in the right place or for the right words. Which Lisp dialects do indeed support the extreme version of this capability to inspect and edit running programs?

It’s trivially easy to do in Clojure (literally one line of code to start an nREPL server, after deps/requires), and often very useful in dev and personal, local projects. In practice, I’ve never once used it in a user-facing production system, in 16 years of writing Clojure.

Out of the box, there’s zero security or audit trail. Building that properly isn’t trivial and, even with it in place, many corporate infosec teams would have fits if you suggested that engineers can make arbitrary inspections/modifications to a running production system.

Where it could be appropriate, often you’re running the code in autoscaling containers or something similar. Modifying one instance then is rarely anything but a terrible idea.

Where I have used it is for things like long-running internal batch systems that run a single instance and never touch any sensitive data. Connecting a REPL in those cases is much more flexible and powerful than, say, building a dashboard UI or a control API over http, and you get it for free.

Yes but I don’t know how someone familiar with a Jetbrains IDE can claim that only Lisp has that feature. I love Common Lisp and SLIME, but most of what it can do, I can also do in Java with the IDE. Change a method definition while it’s running and then restart the method? No problem. Run any code within the context of the running method? Yes, Java can do it. Change local variables values in the middle of a method? Easy!

The Lisp REPLcis still superior because it comes with more stuff, like DECOMPILE, INSPECT and so on that can only exist because the language is essentially a compiler even at runtime, which can also be a problem for sensitive domains… but in Java you can do all those things using the IDE so the distance between what is possible in Lisp and a language with good IDE support like Java and Kotlin is now negligible in my opinion.

it's been my experience that when most people say "Lisp does this that or the other", what they usually mean is "Common Lisp does this that or the other". Often there's an implicit "with SLIME" in there as well
Python is not Lisp, but jumping into a Python REPL in a halfway-run program and poking at the internals easily is _very_ useful as a debugging tool, quickly getting you answers on some messier programs.

It's a shame that other scripting languages that theoretically have the capabilities to do this don't do this (looking at you, node! Chrome dev tools are fine but way too futzy compared to `import pdb; pdb.set_trace()` and "just" using stdin)

I do also use Emacs, and with Emacs Lisp `trace-function` means you can very quickly get call traces in your running instance without having to pull out a debugger and the like. Not like you can't trace functions with `gdb` of course. But the lowered barrier to entry and the ability to do in-process debugging dynamically means you just have access to richer debugging tools from the outset.

People do it in Clojure all the time in the dev setup. And you technically can do in your customer environments too, but it's of course a bit of a cowboy thing to do there.
Not Lisp, but for those interested in editing programs that are running in production:

I read some Erlang article saying that hot swapping is not actually very useful in production because of some reasons, and instead a blue-green deployment is preferred. Can't find the link atm. This was close: https://learnyousomeerlang.com/relups

Compare to this comment: https://news.ycombinator.com/item?id=42405168 Hot swaps for small patches and bugfixes, and hard restarts for changing data structures and supervisor tree.

That sort of hotfix workflow isn't really a thing in Racket or Scheme in general. Changing the definition of a function doesn't update everything else that calls that function like it does in CL.

Maybe emacs lisp works that way?

I use it a lot for my one man projects; it is really fantastic in that setting. I use SBCL exclusively; it is very fast and robust and has image based development. I have my own versioning toolkit so I don't go insane.

It is obvious why it is not really used or recommended as it really falls flat in a team setting, mostly even when 2 people are involved. But fixing bugs live as they happen and then spitting out a new .exe for clients is still a lot faster than modern alternatives. Far more dangerous too.

Have had to do this live in a production MtG card management application. It worked well. The owner kept their MtG card money. Lisp saved the day.
I also see this mentioned often and have wondered the same. I can sort of envision this working in a single threaded application, but how would this work in a web application for example? If a problematic function needs to be debugged, can you pick what thread you're debugging? If not, do all incoming requests get blocked while you debug and step through stack frames?
A common workflow is to run code to test some function in the REPL and then promote it to a test when you are ready, and this process has been the smoothest in lisps, especially since you can create your own test harness if you need to.

Fun fact is that giving AI repls also reduce error rates so much that you can save up to half the tokens/time or more.

It’s common in Clojure as well as other Lisps. I was just doing that exact thing, modifying a running program in production, earlier this week, adding in print calls to gather debugging information and then modifying the code to fix the bug and it immediately going live and the correct behavior verified.
The nREPL is present even in newer dialects. It is as easy as installing Calva vscode extension for Clojure, or jacking in with Cider. This makes it perfect for LLM interaction as well.
> Actually, in my opinion, Scheme (and Lisp) allows you to express complex systems and problem domains in more simple terms than any other language can.

Short article. Worth reading. But all I swallowed was this one sentence.

Its the sytax. If you like semicolons, thats why you like Pascal-like languages.

For all practical purposes, the syntax of Lisp isn't just a cosmetic choice, though.
it's not just the syntax. the entire language, and even the ecosystem in general, has relatively few atoms that can be combined with a higher degree of freedom than the alternatives.

it has both upsides and downsides. the upsides mostly win for me.

I think I much prefer Haskell DSLs over Lisp macros as the basis for APIs in foreign code. That might be due to my relative inexperience with Lisps, but macros just seem to make all the bad aspects of dynamically typed langues much worse. Looking at some piece of code in isolation, not only is it often impossible to tell what is the type/shape of data that are coming in (as is common with dynamic type systems) but with macros added to the mix I also can't tell what the control flow is. So to understand what a single piece of code is doing, I find myself chasing for hints that are scattered throughout the entire codebase.

Contrast this to Haskell's use of DSLs – although they really can be quite dense sometimes, I feel like, when I get stuck, I can always just dig into the documentation on Hackage, and figure out things from the type definitions (even when explanations in docs are lacking). Though it does require being comfortable with the abstractions being used (monads and such). Rust is similar in this manner but to a lesser extent.

But again, maybe the macro critique stems from my inexperience with them.

I wrote a couple macros that record data transiting through code at runtime (it's in Clojure, so basically almost every function is pure, returning what they produce as if it was water flowing out of a faucet), stores these intermediary results in a file, and finally display these values in the code itself, as comments, just below the call-site that produced them.

You can then, for a given call-site, choose to "load" these recorded computations, which will change the displayed comments, both below this call site and all the other instrumented call-sites that are downstream to it, even for code sitting in other source files.

It's a bit fragile and needs more polishing but it's a lot more convenient than any type system that will always get in the way, be not powerful enough, and it allows me to see what kind of data flows in my program without running it. Because I record everything and display the result not at compile-time but at coding time, in the same window, alongside the rest of the code. I don't understand why this was never done (to the best of my knowledge). Biggest limit I encounter is that Clojure doesn't provide any mean to identify areas in my code that are not pure.

(In Haskell) > just adding a simple print somewhere is not going to work without refactor

Interesting. How do people cope with this in practice? Does it mean you can't really use log() -statements for debugging?

You just add `trace`. It's not hard.

https://wiki.haskell.org/Debugging

You could wrap it in an unsafeIO function to make it return `()` again.

However, I’ve had very little use of printing for debugging. In Haskell you write small (ish) and pure functions that you can test extensively with property based testing. The types already help a lot as well.

So basically the only place where you deal with unexpected input is at the communication boundaries of the app where you are in some form of IO already and printing just available.

I learned Scheme before Haskell and as much as I enjoyed the experience, I still wouldn't reach for Haskell first. It's pretty much limited to my xmonad configuration.
I have written a very large codebase in Scheme (gambit) and in the end I really, really, wanted a type system to catch bugs.
> Actually, in my opinion, Scheme (and Lisp) allows you to express complex systems and problem domains in more simple terms than any other language can.

I disagree with this. Lisps are procedural languages like most other programming languages. They can describe procedures in a relatively simple manner (big asterisk on that one, the simplicity of Lisp as a language is greatly overstated and conflated with the simplicity of s-expressions), but procedures aren't really good at expressing complex systems or problem domains. In my experience, relational languages like Prolog are vastly better at this. Lisps are much easier for the average programmer to pick up and write reasonably performant code with, though.

Lisps are expression based languages, but not pure. It's easy to mistake it as "like most other languages", but it's not quite the same - everything is an expression and returns a result. There are no "statements".

They appear procedural because of syntax sugar - ie, the body of a function is basically implicitly wrapped in (progn ...), (begin ...), ($sequence ...), etc - which are all equivalent expression forms which evaluate their sub-expressions in order and return the result of the last one.

   (progn a b c)      ;; CommonLisp
   (begin a b c)      ;; Scheme
   ($sequence a b c)  ;; Kernel

   ;; evaluate a, then b, then c, 
   ;; ignore the results of evaluating a and b 
   ;; return the result of evaluating c.
So when you see:

   (define (foo)
       (expr1)
       (expr2)
       (expr3))
If we desugar, it would be

    (define foo (lambda () (begin (expr1) (expr3) (expr3))))
We get behavior that looks just like other procedural languages (without a "return" keyword) - but everything is still an expression.

A similarity is the comma operator in C. Imagine you didn't write statements but the body of your C functions was entirely chains of comma operators.

CommonLisp has a couple of other useful related forms - prog1 and prog2. They still evaluate their sub-expressions in order, but prog1 returns the result of evaluating the first expression, and prog2 returns the result of the second expression.

    (define (foo) (prog1 (expr1) (expr2) (expr3))
    (foo)
 
    ;; evaluates expr1, then expr2, then expr3
    ;; returns the result of evaluating expr1
    ;; ignores the results of evaluating expr2 and expr3
I think it all depends on the shape of the problem. I love Prolog for its expressive power, sometimes. Complex simulation problems are really nice to model in OCaml or CLOS, and then again, maybe remodelling in Prolog brings some insights. And often writing a recursive function in Lisp is all you need to understand a complex system. It's all layers. An outer shell to prolog would be a theorem solver for example, because Prolog is a very rudimentary one.
Prolog is great for the cases where logic programming makes sense but you can easily create a logic engine in Lisp (or other languages - that's what kanren/minikanren which has been ported to many languages)
Seems a bit similar to 'Why I prefer Scheme to Haskell' (<https://news.ycombinator.com/item?id=3816385>, 2012). Seems a bit plagiarized, but that may just be a coincidence.
It is odd. Especially since the author of the article states they’ve been working with Haskell for years, although they only have a few years of experience and only have a single Haskell repository on GitHub with some simple leetcode problems.

The number of both nearly and exactly identical phrasing makes it feel like someone told an LLM to slightly rephrase that older article.

I didn’t even know the internet was sick.

If you know lisp, just reach for Coalton instead of Haskell
Picking a language is a matter of selecting the best fit given the constraints of the project.

For stuff I like to work on on my own time, "how I like to work" is a major forcing constraint. So it's no surprise that I have a large number of Lisp projects sitting around. Maybe it's because I'm auDHD, but the ability to evolve a program through active dialogue with the machine (and not of the sloppotron variety) just fits better with how I think through a problem and its solution.

Because they're elegant. Haskell is a conceptual and syntax mess.
Irrespective of the language, I love the REPL. For this reason, among others, I just cannot get into Agentic Coding. It seems like a step back to batch processing.
I don't believe monads are a "heavy handed abstraction" and that's what prevents people from prototyping in Haskell.

What really prevents people from writing in Haskell at a reasonable speed is the poor language design. Programming languages are supposed to aid in reading by emphasizing structure. It's important to emphasize that a particular group of "words" constitutes a function call, or a variable definition, or a type definition -- whatever the language has to offer.

Haskell is a word salad. Every line you read, you have to read multiple times, every time trying to guess the structure from the disconnected acronyms. It belongs to the "buffalo buffalo buffalo buffalo" gimmick family. This is a huge roadblock on the way to prototyping as well as any other activity that implies the ability to read code quickly. And then it's also spiced by the most bizarre indentation rules invented by men.

This is not at all a problem with eg. SML or Erlang, even though they are roughly in the same category of languages.

Haskell would've been a much better language if it made its syntax more systematic and disallowed syntactical extensions s.a. introduction of user-invented infix operators, overloading of literals (heaven, why???) and requiring parenthesis around function arguments both for definition and for application. The execution model is great, the typesystem is great... but the surface, the front door to all these nice things the language has is just some amateur level nonsense.

* * *

As for the upsides of using languages from the Lisp family for practical problems... I don't find (syntax-rules ...) all that exciting. I understand this was an attempt to constrain the freedom given by Common Lisp macros, and I don't think it worked. I think it's clumsy and annoying to deal with. The very first time I tried to use it, I ran into its limitations, and that felt completely unjustified. To prototype, you want freedom of movement, not some pedantry that will stand in your way and demand you work around it somehow.

The absolute selling point, however, is SWANK. Instead of editing the source code, you are editing the program itself, that can be interacted with in points of your choosing. I don't know of any modern language that offers this kind of experience. I think, even still in the 80s, this approach to programmers interacting with computers was common. At school, we had terminals with some variety of Basic, and it worked just like that: you type the program and it instantly shows the effect of your changes. Then, there was also Forth, which also worked in a similar way: it felt like you are "talking" to the computer in a very organized and structured way, but real-time.

Most mainstream languages today sprouted from the idea of batch jobs, where the programmer isn't at the keyboard when the program runs. They came with the need to anticipate and protect the programmer from every minor mistake they might've easily detected and fixed during an interactive session far, far in advance.

Whenever I think about writing in C, or Rust, or Haskell, I imagine being tasked with going to the grocery blindfolded: I'd need to memorize the number of steps, the turns, predict the traffic, have canned strategies for what to do when potatoes go on sale... I deeply regret that programming evolved using this evolution path, and our idea of what it means to program is, mostly, the skill of guessing the impossible to predict future, instead of learning to react to the events as they unfold.

> Remember that later, just adding a simple print somewhere is not going to work without refactor (welcome to the IO monad).

This hits home for me. Print statements are essential to the way I code. I use them to debug, to examine variables and parameters, to trace execution flow. I rarely use debuggers.

Haskell is godsend when using LLMs though.
I tried some ML language once, it's difficult even to write a basic factorial example, which in Scheme I could do it iteratively and recursively with ease.

Either with S9 Scheme for quick fun (it has Unix sockets and ncurses :D ) or Chicken Scheme for completeneless (R5RS/R7RS-small + modules), I always have fun with both.

Oh, and well, Forth, too, but more like a puzzle (altough it shines to teach you that you can do a lot with a fixed point). Hint: write helpers for rationals -a/b where a is an integer and b a non-zero integer- and complex numbers by placing two items in the stack for each case (for rat helpers you need four (a/b [+-*/] c/d) .

You can have a look at qcomplex.tcl (either online or installed) as an example on how can it work even under JimTCL itself by just sourcing that file. Magic, complex numbers under jimsh thanks to the algebraic properties. So, you can implement the same for yourself in some Forths, even under EForth for Muxleq. Useless? It depends, under an ESP32 it can be damn fast, faster than Micropython.