Typical ignorant take. Lisp macros allow you to create restricted DSLs to prevent bugs from being expressible, including full static type and structured programming systems. Basically no one understands this until they reach the 'lisp enlightenment', so the decades of beating this drum falls on deaf ears. AI is here now so it's time to let it all go.

Of course you feel this way. You just finished reading SICP or maybe you binged PG essays last weekend. But eventually you'll read Simon Peyton Jones and start screaming about functional programming and algebraic type systems. Then, if you're lucky, you'll get a real job and realize that languages are just a tiny part of software engineering.

I'm excited for you to experience that journey.

Wadler's old article comparing Miranda (a Haskell forebear) to Scheme might be a better place to start.

https://www.cs.kent.ac.uk/people/staff/dat/miranda/wadler87....

This was a very pleasant read.

cute but you don't get lisp yet.

I would be very interest in seeing how "getting lisp" enables you to write software that is more successful than the C and C++ software that runs the world. Perhaps you have written software in Lisp demonstrating this? Something you can show us?

The reference text on time keeping used to be implemented in Lisp.

Alan Kay says a lot of problems with C/C++ go away when using the higher math more easily expressible in Lisp and like minded languages.

Jürgen Schmidhuber says the problem with computers from a mathematician's point of view are the numbers.

Go ate a big lunch from Java, C and a bit of C++ as a systems' language, which Go was designed as "C sucessor without the C++ bullshit". Even the most modern C (Plan9/9front, not what the ANSI C comitee vomits in every iteration) it's a very different beast.

Still, Common Lisp it's in places where trying to build such kind of software in C/C++ would be a reciper for disasters.

https://www.lispworks.com/success-stories/index.html

Do you like Apple ]['s?

You've got a good point. Code generally needs to be read more than it is written. At the very least, you realize this as you try to revisit something you had done previously and are left scratching your head. Encapsulating the complexity in a custom DSL can be great for simplifying all the code you write in the DSL. But it also raises the stakes - if you ever have to revisit the DSL implementation the complexity is there lurking.

I see what you did there.

"Of course that's your contention. You just got finished readin' some Marxian historian -- Pete Garrison probably. You're gonna be convinced of that 'til next month when you get to James Lemon, and then you're gonna be talkin' about how the economies of Virginia and Pennsylvania were entrepreneurial and capitalist way back in 1740. That's gonna last until next year -- you're gonna be in here regurgitating Gordon Wood, talkin' about, you know, the Pre-revolutionary utopia and the capital-forming effects of military mobilization." Good Will Hunting (1997)

This is the "fallacy by Turing completeness".

Sure, and you may as well write full systems in Minecraft and create your own type system inside and whatnot.

Macros can make some small specific uses safe, indeed. But they are not comparable to languages with type systems where everything is type safe, unless you literally have written a new type system and compiler to begin with. Which is trivially true in every other language, C can be 100% safe with a much better type system, I just read a text block and compile it as rust code! Wow!

I have no horses in this race and I'm sorry if I misunderstand but I don't think he means to say Lisp is by nature superior to say Haskell. I take it as a statement on the generality inherent in its design. It is by its very nature very low on restrictions. Which is to say your freedom of expression is very nearly as complete as it'll ever be.

Sure you can beat Minecraft into Lisp and then Lisp into Haskell - as Turing made sure of - but you'll be battling a lot of dragons along the way. It's beautiful that you can beat Haskell into letting go of its type system by, say, creating a C compiler in it. It actually still amazes me the universe has this property.

In general I see "building" (I rather call it "conjuring") a system as going from the most general (all of your programming "language") to the specific (your solution). I can see how the most general of languages makes for a comfortable starting point of that downward journey.

I can also see how not starting from The Universe In All Its Infinity Glory (Lisp) but from The Planet Earth (say, Haskell) is helpful if you want to conjure something made for humans, but if you want to go beyond the mortal plane.. (I'm not saying that's a good idea by the way).

My issue is that a language is just as much about what it disallows, as much as about what it allows.

It's absolutely trivial to allow everything, you just have to make a Turing-complete extension either by design or accidentally.

It's much much harder to carefully choose more limiting primitives that only allow you to build stuff that keep certain important properties about them. You can't subtract stuff after the fact.

E.g. something like rust's borrow checker could not be realistically implemented as "a lisp" macro (what does lisp even mean in this case). For that to work you would literally build a rust compiler over s-expressions and you might as well leave off the s-expressions at that point.

And every part that was not compiled by that macro or whatever would be unsafe and could not interact with the "rust-lisp" compiled parts, as you can no longer assert anything about them (there is nothing mandating the usual single writer, xor multiple read-only references laws)

I agree. Restrictions are core to software development. In fact without them I don't think there is development in the first place. As you know I see software development as going from the general (the full bandwidth of the computational substrate available to you) to the specific (the absolute minimum - if any - computational structure you need to get what you're after).

Rust is more restrictive than Lisp. These restrictions are sane and productive for most general software development in 2026. They form an excellent base to start your work in this day and age. Starting with raw Lisp feels like starting with the general notion of mobility instead of just getting in a car. The car is not inferior at all. It's a set of restrictions on the most general notion of mobility that in fact buy you a lot of comfort like knowing it doesn't need to eat hay or take a poo in the street. The freedom they take away, they give back in strong, useful guarantees about stuff you actually care about ("getting places").

I can see how Rust, Haskell or whatever provide a set of comfortable, general-enough restrictions to make life for the common developer easier and more productive.

This being HN I do want to allow myself some contrarianism and point out that finding the most minimal, yet most general computational substrate is not at all trivial. It's in some ways equivalent to finding the most minimal set of invariants ("laws of physics") that can describe the most wide range of phenomena ("nature"). Which is to say I find it pretty damn impressive. So I only "disagree" with this:

> It's absolutely trivial to allow everything

I don't disagree because it is wrong, it's clearly not. Turing shows all languages collapse into one when it comes to computational power and I don't doubt that. But I do know there is a distance between the computational substrate, the primitives of the language, and the structures it expresses. Let me give an example.

A spreadsheet can represent a computer game. A game engine can represent a spreadsheet. But if you build Excel inside Unity you haven't made Unity spreadsheet-like at the substrate level; you have encoded spreadsheet semantics in Unity's primitives. Functionally they may be equivalent, and that equivalence is beautiful and Turing can tell you all about it, but the relationship between the native primitives and the concepts being expressed is different.

Turing makes building endless towers that are functionally equivalent possible, but what I mean is that there is an architectural difference between a system directly built out of the least semantically committed substrate available to it ("native") or "embedded" in simulations of substrates built on top of those primitives.

I think Lisp is close to the "least semantically committed substrate" you'll ever come across in practice. It's certainly not the only one, but it's a particularly clean and relatively practical one. One can say, for example, raw lambda calculus is equally uncommitted but it's kind of .. hairy. Lisp's primitives are basically the machinery of symbolic manipulation itself. It's bloody amazing.

Sorry for the verbiage. I went way overboard and meandered into foggy, mystical meadows full of mysterious entities so feel free to ignore.

Have a nice weekend!

Thank you for the reply, and I definitely agree with you on many points. I absolutely don't want to take away from Lisp that it is somehow uniquely elegant, and does have a mathematical beauty to it, this is certain.

I just feel this (and its practical ramifications) are often overexaggerated.

> I just feel this (and its practical ramifications) are often overexaggerated.

I dunno. Can't fully agree or disagree. Nominally, yes, you really don't need s-expressions and homoiconicity for creating reflective, self-hosting runtimes - live redefinition is possible in Erlang, Pharo, Ruby. Metaprogramming ergonomics - sure they are cheap in Lisps, but even Lispers try to avoid reaching there, Clojure specifically recommends thinking twice, although projects like Hyperfiddle prove macros absolutely can be very powerful. Syntax, mathematical beauty, yada-yada - that's all "poetry", much of the real world operates on tons of very ugly yet functioning code, right? So, really Lisp-shmisp, whatever, no?

In practice though, Lispers are enormously pragmatic - I'm not self-referencing here, I have worked with some. It's incredible how rapidly they can build things, prototyping on the fly. How quickly they can move between different runtimes - I've seen codebases sharing ideas between absolutely dissimilar platforms. It is inspiring how undogmatic they could be - they easily move between modes - data/code, FP/OOP, interactive/compiled, etc. They have good understanding of type systems and some even know good deal of theorem provers. For whatever reasons, Lispers are conspicuously, disproportionately effective, and this is true. Sure "citation needed" here, but this is my empirical, anecdotal observation working in different groups, using distinct language stacks for many years.

The causation probably runs through the programmer, not the program. It's not like Lisp unequivocally emits good engineers, maybe it's that a substrate with minimal syntax and maximal malleability trains a particular disposition - they treat everything as reshapeable material, distrust dogma because the language never enforced one, reach for the smallest thing that works because the language makes "the smallest thing" actually small? Maybe Lisp doesn't make anyone write better programs, it just makes it cheap to keep changing your mind? Perhaps cross-runtime fluency, undogmatism, and rapid prototyping are all just "cheap to change your mind" in a way?

Could be selection bias - maybe Lisp just attracts curious, theory-literate, undogmatic people? I don't know. What I observed is for whatever reason Lisp typically attracts older, more experienced engineers. And therefore all the "Lisp propaganda" comes from them, and demographically that's a small subset of overall community and maybe that why it often feels like overexaggerated rhetoric?

> unless you literally have written a new type system and compiler

This is entirely possible with plain lisp macros. See https://coalton-lang.github.io

Having that natively available as plain Common Lisp code is a very different thing from your "just read a text block and compile it as rust code!" concept. Or at least, the tool chain gymnastics required to make a C program emit its own source in another language... Why?

Well, I was talking about the fundamental part. Of course you can have more practical implementations, like Scala or rust's macro system, that gives you a proper typed AST.

Type safety is a domain specific concern, 99% of the time your domain will not have 'types', often it won't have notions of things at all. But any kind of type safety can be added at any point with macros. Something like rust would be a huge project but it doesn't get easier than macros, see Coalton, and they compose with one another. When you run into the same requirements in another language you are simply stuck source pre-processing. There is no stuck in lisp.

There are plenty of languages with macros though, nothing special about lisps.

And no, language semantics absolutely don't compose. Like you can't just do some kind of optimization in one place if you do some mutation on it in another place. Optimizations work on global assumptions that every part of the codebase have to abide by. Any part not doing it will make the whole thing crumble - and "there is no stuck in lisp" is exactly why you can't have your cake and eat it too.

In an awkward, error-prone, non-mathematically rigorous and easily subverted manner. That whole "make invalid states unrepresentable" thing relies upon the fact the semantic structures you're doing it with can't be shadowed silently by other parts of the code, and the safety constraint itself is guaranteed to be completely pure and deterministic. Lisp macros and reader macros fail at that criteria. They aren't any different from any other collection of procedures and their collective interface. Actually, they're a fair bit worse since the natural cognitive overhead from juggling multiple layers of evaluative indirection lends itself to blindspots of edge cases and unsoundness.

I think "macros are a safety mechanism" might qualify for the top 3 "new lisper mania" things I've ever read.

It can be made as mathematically rigorous as anything computable, and as non-awkward as is possible to represent. You either reify the invariants and semantics as statically analysed syntax or they are implicit in your collections of procedures and objects which is far more cognitive load. If you need to judge multiple levels of abstraction while using the DSL then the DSL is the wrong fit for the problem. There is no 'evaluative indirection' because a macro is a _compiler_ and the underlying code can be extremely alien to the DSL semantics.

Any argument against macros must be levelled against compilers in general, as _they are the same thing_. Rust is compiler as a safety mechanism, not different from macros as a safety mechanism.

Again, fallacy by Turing completeness.

It's actually harder to make something not Turing complete - in and of itself this property is absolutely useless and tells you nothing about how practical something is.

yes it's desirable to use non turing complete languages as less shit can go wrong, which is why you need macros. You seem to over estimate the difficulty of writing compiler macros, remember you only need to go from the DSL -> Lisp, not lower it into machine code. The DSL's compose perfectly as well, lower one to another or combination of them + lisp.

Well, compilers have layers - you ain't outputting assembly from the AST layer directly, usually there is some kind of IR. Like LLVM have plenty of layers.

And for a simple DSL you can usually just use a sufficiently expressive language, no need for macros. Like kotlin/Scala has html DSLs that look pretty decent.

For anything more complex though, you can't just "compose stuff", a type system is a global property so for that you literally have to write something way more complex than a local macro should contain.

The inflection of the copula "can be" is exactly the problem, as is the 'anything computable'. There's a reason Isabelle is used as a proof assistant, and Prolog isn't. The former guarantees sound, mathematical rigor that can't be trivially violated. The latter doesn't. Prolog is much more powerful and flexible than Isabelle. Power and flexibility are disastrous for safety. As someone else said elsewhere in this thread, safety is about restrictions.

In this case, it actually runs deeper than this, there are structural issues which prevent them from being sound right from the beginning. It's the mere fact that the whole behavior of your DSL can be silently changed without touching the module it's defined in, or the file it's being imported into. A well-meaning junior, in a completely different department, can completely destroy your invariants in a completely unrelated part of the codebase. Whether by shadowing runtime functions, or a package-level collision that exists upstream. Even Scheme's hygienic macros don't actually solve this, they just make it less likely. At the point in which you have no behavioral guarantees of semantics, you do not have a safety mechanism at all.

> There is no 'evaluative indirection' because a macro is a _compiler_ and the underlying code can be extremely alien to the DSL semantics.

This is a trivial contradiction. That is precisely evaluative indirection. Yes, stacking multiple compilers on top of each other, especially when their semantics are non-isomorphic, is a massive burden of mental overhead. Given you already have this problem with the mapping between Lisp and binaries, adding even more complicated layers on top of it is a recipe for disaster. Particularly because: you are going to make mistakes in your logic. Macros provide no means to stop you from blowing your own leg off when writing them. That is not what they are designed for.

Finally, you cannot escape the awkwardness of the underlying metaprogramming system, you will always have to wrestle with quotation and quasiquotation. You have to define a DSL before you can use it.

Only at the point in which you treat and use Lisp like an unserious toy, rather than the powerful tool that it is, can the fantasy of "macros are a safety mechanism" begin to make sense.