A road to Lisp: Why Lisp
scotto.me
scotto.me
The Light Side is about preventing the programmer from making mistakes: Get rid of go-tos! Add static types! Do not allow a bug to be expressible. The Dark Side is about giving power to the programmer: Macros? Obviously. Operator overloading? Self-modifying code? Multi-line reg-exps? Go to town!
The Light Side knows programmers are flawed and imposes constraints. The Dark Side trusts programmers with power. Neither side is correct all of the time, and a good programmer learns both.
Lisp is interesting in that it is clearly Dark Side programming (the programmer can do anything) but it's still admired by Light Side programmers. Maybe there's something about the simplicity of the language that makes it seem platonic--almost incorruptible. Or maybe Lisp is so pure that it embodies both Light Side and Dark Side, like a god that spawned the programming universe.
It's much better to give all the power to the programmer, to allow him to fix his mistakes rather than fantasising about preventing them.
This isn't so far off, considering that some people consider the "Lisp in Lisp" bit from the 1.5 manual to be the "Maxwell's Equations in Software":
https://michaelnielsen.org/ddi/lisp-as-the-maxwells-equation...
http://www.lucacardelli.name/Papers/TypefulProg.pdf
Page 52 perhaps.
S-expressions are a generic tree, that's not a language, that's just syntax.
Is it CL, Clojure, scheme?
Like these don't even share a basic object model, it's like getting the AST of Haskell and C and then talking about how good that AST is.
Relevant PG: https://paulgraham.com/langdes.html
And you have a good point. Maybe it's a weakness in the metaphor if one can see it either way.
I fundamentally believe that there is no perfect language, instead one merely chooses a set of trade-offs. What's interesting about Lisp is that it is simultaneously well-respected and yet not widely used in productions (compared to C, C++, Python, JavaScript, and even Rust).
It's almost like Lisp chose a particularly extreme set of trade-offs that yield great power but also repel large swathes of programmers. I think PG might say that it repels dumb programmers, but maybe not.
I've always thought it's a misanthropic philosophy and sucked much joy out of programming but also there's something to be said that there's safety in the expressiveness of Lisp. I read an article a while ago that found a robust correlation simply between length of a code base and errors regardless of languages used. And given how succinct and clear Lisp codebases can be that's valuable in itself.
we're kind of at the logical endpoint of this now with gigantic slop codebases that nobody understands just held together by 20 different tools, and if you ask me if I had to pick one of those or something one tenth the size written by a guy or girl who has been writing Lisp for ten years I'd say thank you I'll go with #2
If there's one thing I've been happy with learning Common Lisp and Clojure is that you only want libraries for things like algorithms, data formats, and protocols/interfaces. Mostly because those often have standards. Anything else should be a snippet to copypaste even when presented as a full program.
Even without static types, and even allowing macro craziness, there's just such a stronger baseline of declarative and functional thinking, you're off to such a good start in clearer thinking and reasoning about a program.
But again, I suppose all the Lisp forms return values, and quibbling about the declarative : imperative :: expressions : statements mapping is just petty semantics
I would argue that it's very much not just semantics, or at the very least it's certainly not petty. The distinction has a noticeable effect on what the experience of writing code and on the reliability and related properties of the written code.
There are prologs, constraint solvers, ffis, JavaScript alternatives, garbage collected and not garbage collected languages that call themselves lisps.
Almost by definition that implies that it makes some trade-offs that turn off lots of programmers. Still more popular than Lisp, though.
People learn JavaScript or TypeScript because they want to write web apps. Swift or Objective C to write iOS or Mac apps. SQL because there’s a database they need to get data out of. Python because there’s a machine learning library they need to use. Etc etc.
Language design is far down the list of priorities.
People go where the money is, hence python. But python is widespread because there is employment in it. That adoption by employers follows a network effect, but it originally came about through the language design.
Early-adopter => mainstream => long tail
Adoption hinges on that jump from evangelists to the mass market, and that implies:
- attracts early adopters because of language design - early adopters can pitch the language to their bosses by showing cleat benefits, also because of language design
The most obvious exception is javascript, which was inflicted on the world by browsers. Even so, it had to be sufficiently shit for the php crowd to feel at home. (Unsure which came first, but you get my point.)
Php is exactly popular due to design. It was an exact fit for the myspace dotcom era, and there were no json APIs to illuminate its shonkiness. (I am still angry that [ ] serialises to either [ ] or { } depending on a fucking global variable. I cannot think, being maximally charitable, of any value that comes from conflating arrays and hashmaps. No, i do not want an array containing the keys 0 and "0". I can count on the fingers of one foot the times i have wanted that.)
Sorry - my therapist encourages me to rant about php. She said it's part of healing. I haven't even told her about back-end php yet.
Now, the thing that determines popularity is popularity. So Python still wins, even though its beginner friendliness isn't so important anymore.
Clearly there's a trade off or else everyone would just use Haskell.
For one, there might be trade-offs against dimensions other than safety and expressiveness. Performance, corporate support, libraries, popularity, learning curves, similarity to other languages...
For two, popularity is simply not strong correlated to quality. Popularity is a social function driven by, well, social factors. It's heavily path-dependent and noisy. There is absolutely no guarantee, not even close, that the "best" thing in any sense will be the most popular, or popular at all.
* Of course there are no user-visible pointers** in Lisp so the concept of free vs. not free doesn't come up because memory allocation and garbage collection are automatic.
**Under the hood most everything in Lisp is a pointer, but the user cannot see them or mess with them.
Highly reliable systems are written in Erlang, which if you squint is another Lisp dialect. There's even a sexp-based version called LFE, for Lisp-flavored Erlang. Erlang's key to reliability is error recovery, rather than exceptional levels of error prevention.
I do like your light side/dark side classification.
Define a goddamn language, syntax is not enough to define one! What are the semantics? Without that you are just talking about syntax trees like they would mean anything
Yes, that is true. I'm not big on the idea that Lisp is defined by parentheses. The implementation strategies are another way to look at it. That doesn't capture it either, but it's an angle to try.
Indeed there was a proposal to add Algol like syntax to Lisp,
JS is a dynamically typed language with prototypical inheritance objects that work like universal key-value maps for the most part. It is also mutable.
Clojure is a dynamically typed language with key-value maps. It is also immutable.
You can surely see where I'm going , the underlying semantic model is the meaningful part. Homoiconicity doesn't give you anything special if your language can parse itself and can eval code. It just makes these completely abstract implementations simpler.
Well, in a way it does give you something: By making expression of things like macros simpler, it makes them sometimes worthwhile, and makes it a reasonable request to have this kind of meta programming in your language at all. Without homoiconicity such things become even more difficult endeavors and often unjustifiable for the language design and its implementation.
But let's compare it to a modern macro system like rust's or scala's, where you get a typed object representation of the AST, and for anything non-trivial you are better off with this latter.
Also, arguably the best is to have certain features in the language itself, that can be used to build proper abstractions - so you don't have to resolve to using macros in its place.
I don't agree, because that would mean, that the language must be huge, or grow huge over time, or alternatively be extremely abstract at its core, to be able to fit every use-case. Furthermore, so far I have not seen a language, in which the language designers managed to pull it off, so I tend to think that what already has been successfully pulled off, which is macros for language extensions, is the way to go.
Also this seems to be arguing from a limiting idea about what macros do. Macros are not always the right solution for any problem, in fact often they are not, but there are things you simply cannot do otherwise (without syntactic clutter), like for example changing the order of evaluation.
I also don't agree with your point about only making toy examples easier to write. For example I have written macros for implementing new define forms, which allow to specify contracts for function arguments and return values, or a macro for automatically defining functions that communicate to API routes, based on the function name, which I used to implement a proof of concept docker client for Scheme. Those are not toy examples, but real world applications, where a small macro can have big effect.
If you think macros need to be big and elaborate and complicated and otherwise are toys, then you don't really understand the power of macros. One of my favorite macros is the following threading macro:
(define-syntax ->
(syntax-rules ()
[(-> expr) expr]
[(-> expr* ... (op args* ...))
(op args* ... (-> expr* ...))]
[(-> expr* ... op)
(op (-> expr* ...))]))
Small, but a great addition to the code, that improves readability in many places of the code. It doesn't have to be big or long, in order to not be a toy example, but actually be a useful macro.And my other point was macro systems in other languages, which I believe are better, in part due to not having homoiconicity, like Scala or rust.
Prolog disagrees. IIRC the first versions of Erlang were written in Prolog, and you can still see its influence in the syntax.
Erlang's computation model is functional, like Lisp, unlike Prolog.
I'm with you, on the dark side I've seen people slowly turn into crabs, screeching about memory safety and static types. Can you imagine that? Now stick those wings on me, I'm going to fly close to the sun.
Because of this, any decent programming language must include GOTOs, but in many modern languages they are not named GOTOs, because of the bad reputation of the word.
For instance the language Scheme does not have GOTOs, but it has mandatory tail call optimization, which means that a "tail call" is just an alternative name for "GOTO". For example, this allows the writing of a state machine in Scheme, exactly like it would be written in a language with GOTO, but using tail calls instead of GOTOs.
Other languages have labelled loops and they allow exit a.k.a. break with a label and next a.k.a. cycle a.k.a. continue with a label. Such instructions with labelled targets are just GOTOs with a bad placement of the label, which makes reading the source more tedious than with a classic GOTO.
For providing the benefits of GOTO, a restricted variant is sufficient, i.e. a GOTO that may jump only forwards and which cannot jump inside nested blocks.
Most programmers have seen only programs that do more data movement and comparison than actual computation, but nonetheless there are programs with abundant computations, the kind of programs that were originally written in Fortran, but nowadays they may be ported to other languages, or alternatives for them may be now written in more modern languages.
In such programs, overloaded operators do not make programs "slightly shorter to type", but they reduce the amount of text at least an order of magnitude or even much more.
The main advantage is not that you type less, but that you can read the source of some function in one page, so you can see it in its entirety, instead of having the source spread on many pages, forcing you to wander through all those pages continuously, when you try to understand what it does and whether it does it correctly.
I have written such programs, for instance which were full of long formulae where most variables were complex vectors or complex matrices (for computing some radiated electromagnetic fields with the method of boundary elements). Without the operator overloading of C++, reading them would have been extremely tedious and maintaining them would have been very error prone.
For myself, any programming language that lacks operator overloading, e.g. Java, is disqualified, because it definitely is not "powerful" enough.
Operator overloading also increases the safety of a programming language, because you can define distinct data types for each kind of physical quantity, allowing the compiler to detect and reject the invalid operations. Without operator overloading that would inflate too much the size of the source text.
I think what you mean to say is, a lot of linear algebra is involved. So multi-dimensional structures like matrices and vectors are expected to have linear arithmetic operators defined over them, because they're used quite a lot.
You don't need operator overloading for that. Your language should be providing them as primitive structures and extending operators over them. Think like how C has a polymorphic addition, where '+' doesn't discriminate between integers, pointers and floats.
There are a lot of problems with user-facing operator overloading, but I think Haskell is actually a really great example of how to do it sanely. At the point in which you have type-families, you can properly restrict the semantic domain of a polymorphic function but still keep it an open set. The addition operator being restricted over types belonging to the Num type-family as an example trivially allows you to use '+' for matrix addition.
https://hackage-content.haskell.org/package/matrix-0.3.6.4/d...
Now you've avoided the worst part of user-facing operator overloading (juniors creating incoherent DSLs by abusing the mechanism) while still providing the mechanism where it's useful.
That is a very powerful idea, but unfortunately it cannot be realized with a fixed set of language rules. Some invariants are universal, but some are bound to the domain. Some projects require, say, an int (EvenInt) to take on only even numbers and others odds (OddInt). You can imagine the possibilities here are unbounded ("the numbers should be prefixed by numbers that are divisible by my age squared").
Ideally your base language has the expressive power to formulate new abstractions and constraints. This fundamentally requires the Dark Side. Once the proper primitives are in place a different "language" - this can be literally or figuratively - is used to express the interplay between those primitives.
In effect you are starting with the most general (say, all of Lisp), restrict it to become more and more and specific - only "these modules" - until it cannot be reduced any further. If you can bring your domain down into being expressible as, say, a single config file, that'd be quite ideal. If you don't need the powers of abstraction to express your solution then exposing said powers would only invite in trouble. The mathematical equivalent of introducing unnecessary variables.
As a programmer you are free to traverse the journey from say all of Lisp to a JSON config file in whatever way you please.
The "Light Side" people have converged, or try to converge, on some intermediate state between full powers of abstraction and configuration only. I think this is useful because many if not all problems travel through that intermediate landscape on the way "down" (into their specificity). For example what you call "types" is a significant restriction on your freedom but at the same time it's so enormously general that this restriction can be considered a worthwhile default because just about any problem I can think of can potentially benefit from that restriction.
All this is to say that I don't think it's a dichotomy so much as two interacting polarities whose interplay gives each its strength.
Lisp is a particularly minimal base introducing very few restrictions of its own. But it's not the only one. Forth would be one in another direction.
I notice a lot of arguments on this topic resolve to "but Turing", which is not completely uncalled for but I think misses the point a bit. Not because it is wrong, but because it highlights the wrong property.
I don't doubt, say, Brainfucks ability to express any arbitrary computation, but I do doubt its ability to do so sanely. Now you may say Haskell's type system is very clean, but some constraints will definitely push it out of its comfort zone. I'm not saying it won't be able to express them, but I am claiming there will be dragons. One example of a constraint that's at least awkward is when the absence of a fixed type is part of the design. There are of course myriad solutions to this problem but they generally all require not quite so straightforward constructions that eventually might make sense with enough exposure, but whose complexity can actually be disproportionate to the value of the guarantee.
It's very much a testament to the genius of languages like F# and Haskell that you have to think hard of practical counter-examples.
That said, I actually think the Dark/Light polarity rears its head again even deep inside Haskell as any sufficiently complicated software system encounters barriers it needs to overcome and those require carefully constructed escape hatches: unsafePerformIO, metaprogramming, that is to say, the (relative) Dark Side.
In that metaphor undisciplined use of Lisp is like a vast, dark gravitational field of possibility and Haskell is like a sea of light with some carefully marked dark patches.
It’s clear why it feels so good to use something like Lisp for personal projects. It’s also clear why the vast majority of companies don’t use it
This matches my experience because you don't chose who you work with. Skill level is uneven among the team. Less skilled co-workers will make mistakes that will have more consequences when the language is more permissive.
That's one of the reasons you want code reviews, but then you convert your skilled programmers to teachers. This is not a good trade, because the time they spend improving the skills of other programmers is partially wasted because programmers are free to come and go. AI could change this picture.
The name of the game is therefore to keep your teams small and skilled, but this is can be difficult because of typical company politics - e.g. "bus factor" considerations, shortening time-to-market is often achieved by adding more programmers.
That's what job interviews are supposed to be for. It should be easy to hire only the most skilled programmers, especially with the massive number of unemployed programmers out there right now.
Joking about Rust users rhymes with joking about vegans; it’s true both parties generally can’t keep from talking about their choice. But you do want to hire someone who’s thought about their options and made a decision.
Don't bet the company on no one ever making a mistake. Set up a system where the unintentional risks are minimized and the consequences of inevitable failures are mitigated by seatbelts and insurance policies.
Even the most skilled engineers are going to make these mistakes sometimes. That's the whole point of more restrictive tooling, and why Rust has exploded.
Don't get me wrong. I love Lisp, and use it in some personal projects. But I wouldn't want to use it in must not fail scenarios unless those situations can wait for a human to fix the running image.
Static typing is a huge advantage in refactoring, and if you don't refactor a large program as it grows, you end up with a mess.
The Light Side believes in giving people freedom and power to achieve freedom, to allow themselves to express themselves better through programming, to become better programmers through the tools the light side provides; in the Light Side believes in programming as a way of thinking.
The Dark Side believes that people are incapable of handling freedom and must be restricted and controlled under the service of various organizations - generally companies. The companies will control access to programming and control the programmers via this access. The dark side does not believe in making programmers better programmers, it believes in managing programmers. The dark side does not believe in helping programmers to think, it believes in thinking for them.
What side for python? Haskell? Idris? Clean? C? K?...
white/dark side is too much of a simplification to me.
Programming language equilibrium is a very complex subject and fascinating one.
After years of experimenting with many of them, it is all a matter of context of usage, personal preferences, objectives, etc.. there is no such thing as "the best programming language ever", neither "best for light side programming" nor "best for dark side programming".
Similarly, there is no such things as: Light side/Dark side in artistic painting. There are tons of paints some are clearly greater than others, but there is no such thing as "the best painting most beautiful for everyone seeing this paint comparatively to any pain".
[^Malbolge]: https://en.wikipedia.org/wiki/Malbolge
[^Clean]: https://en.wikipedia.org/wiki/Clean_(programming_language)
[^K]: https://en.wikipedia.org/wiki/K_(programming_language)
C is clearly the dark side with to chance to protest.
The only way to consider K as dark side is its speed constraints relative to my favorite PL which is J. K is so fast because its interpreter fits into CPU cache.
Haskell is a border case between sides because the idea of PL is so light that using so much light in programming leans to be really dark. Haskell extracts the cognitive friction from the code environment to the types environment, programming in paper is not a joke for Haskell.
A workaround is to open one of the code elements in the browser's debugger and add color: white to the :not(pre) > code style.
Mine: a complete, single-download application that comes with everything needed to experience the interactive and incremental development programming workflow, including hot-reloading and on-the-fly debugging. For CL and Coalton.
https://coalton-lang.github.io/20260424-mine/
OLIVE: a new hand-made plugin for VSCode.
ICL: a new REPL for the terminal and the browser with advanced features.
as a bonus: a JupyterLite kernel based on JSCL that runs 100% in the browser.
find them all: https://lispcookbook.github.io/cl-cookbook/editor-support.ht...
Articles like this, and the PG articles it references, amount to "if you know, you know". I understand the appeal and I understand the explicit and implicit arguments this article is making.
Computer programming has matured quite a bit in the past 60 years. I would like to see more articles that are more considered in their examination.
It's been discussed many times on HN.
The second flaw is that it eats perfectly capable minds for years and the results don't justify the time investment. Python or whatever is fine. I wish I took most (but not all) of the time I invested in Lisp and put it into something else instead.
I started with pascal and then C++. And then discovered Emacs and Lisp. Boy, that was a revelation!
Never ever in my life shipped a line of lisp code to production. My real life code was always Python or C++ or Java or C. All of my lisp was toys and emacs tweaks (50k loc in my config!).
But most of production code is gone from my life. I am a mid-level engineering manager now. I mostly write texts, or messages, or emails, or slides... and still use emacs and lisps for fun and profit and competitive programming.
Not a single production-grade lisp LOC in 20+ years. But, OTOH, i contributed to emacs, tinkered with compilers and interpreters and prog. lang. internals - all because Lisps made it interesting for me.
And, in a way, this brought me closer than ever to the job of my dreams: i work for a major player in static analysis space.
So yes, you are right and wrong at the same time.
Standardized Concurrency is basic table-stakes for a language today. CL does not have a standardized async/await or concurrency model. The standard hasn't been updated since 1995 so it will never happen.
That's where Clojure, Julia, and many others have advanced beyond what CL (by the standard) provides. The language needed at least one or two more standards to drive those features forward, but never got them.
https://www.lispworks.com/documentation/HyperSpec/Body/17_a....
None of the functions operating on sequences are generic, so you cannot create your own type and do a `defmethod` and have them automatically adapt to it. And unlike `print-object` which gets used by format and print and others, there's also no "protocol" (or whatever term you like) generic function you can implement which will make your collection acceptable as a sequence.
You could build something like this, and maybe shadow the standard functions so that it's not a new set of functions from a user perspective, but it's a notable absence in a language with a rich OO system through CLOS. It makes sense that it wasn't in the original spec, but I still think it would have been something that would have been added if the development of CL (as a standard) had continued.
The latest finalised submission to the Common Lisp Document Repository (confusingly called CDR instead of CLDR) dates from August 4, 2013. CDRs are the equivalent to the SRFIs from the Scheme world. You could argue that they are not THE standard, but they are A standard. And considering the original standardization supposedly cost 400,000 USD (about 900,000 USD in today's money), I can see why there hasn't been another official ANSI standard.
And since the Common Lisp Document Repository appears to have been created in 2006 (the first document at https://cdr.common-lisp.dev/index_files/final.html is a description of the rationale for its existence, dated August 2006) while the Common Locale Data Repository was created in 2003, the Common Locale Data Repository had the CLDR acronym first. So kudos to the Common Lisp Document Repository folks for not overloading the acronym but finding an alternative.
There are also other basic things needed for adoption - standard APIs for database providers, etc
So while the official standard may not have concurrency, that does not mean that there isn't a de facto community standard way to do it.
Your wording sounds like it implies that Lisp "got stuck" somewhere in the past, no?
Clojure, Clojurescript, Clojure-Dart, Fennel, Jade, Jank, Jolt, Coalton - these are relatively recent (and still developing) languages, and this is just off the top of my head, there are so many more.
Lisp is not a programming language (in a sense), it's an idea. It influenced pretty much every single PL we use today and continues to do so. You can't really "level-heatedly" criticize an idea, it's like criticizing I dunno, group theory. You can though debate about merits of a specific implementation of it.
Pretty much. See pg's famous "Blub Paradox" where he sees Lisp as the top of a tower of lesser languages. He doesn't recognize that Lisp might at best be called a limit ordinal, to use math jargon. That is, Lisp is just another Blub, and the Lisp zealots haven't figured that out.
You don't necessarily want to keep going further and further up, of course. Lisp still has fascination. But e.g., in Common Lisp (I mean just the stuff in the CL spec, no ad-hoc extensions allowed) you can't write anything resembling an OS. You can in Scheme, using continuations to handle process switching.
Going further up, Lisp doesn't make it easy to ensure the absence of particular behaviours in a program, what TAPL calls the purpose of a type system. Tony Morrison has a semi-realistic example of what static types can get you:
http://blog.tmorris.net/posts/understanding-practical-api-de...
Link still works but TLS certificate expired in 2025, tsk tsk.
AI is an interesting point too where being closer to a raw AST is likely an advantage because it can focus more on the semantics instead of the syntax.
Of course we're not talking about "parenthesis". Clojure even added square brackets for arguments and destructuring, it's not about the syntax, I agree on that.
Your "Lisp is just another Blub" would be true if Lispers were stuck looking up from Lisp-as-it-was and failing to see higher. If Lispers were trapped in the Blub position, they couldn't have deliberately imported ideas that sit "above" classic Lisp. But they keep adapting: Clojure added persistent immutable data structures; brought CSP/channels; introduced structural contracts like Spec and Malli. With Coalton and Shen they are explicitly reaching for the static-types level you say they can't see. Racket's whole "language-oriented programming" perhaps a level above any perceived Blub.
Maybe what you see ain't a Lisp ceiling? Could it be that some powers must live in the substrate - and Lisp's distinctive traits are precisely what makes building that new substrate cheap?
Because that's all these languages share. What's common in CL and Clojure? They are as dissimilar as C and Scala.
homoiconicity, macros, functional bias, REPL - to count just a few.
Also, only the first one is remotely unique.
Switching between different Lisp dialects is far less mentally taxing, even when they operate in completely dissimilar runtimes. I have seen days when I needed to jump between CL, Clojure, Fennel, Elisp and Janet and from all practical points it felt like almost using the same language everywhere. While switching even between JS and TS is enormously vexing for me in comparison. Although I have programmed in both for far longer than any Lisp.
Certain people seem to find it mentally appealing. I would liken it to one of the really niche music genres. Only a small number of people like it, but they do so very intensely. The Stockhausen of programming.
That's basically all it is. A fandom. A remarkably enduring one, but not one which has ever broken out, and therefore is unlikely to ever do so. Now in an even worse position: if you are armed with an LLM and therefore uninterested in the code itself, why on earth would you direct it to write in Lisp?
I do think it is worth highlighting how many advanced parts of hot-reloading have already been covered in Common Lisp. Same with highlighting how the REPL is largely not used to directly type into, but is instead a very powerful interface for tools to interact with a running image.
But, again agreed that simply these existing are not that notable today.
I used to think so too back in the day when I was getting into Clojure. It was much later when I realized that when Lisp people talk about the REPL they’re usually talking not so much about the interactive CLI where you can evaluate commands easily but more so the ability to connect your program to a live session where you can quickly evaluate forms within your text editor and in the context of your running application, which enables much more interactive development than in other interpreted languages.
Man, it gets so exhausting trying to convince every PL critic who grabs a single (or a couple) of axis of any language and tries to dispute the value of a language without ever understanding the holistic, overall experience working with it.
Like, I don't understand, do people think that tons of Clojurians or Common Lispers who fall in love with the language after decades of working and getting seasoned in literally dozens of different PLs are on some kind of delusional trip or something?
Guys, just take a gander at Clojurians Slack; see what people are working on, what kind of stuff they're building; check their profiles. Many of them are the battle-scarred veterans of coding. Sure, some of them may have wrong opinions, but surely they can't be all wrong, can't they?
I used Clojure when it first came out, I've used Common Lisp for years, I've also used languages like Smalltalk and many others.
Yes, Lisp environments are nice, but sometimes I think Lispers are so insular they don't realize that other languages have similar things. R, Julia and Ruby have similar environments. Smalltalk is next level.
And homoiconicity is great for macros and parsing but it's late-binding that enables the live programming behaviour, which isn't exclusive to Lisp.
Also if you'd ever gotten deep into say, SB-ALIEN, you'd know the limitations too; Common Lisp isn't magic, it can't just redefine say, instantiated structs in memory. It relies on pointers then switching references on the fly.
Yes they do. God, how did you (and apparently you're not alone) read my rant and still got it 100% backwards? I specifically hinted about not picking a single aspect of a language - REPL or whatever. What is so confusing about my wording on "holistic, overall experience working with it"?
> There is real, palpable, practical, functional difference between working a Lisp REPL and a REPL in a non-homoiconic languages
And it's false. There's nothing special about a REPL in a homoiconic language. Homoiconicity has nothing to do with a REPL or tools.
I do however think SBCL has a very nice environment with great tools (especially it's compiler) and that's why I've used it a lot over the years, but I really think you underestimate the environments in other languages (including the "holistic, overall experience").
You are wrong. You might be correct about the workflow but still wrong about the semantics. Pedantically, your notion is false, because homoiconism gives the REPL a genuine capability (program-as-manipulable-data across the read/eval boundary) that non-homoiconic REPLs lack.
If yes, then you can do everything that Lisp repls can.
The parser being slightly more complex than parsing s-expressions doesn't materially change anything.
Specifically, cached state and behaviors still need to be reconciled with new inputs/overrides during “eval”, and no Lisp has an easy answer to those (neither, as far as I know, does any other language). Avoidance of closure state via late binding and a convention of reliance on very simple data structures help Lisp’s REPL/hot-patch story, and that is indeed nice, but it’s far from “guaranteed ease/safety of REPL patching” or a totally different paradigm.
Perhaps you have never tried/heard of nextjournal/clerk, scicloj/clay, djblue/portal, vlaaad/reveal or just simply tried building a simple web scraper with Playwright running on nbb. Oh, and hyperfiddle/electric - something like that would not be very trivial without homoiconicity.
Smalltalk is not homoiconic, and it's REPL experience is equivalent (I'd argue somewhat better, but that's mostly a tooling thing, see the commercial CL implementations as examples of improvements over SBCL + Emacs + Slime). Homoiconicity is not the trait that makes the CL REPL experience better than others, it's that it includes a very good debugger, the compiler, hot code reloading, the ability to redefine classes and update current instances, and so on.
That's the tooling, not the language, that provides the experience. Nothing about being non-homoiconic prevents other languages from having a comparable (or even better) experience.
Nothing about being statically/dynamically typed. Nothing about being functional/OOP/relational/logic. Nothing about being pure/side-effecty/strict/lazy. Nothing about being compiled/interpreted. Nothing about imperative/procedural/stack-oriented.
You can pick just about any single or (few) aspects about any language. Heck, it doesn't even have to be a programming language and you can find things to complain about.
You know that there are three genuine, true, legit ways to build robust, bug-free, performant software? Three! The problem? Nobody knows what they are and that's why we are all doomed to keep bashing on everyone else's choices and opinions.
"Better experience" is not dictated by "features". Better experience comes, well, with experience.
I suppose it's my own fault. I tried focusing on "holistic, overall experience", yet still picked a single aspect to chime in.
I'm simply pointing out that there is a meaningful difference in REPLs in homoiconic and non-homoiconic PLs. You don't have to listen to me, this is easily verifiable information. Google it, ask LLMs, try it yourself, or ignore the notion entirely - it's completely up to you. But let's not get too intimate and slide into insulting one another - you have no idea who I am and what cults I'm specifically fond of.
I am not an asshole for pointing it out though, I’m likable.
You obviously haven't implemented a Lisp either, because then you'd understand what homoiconicity actually gives you. And also how a REPL is implemented (since you wouldn't just get one for free, it's a tool to implement).
Rhombus built on Racket, yes, means it reuses Racket's machinery: the macro expander, the module system, the compiler, the runtime. But it doesn't mean it inherits Racket's parenthesized syntax.
Racket's expander does not operate on text. It operates on syntax objects (shrubbery). It has its own REPL, because the Read stage has to parse shrubbery syntax, not s-expressions. So, my original point stands - REPLs do have differences for homo and and non-homoiconic PLs.
Check out stuff like CHANGE-CLASS or whole image loading.
Even Bash supports hot-reloading. I've developed modules that are updated in place by sourcing:
$ . /path/to/script.shFor what it's worth, hot reload is very widely used in Dart/Flutter. When you are writing UI code using a reactive style framework where rendering the UI appears to be "generate a new UI from scratch on each frame", it's more straightforward to have an intuition about what does and doesn't get reloaded.
It's not perfect, of course. But it works really well for the kind of changes you make when iterating on a user experience.
It is limited utility, but if you start out from the mindset it's something you'll use a lot, those limitations start disappearing rather quickly.
I never managed to get over the ().
Ruby has a very flexible syntax, compared to many other languages, in that you can omit syntax in many cases. For instance, using () for method calls is largely, for the most part, optional. So when I have the python code:
cat = Cat()
cat.meow()
I find it worse than the ruby code: cat = Cat.new
cat.meow
(Though you can use () in ruby there too; but most people
won't do so as there is little point in that.)This is a superficial issue though. Python's biggest mistake is to require implicit self. It always feels as if I need to hand-hold python and trying to explain to it what an object is and what self is. In ruby I don't have that issue. Note that I find both languages fine, but ruby is "more" object oriented than python is, for many reasons.
Lisp is quite different though. I had some exposure to it via scheme and while it can be fun, I feel that lisp is a worse programming language than either ruby or python. The old game haxima/nazghul was given up eventually, primarily due to lack of time, but also because the author was no longer convinced of scheme (the core of the engine is in C if I recall correctly). He tried to switch to python, though, which did not work (but, again, I think it was mostly due to lack of time). The scheme code in haxima/nazghul was quite interesting (https://sourceforge.net/projects/nazghul/ if you want to look at the scheme code there), but I much prefer either ruby or python there. Although it would be interesting to have DSLs that would really focus on the game or project at hand, like in the old Zak McKracken game.
As a non-Ruby user, this confuses me. Why can't `Cat.new` be a function reference? Does Ruby explicitly disallow this (i.e. passing around a function)?
I suspect what you're actually objecting to is a lack of referential transparency, which is a problem in many languages (including Lisp, to be fair).
A great example is Common Lisp's own LOOP macro - if someone's usage is difficult to understand, you simply ask Lisp to expand it into the more verbose non-LOOP fundamental calls, and you don't need to understand anything about LOOP. In fact you can replace the LOOP form with the expanded code.
That's a key difference from DSL's in most other languages where the DSL code is really data structures that are interpreted.
I always thought that the "answer" to programming would be that one day everyone would use Lisp and with awesome tooling and libraries things would be wonderful. In fact, my plan for retirement was to build high-quality libraries for a Lisp language to accelerate this process.
Does the rise of AI bring an end to this dream? Is that, once again, we have solved the problem by adding more cruft? Rather than a superintelligent AI writing in the best programming language available, we're going to just spam lots of Python code until it works?
Does this matter? I don't know, I just wanted a world of elegance.
>Jean-Marie Hullot created "SOS Interface" in Lisp for the Macintosh while working at INRIA (1984) which was the first modern "interface builder."
https://denninginstitute.com/itcore/userinterface/GUIHistory...
had become a mainstream Mac product rather than being co-opted for NeXT and use w/ Objective-C.
LISP clicked with me (when taking a comparative language class in college I was the only one who managed to do all of the LISP homework) and learning it was a lot of help in using TeX.
I just wish that there was:
- a nice native or cross-platform GUI toolkit for it which was opensource
- an easy way to distribute projects as stand-alone compiled code
Deploy: SBCL can build standalone binaries I think.
> they're code that's generated at compile time
They're code that generates code at compile time. Macros can actively walk the AST of the parameters they process and rewrite them completely into new shapes. That transformed AST is what then actually gets compiled.
It will be the third arguments' 2nd arg with s-exprs, following your arbitrary pattern you figured you want to use. So it's very arguable which is easier to use, sure for some "party tricks" lisps will win, for anything more useful my vote would go with rust/Scala macros for sure. So again, you win nothing by homoiconicity, the by-the-compiler implemented parse function is more complex in rust, which is 100% abstracted away.
1. Come up with an algorithm to define an algorithm.
2. Code expansion. Instead of typing out 1000 classes that are best represented as a template of a single class, you can define a macro then use it.
3. C++ at least uses them to provide generics.
4. They let you peel back the layers of abstraction to use the language itself as an API. Useful if you want to write static analysis to do analysis on code quality, security, linting, etc.
5. Anything you can imagine, it's metaprogramming.
but also it can change your code, so you get to do all the java annotation magic stuff.
Suppose you reject the idea that there will ever be a compiler. Macros are still useful for doing "compiler-like things" in the context of interpretation, like transforming code into something that will interpret better.
We can regard the interpreter as a virtual machine acting on a representation of the code; the macro system lets you manipulate the representation in a pre-computed pass, which has no further cost at run time.
If you have a code expansion pass on top of an interpreter, for supporting macros, you can use that as an excuse to perform built-in code transformations that are not macros; those enable you to have more flexibility in how special forms are implemented.
In TXR Lisp case is a macro (family) which performs certain optimizations like recognizing values in a range and emitting a table switch. This works interpreted or compiled:
1> (macroexpand '(case x (1 'a) (2 'b) (3 'c) (4 'd) (5 'e)
(6 'f) (7 'g) (8 'h) (9 'i) (10 'j)))
(let ((#:test-0005
x)
(#:swres-0007
'#:nohit))
(and (integerp #:test-0005)
(<= 1 #:test-0005
10)
(sys:setq #:swres-0007
(sys:switch (- #:test-0005
1)
#(('a) ('b) ('c)
('d) ('e) ('f)
('g) ('h) ('i)
('j)))))
(if (eq #:swres-0007
'#:nohit)
(progn) #:swres-0007))
It is faster to do some checks and interpret the sys:switch special form to dispatch by a numeric index than to do a large number of exhaustive comparisons.Ultimately, the way we optimize interpretation is by compiling, but it can still be worth it to have better interpretation here and there.
You don't want to do this kind of optimization at run-time; you don't want the interpreter to be evaluating the condition "are these cases integers (or characters) in a tight range?". That's a property of the syntax in which the cases are constants; it wants to be pre-computed once.
I am just saying that the end-user do not need macros for much anything, unless she has some specific optimization in mind. Bloody annoying when they start making cryptic macros to "extend the language".
End users benefit from things that they don't strictly need.
Once you start talking about what people don't need, it's hard to do it in a way such that they need Lisp but don't need macros.
Sometimes "need" is about dependencies; i.e. things you "need" are provided by upstreams; you stop needing things that you made yourself.
E.g. "I don't need objects. Because, well, I have lambdas and macros, and with that control over syntax and semantics, I made my own objects. So, strictly speaking, I do need objects, just don't need them from you or anyone else."
People disagree over what constitutes a benefit; something someone finds beneficial in their work is crap to someone else, and the easiest way to disparage someone else's beneficial thing is resort to the necessity rhetoric.
"Everything that I found beneficial in conducting my successful XYZ project, which got to a level of complexity and quality in so many years of effort and lines of code, was obviously necessary; all else is unnecessary."
A really sneaky way to do that is to repeatedly acknowledge the benefit of something while emphasizing the lack of necessity. With a sprinkle of FUD about possible harms that outweigh benefits ...
In so-called real life somebody has to live with the malformed macro, whose only purpose is to beautify the code.
I even made un-macrofier, which deleted all macros and replaced instances with macroexpand, because slight adjustement of code resulted mysterious errors cause by nasty macroes.
And lets not talk about Common Lisp loop macro, which was probably scooped up from latrine of hell by the Devil himself.
The malformed macro situation is a lot better than the situation of a malformed API function to which you don't have source code.
Anything that is formed can be malformed. DNA can be malformed, leading to malformed fingers someone actually has to live with; so let's not have fingers.
(defmacro foo [code] ,,,)
(foo "<some totally different language in a string>")
=> actual, compiled Clojure function built up by `foo` parsing the string, producing a Clojure list, and calling `(eval the-list)` and returning it.I passed a string to `foo` in the example, but it could actually be anything the Clojure reader can parse even if it is semantically invalid—arbitrary Clojure data.
1. Syntax can be more familiar
2. Performance (to the extent that there is overhead for functions-calling-functions).
For example, let's consider a hypothetical lispy language that doesn't have a short-circuiting "and" operator. Macros would let you implement something to used like this:
(and (foo x) (bar y))
Higher order functions would require you to do something more like: (and (lambda () (foo x) (lambda () (bar y))
More noise, and more work for the optimizer (or in the worst-case, more work at run-time). Languages that rely heavily on higher-order-functions will tend to have terser syntax for anonymous functions. For example in javascript you might do: and(()=>foo(x), ()=>bar(y))You want a function that takes in arguments, but does not evaluate the arguments when called. So:
func(foobar(), foobar())
Normally, foobar() will be called twice - at the time of the call. With macro expansion, you can ensure that it's not the result of calling foobar that goes into the func, but this expression.A canonical example is if you want to write an if/then/else function:
if(condition, then_path, else_path)
It's quite possible that the else_path is invalid and will terminate the program if the condition is true. But if you wrote a function this way, it will evaluate both then_path and else_path - not something you want to do in a regular if expression!Technically you can accomplish the same thing in languages with first class functions if the caller wraps their code in a lambda.
The point is that in most/all languages, if the condition is true, the else branch is not evaluated. And it's usually OK to put code in there that can crash when the condition is true, because we know it won't be evaluated.
But if you make an if function like I did above in, say, Python, both the then_path and the else_path are evaluated before the decision is made.
> Technically you can accomplish the same thing in languages with first class functions if the caller wraps their code in a lambda.
True. For languages that have lambdas (which I guess is most of them nowadays).
But yeah you are right in case of a naive compiler.
(And I have trouble reading macros).
Let's say you have some Java-style a + b. It needs to work on native strings (concatenation) along with various kinds of ints and floats. The issue is that each one of these works differently internally, so the system is going to look at the incoming symbols and metasymbols (inferred type data) and is going to dynamically change between different functions like ADD_STR, ADD_INT8, ADD_INT32, ADD_FLOAT32, etc. If it did this at runtime by introspecting the type with some `switch(incomingType)` statement which involves all kinds of extra data and branches that clog the cache and create branches (two of the worst things you can do for performance). Instead, the macro (though they probably don't call it that) looks at everything and hardwires the correct output.
This hardwiring and its associated performance is the difference between a function and a macro. A macro `foo!(a b)` can inline into the current function while the function alternative must save all the registers to the stack and create a new stack frame which is very expensive.
The beauty of lisp macros is how functions and macros look the same. You may think that `(+ a b)` is the same as your favorite languages' `a + b`, but most lisps also allow `(+ a b c)` too. `+` is a macro rather than a function and it knows that `(+ a b c)` needs to convert to something more like `(+ a (+ b c))` and a typed lisp may go further to something like `(+f64 a (+f64 b c))`.
The power of this is that this function is also written in plain Lisp and it also sees all definition already existing in your Lisp image. Also, it is completely unlimited in power. So while being run, or as it is called "during macro expansion" the function could google for one of the things passed and use the result to create the output. Of course, this is not a practical example, but shows how flexible the system is.
Now I think the best programming language is Barry Jay's Triage Calculus, which is close to combinatory logic or untyped lambda calculus.
But unlike lambda calculus (which is easily expressed in TC), triage calculus has a built-in quoting and introspection (similar to Lisp's CAR and CDR), which lets you easily add typechecking. So in TC, you can have any syntax you want, just by building the correct abstraction. It's truly an ultimate programming language.
TC shows that there is a false dichotomy between programming language features and syntax on one hand, and the function/API definitions in the standard library or user code on the other hand. Every time you write a piece of code intended to be reusable, you're adding to the language. It can all be expressed as some term in TC.
All the syntactic squibbles are a matter of habit. Although one syntactic feature you really want in TC is the let over lambda abstraction, because it's somewhat annoying to order combinators by hand.
As a hobbyist, probably all of them. It’s worth at least trying to go through SICP in Scheme (use Racket with the SICP language), it’s worth learning CL (use SBCL) to fully appreciate everything the Lisp world has to offer without compromises, it’s worth learning Clojure because it’s, frankly, a little bit cleaner and more elegant than CL in many respects.
games (Kandria),
Maxima,
a faster pgloader (https://tapoueh.org/blog/2014/05/why-is-pgloader-so-much-fas...),
hackernews (was rewritten to SBCL: https://lisp-journey.gitlab.io/blog/hacker-news-now-runs-on-...), first reddit…
https://www.lispworks.com/success-stories/
also Emacs (for another dialect).
(Yeah, OK, he didn't have a programming language...)
arguably redundant since GP did say symbolic communication, but Lisp had T before Algol 60 came out
phanitathion, roaphs, aqueduphs, mediphine, wphine...
I'm sorry. Couldn't resist - I watch Python and do Lisp. Or the other way around, sometimes. And, no, I'm not from Barthselona.
- Guy Steele, Java spec co-author
"35 years of Lisping at SISCOG"
https://www.siscog.pt/en-us/blog/35-years-of-lisping-at-sisc...
https://en.wikipedia.org/wiki/Emacs
https://help.autodesk.com/view/OARX/2024/ENU/?guid=GUID-A0E9...
Because you treating Lisp just like any other (non-homoiconic) PL. Give an agent a true Lisp REPL to mess around, and you'd be surprised. Things get very interesting. I still don't understand why more people don't do that - isn't that obvious first thing anyone should figure out? Like I can't even imagine working with Lisp without a REPL and structural editing - I'd immediately fail at balancing parens. Why do you expect a [dumber] machine would do any better?
What do you think about making the agent write type annotations? There are built-in forms in CL, and in Smalltalk, I settled for pragmas for now. They are not checked, but since I started using them, I think the rate of one- or two-shotting solutions has gone up.
I think that was true last year. In my experience, it’s no longer the case with Claude Code or Codex.
It can get good with the right setup. I made Codex work with GToolkit (similarly underrepresented, but technically impressive and a seemingly good fit for LLMs), but it took a lot of tweaking of the project structure, extensive instructions in AGENTS.md, and some custom skills (some borrowed from Gt4Llm, the GT's built-in model harness). Out of the box, it burned tokens and took way too long to implement even basic things.
YMMV - as with everything LLM-related - but I think without a similar setup, an agent instructed to write CL/Clj/Scheme/Racket will have the same issues. It might be better in an established, large project - but starting from an empty Git repo, I suspect you'll have to fight your way to productivity.
LLMs do somewhat okay job when you use them with Lisp, treating it just like any other PL, which is roughly the Unix/pipe model - batch-style. Agent spawns process -> reads stdout/stderr -> spawns next process. State lives in-between the calls and in files. Each tool invocation is stateless.
Things get far more interesting when you give an LLM a true Lisp REPL. LLM stops guessing and starts empirically analyzing current state of things and produces working solution faster, costing far less tokens. And you get to watch it solve things interactively, e.g. I often let AI poke through our UI (via Playwright-driven Clojurescript REPL), while monitoring situation in k8s - through nrepl port, connected to Clojure REPL. It literally interactively walks the DOM, finds the selectors, clicks buttons, etc. All without restarts, complex state management and all.
https://github.com/malkia/p4bee
but haven't touched it in years....
I haven’t let coding harnesses run REPLs. When I do let a coding harness run tests I specify in, for examp,e, my Common Lisp skills file to run ‘sbcl -load …” so bash test commands are one liners.
It would be interesting to work on skills and a harness to use REPLs - nothing bad about that idea, I just haven’t tried it.
I looked at lisp many times and it just doesn't do it for me.
Any style of writing you are not familiar with will require a lot of training. Try reading anything written in the Greek alphabet for example.
It uses python for the interpreter.
It's a lisp so it can be used for anything since macros have been implemented. You can create your own syntax using macros.
That's one way, you stopped too early in your investigation though.
You can produce binaries, though the precise mechanism will vary by your implementation. And there's no reason to use the REPL for it. You can create an executable file with something like this:
(defun main () ...) ; do whatever you need in here for program launch
(sb-ext:save-lisp-and-die "my-program" :executable t :toplevel #'main)
And then `sbcl --load program.lisp` (or whatever you name it) and it'll produce a binary for you. Other implementations will have other methods of achieving the same thing.Or, if you don't need a binary, you can have something like this:
(defun main () ...)
(main)
And then run `sbcl --load program.lisp`. That will compile and execute it without ever invoking the REPL.(NB: Using a function named main isn't strictly necessary, I named it that for the example. Name it whatever you want.)
The one part I can’t imagine is how do you pass a Lisp function as a callback?
If you're really good at Lisp, you can write that automatic-callback-creation function yourself. But if you're really, really good at Lisp, you will just use one of the ones that other people have already written, e.g. `define-alien-callable` from https://www.sbcl.org/manual/#Calling-Lisp-From-C or some other similar FFI.
Maybe I just don't know where to start.
ps: you don't need to learn Emacs. A terminal even is enough. See the Cookbook/ Editors section.
> [...] among its extensibility, its interactive environment, the REPL, and a lot of other features we haven’t touched yet. It is the combination of all of them that makes Lisp programming what it is.
Agreed. However, although the alternatives are few, they do exist. Today, I'd like to convince you (whether you're OP or a commenter) to give one of them a try. I'm talking about GToolkit[1]: Smalltalk/Pharo-based reimagining of Smalltalk as a productive environment for modern system design, analysis, and implementation. It's based on Pharo and its VM, but with GT-specific extensions and replacements, developed on GitHub in both Smalltalk and Rust.
I used both Common Lisp and Smalltalk over the years for some of my side projects. Technically, the environments are comparable: image-based, live, interactive development is central to both. Lisp is easier to fit into modern workflows: it's still just files on disk. If you ignore the REPL and treat the image as a bundler, you can have a Java- or Python-like development workflow. You can gradually adopt the more interactive ways of working with the codebase. Smalltalk was historically more of an "all or nothing" approach, but nowadays it supports Git-based workflows as first-class, with GToolkit providing additional tools directly. GT is tied to a single IDE, but that IDE is genuinely powerful, pragmatic, and easy to customize: the entire IDE is Smalltalk code[2] that lives in the image beside your code, so you can live-edit any part of it at any time.
The languages differ, most obviously in the object model (single inheritance and message sends in Smalltalk, and multiple inheritance with multimethods in CLOS), but I don't think one is strictly better than the other for the vast majority of code. You just need to structure your code differently. Both systems are very dynamic, so neither is like Java or C++. After quite a few projects in both, I am convinced that this difference doesn't matter.
Both languages have very simple syntax. Smalltalk doesn't have macros; instead, it has an extensible/replaceable compiler for method bodies. This capability is used, for example, for compiling grammars into parsers/lexers in SmaCC. It's not as convenient for control-flow abstraction as CL's macros, but Smalltalk tends to make them out of blocks (lexical closures with non-local exits) plus actions on thisContext. In practice, I never found either language too limiting in what I could express, and both have small, regular basic syntax.
TL;DR: Common Lisp and GToolkit Smalltalk offer comparable technical merits, with the most pronounced difference being GT's built-in, high-quality IDE, which open source Lisp lacks.
The IDE (and the capability to build cross-platform, but natively rendered GUI apps) is the major selling point of GToolkit, but the bigger reason to consider it is social. Lisp had a bit of a renaissance from 2005 to 2015, but ~it has since died down~ (EDIT: After checking a bit, it's more like there are similar levels of development now, but it's just varied, and I personally hit a few areas where nothing new happened for a while. Or in other words, the hopes of CL getting catapulted to mainstream by the wave of adoption didn't materialize, and the adoption seems to have largely plateaued, with a slight upward trend since then.) A lot of great things for Lisp appeared in that time: ASDF and Quicklisp being prime examples. Unfortunately, the momentum was lost, and since then, the rate of development has plummeted. It's still a solid proposition because of its stability, which means a lot of old code still works perfectly well, but it's a double-edged sword: it would take a hundred miracles in a row for CL to get a plausible M:N concurrency story, for example. This isn't true for Smalltalk, and doubly so.
There's a small but active community around Pharo, mostly academics and hobbyists (IME; and it's from a decade ago, so YMMV). Pharo itself is already a "Smalltalk-like" language, and it regularly gets features that are not in original Smalltalk (stateful traits and slots (reification of class and instance variables) are good examples). It continues to evolve, and each version brings notable improvements to the language, the VM, and the class library. In CL, since there are many implementations maintained by various groups and the standard is set in stone, adopting extensions to the standard is incredibly hard and time-consuming. You can, of course, commit to a single implementation, but there are only 2 or 3 implementations that are actively developed and trying to do something "new"; they are all experimental and incomplete. And while SBCL has quite a few nice features, its codebase was a bit hard for me to grok (read: days on end of banging my head against a wall, then giving up; I'm much more comfortable messing with Smalltalk bytecode compiler).
Secondly, GToolkit is driven by a group of coders[3] who have established a profitable consultancy. It's being developed around a single vision that the authors swear helps them achieve their projects' goals. Even if you're not sold on moldable development[4] as a methodology, GToolkit gives you all the tools you might need in a modern development and then adds tons of domain-specific tools and utilities that might be useful after just a bit of adjustment. The whole environment is built to make such adjustments as painless as possible, too.
The only problem I experience with GToolkit is the lack of structured, book-like documentation. The GToolkit book is closer to the PHP wiki or a bundle of tutorials than something like the Rust book. It's not that the docs aren't there - they often are, but finding them effectively is a bit challenging. I found that simply cloning all the repos that GT consists of (tens, if not hundreds, but there's automation for that) and pointing an agent to the directory with them is often enough to quickly find what I'm looking for (if the built-in Spotter fails and I'm too lazy to construct an in-image search).
TL;DR: GToolkit/Pharo move much faster than the CL world, and while the number of maintainers might be similar (honestly hard to estimate, though), the effort on the GT side is more centralized and, in my opinion, heads in a better direction. If you want an extensible, live, dynamic environment that shares many of Common Lisp's strengths, but is more polished, more actively maintained, and feels more modern, take a look at GToolkit.
(Disclaimer: no affiliation, just a programmer who likes to explore unpopular languages).
[2] Some parts are implemented as Rust dynamic libraries and called from Smalltalk via FFI.
Interlisp-D, Smalltalk, Mesa (XDE) which evolved into Cedar.
If you read Xerox papers about all of them, there are several quotes on how relevant it was to share the same programming experience across environments.
Which is why, given their linage, JVM and CLR are the closest big mindshare ecosystems that somehow still have traces of those features when using their IDEs and runtimes, even without being a proper Smalltalk or Lisp.
Right. If you view the language as just a part of a bigger "programming experience", you can do with a worse language with better tooling (pathological example: (lack of) namespaces in many Smalltalks to this day). If you focus on language design above all else, you end up with either something pretty on paper (but never implemented) or something practical and (kind of) elegant, but it'll take 40 years to get the tooling to where you want it (and still won't cover many important capabilities).
Also, it's easier to swap languages if you focus more on tooling. From what I read, many Lisp and Smalltalk IDEs/environments were (and some still are) polyglot. I sadly didn't have access to them at the time, but I imagine switching from Turbo Pascal to Turbo C++ was easier than going from Turbo C++ to Visual C++? That said, tooling tends to break if the language actively fights it, so it's not like PL design is completely irrelevant.
> Which is why, given their linage, JVM and CLR are the closest big mindshare ecosystems that somehow still have traces of those features when using their IDEs and runtimes
Yes. The only problem is that those concepts/features become niche black magic (somewhat unavoidable for advanced features in "big mindshare ecosystem"). You can definitely compile Java classes dynamically at runtime, install them, and use them normally; however, outside of frameworks/IDEs/JVM languages, the ClassLoader is used mostly for loading static assets, and even simple reflection is viewed with suspicion.
Using Smalltalk or Lisp directly puts those powerful concepts "front and center", which a) is good for education; and more importantly, b) feels good when things click :) It could be an advantage for small groups of programmers, but from what I understand, it becomes a liability in large organizations. Pharo is especially bad at it, and GToolkit doesn't fix everything (they do a good job at providing tools, but don't want to maintain too large a patch set for Pharo, which is a good trade-off given their limited resources). But I'm replying to "Why Lisp (or When)", and in this context, Smalltalk is (IMO) the better direct alternative than JVM/CLR.
My weird take is that lisp would have a better shot if one would replace the parentheses with square brackets. It might just be me but the round parenthesis make it visually painful to look at code blocks. I feel square brackets would fix that.
But ... somehow almost everyone simply ignores this propaganda --- that's actually the way we ought to handle propaganda! Funny that it works with Lisp, but not with many political topics where it can be shown that propaganda really works.
My experience with Lisp is somewhat special-cornered. I only know Emacs-Lisp, and that not great. And for me, Lisp an out, outdated and confusing thing.
I don't know ANY other programming language that elected assembler mnemonics to be "high"-level expression: car and cdr.
While I love Emacs, they claim all the time that is has 100% discovery through build in doc. But that is only 70% true. It doesn't exist in Lisp like it does in other languages. For example, I have an association list. Now, what functions exist that uses them? I can't use the equivalent for (dir alist), alist.dir(), or use some LSP for that. I can't even look in the symbols, because operators are written like assoc or assq with not logic. And then there are either prefixes to them (rassoc) or postfixes (assoc-string).
That there is no namespace is soooo 1970. I mean, even Turbo-Pascal had namespaces.
Typechecking is nonexistant.
At least we have byte-compiling.
And note that I don't croak about the parenthesis. While I find it mildy weird that I as a human program in almost-AST, Emacs has superp support for the parenthesis. So people croaking about them just had never had a good editor IMHO.
Now, one can argue that Emacs Lisp != Common Lisp. But these propaganda articles don't tend to promote one specific Lisp, they only talk in generals. So I can apply this general principle and compare it to the tiny corner of these awful language family.