... and I think you will both find it intellectually stimulating to explore the complexity behind ERP systems that makes a common DSL near impossible across industries, or as businesses change.
DSL presumes agreement on semantics, and that's often the most difficult part.
- economies of scale no longer work, and you end up doing a custom ERP for your business from scratch.
- your business changes might invalidate your model quickly. You sell through distributors, but open an online shop -- and suddenly your customer is not one of few dozen well-known businesses with a known address and tax number, but user2252 who bought something late at night last night. And you want to understand the needs and behaviors of both.
- for the economies of scale, you might develop your custom solution 20x faster now, but you're still in competition with the established provides with templates for most of the cases (who btw have the same LLM capability at their disposal)
- for the change in business -- you can ask an LLM which changes this induces, and it will give you most typical impacts. And then you're back at square deciding if it's better to roll your custom DSL and the custom system downstream, or just use off-the-shelf stuff that covers 95% of it from day one (well, maybe day two or three)
When it comes to back office business programming, there’s just a lot of code tasked with copying a litany of bits of data from one structure to another.
Whether it’s copying a web form into a database, or converting Their JSON to Your JSON, it’s a lot of detail that does not abstract well. It’s all shapes and sizes and formats, and it almost always has to be enumerated in excruciating detail and, typically, twice.
Sure, there’s logic and whatnot involved, but it, too, is specific to some subdomain of the larger system and it, too, does not abstract well. Not in the large context of the overall system.
Accounts Payable and Accounts Receivable, at 10,000 feet look almost identical. They’re almost literally the same thing with the sign flipped. But in practice, they don’t share code well. You end up with two similar systems, but not similar enough where sharing is actually worthwhile.
At best they can leverage a common API to the GL.
Turns out a lot of languages can manifest a decent level of abstraction. But even then, folks push back.
Consider the love/hate relationship with ORMs. Or the annotation driven markup in Java programs and the underlying “magic” that they enable. Like scribing mystic runes onto things.
Those are both very powerful, yet folks experience that and toss their hands in the air and throw out the baby with the bath water and jump into something “magic free” like Go.
Just because you can use something like CL to “make your own magic”, doesn’t mean it’s a good idea. Doesn’t mean it scales. Doesn’t mean it communicates well to others. AI or no.
It’s not the AIs world yet. We already know that if the AIs want a better language suited to AI efficiency, they’ll come up with their own. I’ve already seen crass examples of “code only an AI could love”. Completely impenetrable, at least to me. May as well have represented it as a color image and collection of RGB values. Opaque to me, but the AI could “read” it.
There is much more to programming and systems than token density, and AI is still getting cheaper by the day, so less reason to even pre-optimize for it anyway.
Also, in most languages an error will crash your program. So if you’re writing code with an LLM it will have to read your crash logs to make some changes and run your program again. In Common Lisp your program won’t crash, it’ll stop and open a debugger with the whole stack and all the variables. You can just point your LLM at the debugger, and it’ll make its fix and resume the program.
What does this look like in a real example system that you're maintaining? I can't imagine you'd always be able to resume like that if it's something like a webserver.
Where I'm coming from: I have never used Common Lisp, closest was Erlang for a ~3 year project. I understood the appeal of functional programming, just didn't like it.
Not the author, but unless the server was running in a short-lived ephemeral container (in which case the management system probably killed the container and started a new one), the CL process would still be around in a paused state, waiting for you to connect to it and tell it how to resume.
https://comp-348.github.io/lisp-debugging.html has an example of what it looks like. A toy example, to be sure, but the basics of a real example would still look the same. You're given a choice of several options, very reminiscent of the "Abort, Retry, Ignore?" choice that used to be oh-so-familiar in the days of DOS. Except this one is more useful, because it offers ways to specify how to resume. E.g., the toy project is halting on a `(print X)` call where the value of X is not defined. And the choices are:
0. Continue. (Retry using X).
In the toy example, this would fail, because nothing else has defined X. But in real code, the name might have been undefined because the data needed to define it hadn't arrived yet, from the database or the filesystem. In which case retrying the statement might work the second time.
1. Use-value. (Use specified value).
This one prompts you to enter a value for the undefined variable, and continues, but it does not modify the value of X in the program. The next time the program tries to use X, it will halt again with another "unbound variable" error.
2. Store-value. (Set specified value and use it).
This one, just like Use-value, will prompt you to enter a value to use... but then it will set X to that value and continue running the program. Next time the program tries to read the value of X, it will have one, and the program won't halt.
3. Abort. (Exit debugger, returning to top level).
This is what you would choose if there's no good way to fix the error, and you just have to quit the program and restart. Though note that choosing this option isn't going to exit the program you're debugging, just take you out of the debugger. You'll still need to kill-and-restart it some other way... or come back an hour later when the value is finally available, and then choose options 1 or 2.
Hopefully that gives you a taste for what the CL debugger is like to use in practice.
Test driven developmet practitioners in Smalltalk used to (still do?) write the test before writing the implementation, run the test, and, when the "method not implemented" error shows up in the debugger, type the implementation and continue the execution. Common Lisp can do that too.
So, in practice, it might look like you hit any kind of runtime failure, and then the LLM writes some code to fix it, and the user's request completes successfully with no errors.
Loved the post, have been batting around similar ideas w/clojure. I have two questions:
1. Have you had much success w/moar macros in the age of the LLM? I've been impressed by the models' ability to write good ones, but I can tell my taste/judgement for macros isn't quite there. But they tend to be pretty good at writing gnarly ones, and I would love to work more macros into my workflow. Would love your thoughts. (Have I taken "Simple Made Easy" too far and left macro value on the table?)
2. Do your models ever get confused with image-based dev, and state? It's seemed dumb to me to have models keep running `sed` to change files, but it is nice to have a human-readable, filesystem-backed record of definitions. Would love to hear your experience here.
I've seen a big improvement in LLMs writing macros since Opus 5.5 came out. What really helps I think is that I've written skill files with my own examples and instructions.
Same thing for image-based dev. without an agent.md file with good instructions on how to work with a live image it will do dumb things. this sort of workflow is jsut so far off the training distribution.
I think what changed recently isn't that LLMs got better at CL, they just got way better at taking my skill/agent.md files and reasoning through them.
I'm convinced we'll be moving at some point to n-modular redundancy systems, using different languages implementation, simultaneously. For the cost of writing code in n languages tends towards zero now (due to the use of LLMs) and the various stacks and implementations shall then cross-check each others.
There shall be one minimal, ultra-hardened, tiny attack surface, "majority gate" picking the answers that most implementation agrees on.
This shall not only detect a great many implementation issues but also it'll help find security issues and platform defects (say the Common Lisp, Haskell, Rust and Python all agree but the Java one fails: in rare case it'll be due to a JVM bug and finding that out shall be simplified).
Code shall be generated from specs in n languages and ran on n stacks. The gate shall return the answer as soon as a quorum is met and, later on, any bogus answer arriving shall be cause for enquiry.
We'll have such systems, it's just a matter of time.
Presumably a DSL would allow you to do more heavy lifting in your specific domain with less tokens.
Let's way you wanted to do a web application using LLMs. Using a web framework would cost less tokens than using the vanilla underlying language (Python, PHP, whatever..), which is again way less tokens than building up from assembly (an LLM should be able to do this given enough time and compute).
I hadn't thought of that, and it's an interesting point! Writing a program in fewer tokens (in the older sense of "token", meaning that one measures a program in AST size rather than lines of code) has been the advantage of Lisp all along. If, because of token economics, the same advantage accrued to LLMs in the end, I dare say certain people would be pleased:
I came to the same conclusion over the last year. The Lisp family, especially used with a CL REPL, present a lot of inherent advantages for agentic coding.
Ironically, this comment of mine from a few days ago touches on a lot of the points from this article and was flagged to death:
But writing good DSLs requires one to be great at going up the ladder of abstraction. Not that LLMs can't do it, but they stuggle and choose the path of least resistance. That's easier even if it results in more code.
Readability for yourself. I find the biggest issue with lots of LLM assisted development is that the agent harness can generate code faster than I can read it. I'm the bottleneck.
There's a "zen" moment felt by folks who've written lots of macros (experienced in Lisps and Forths) where, when designed properly, you really feel like you've "grown" a language and have really walked up the abstraction ladder. My thesis is that macro heavy code when the author designs the macros well are very readable. That an agent's output when stacked upon macros can be a lot simpler to read and understand than in languages where the syntax is less fungible. And if you leave a project for a while and come back, an agent is a perfect tool to help you read your macros and familiarize yourself with the abstraction surface again.
> I dont really understand what macros get you when llms exist since a llm doesnt really need to create dsls to get work done
Pithiness. A human might need to be able to read and understand the code. If the code is much longer than it could have been, it will take much longer to read and understand it.
I mean its cool but what about all of the extra infra I need to write because support for the lang is not as extensive and so more code I need to maintain?
> In Common Lisp your program won’t crash, it’ll stop and open a debugger with the whole stack and all the variables. You can just point your LLM at the debugger, and it’ll make its fix and resume the program.
> To my knowledge Common Lisp is the only mainstream language that does all of this.
Sounds like someone who has never used C# and Visual Studio? Even JavaScript is capable of doing this, honestly JavaScript might be the one language with the richest developer tooling of all time (possibly?), sad to say because there's nicer to work with languages out there.
I think what is referred to here is that an exception doesn't unwind the stack. Even if the handler it is uncaught, you can fix the line or value and then resume where the stack was.
No, it's catching/editing/recompiling/continuing at the point of crash, also you can change the current line and variables so many crashes are "resettable" manually.
Visual Studio has done this since the 90s for C++ code (and C# code for a good while also, just most people have debugger exception catching turned off).
How I know? Back in 2000-2001 we were working on a Dreamcast game with a 3dfx/Glide PC renderer, that 3dfx driver on WinNT was unstable and crashed the machine every 3rd-5th start of the game so being able to edit-continue was a damn lifesaver since a lot of common tweaking of custom stuff outside of what connected to the scripting system could be done at runtime.
This is one thing I think so makes people loyal to MSVC despite it having been behind on standards (gonna be interesting to see if they get reflection up to speed for the 2027 release, GCC's there but not yet Clang).
You continue from where the exception lands, but not from the origin of the expection. The .NET runtime for example does termination handling and it's in the spec if I remember correctly.
Common Lisp has a resumption model.
The leave instruction is required to exit a try, catch, or a filter block. At least last time I read the spec (which is a while ago, but still).
No, the debugger (when using Visual Studio proper) breaks at the point of throwing (be it an explicit throw or something underlying like a NPE or out-of-bounds access), _I_ literally use this _all_ the time to fix code while in motion (set next statement on a line a bit back or even the same line to rewind just a tiny bit, change the code, fix errant variables, press F5 to continue and the IDE will rebuild and reposition instruction pointers and update stacks).
What the spec says about execution semantics of exceptions is one thing, what capabilities the debugger provides can be something entirely different.
Perhaps VSCode or some other environments don't have as good debuggers, but trust me when I say that it allows you do do exactly what you need.
Someone saying something in a comment doesn't negate a capability that person doesn't know about.
I've had some of the same ideas, I would love a full macro system on a dependently typed language. My stab at it earlier in January was not quite the right thing, but I do think this is the way. Weirdly enough concision is even more important in LLM context than in regular human context.
One thing that has surprised me is how good LLMs are writing Lisp macros. All the annoying boilerplate and backtick-comma arithmetic that has ground my gears over the years, they churn out cheerfully. I still review it to make sure they haven't done it in a needlessly complicated way, but I'm starting to wonder if that is just a superstitious anachronism.
I shouldn't have been surprised, because that part of macro writing is purely-mechanical syntax transformations—it's just a "take these tokens and return those tokens" function that one would expect LLMs to be good at. But I was surprised, because I always thought of that as the hard part.
So now I can think up new macros to abstract over patterns in my code—the part of macro-writing that is actually enjoyable—and push a magic "implement this" button to just make it work.
You might be interested in Lean. It's my favorite lisp, even though it's really not at all a lisp. It's a nice programming language, and it lets you really hack on commands, macros, syntax, and elaboration.
It's often suggested that Lean is a general programming language but I don't see typical libraries to do basic stuff in it, like an arg parser, web server, GUI, SQL integrations etc.
Can lean generate small static binaries the same way Go/Rust/C/C++ can?
Lean compiles its code to C, so it's pretty good on that end of the spectrum, though it does have a bit of a runtime, so it's certainly not at the very end of spectrum where Rust/C/C++ are (never used Go, so idk). With some optimization my personal experience is that it's not that bad to get it within a factor of 2-5 of half-decent rust speed. My personal mental model of it is like a more-to-my-tastes wildly-faster python but without an ecosystem.
As for library support, yeah it's definitely not a mature ecosystem. But since it compiles to C, the FFI is great, so you can just ask an LLM to write bindings to your favorite rust/C/C++ library and they do just fine. I recently wanted (apache) arrow bindings for lean, and LLMs knocked out perfectly sufficient bindings just fine.
Lisp isn't the best language for every problem; it's a language for making better languages for particular problems.
So Lisp is better than itself.
If the extension improves something without making anything else worse under the chosen criteria, that's a Pareto improvement. Otherwise, it's a tradeoff.
Managed vs unmanaged is a completely different level of performance.
CL is very competitive with popular managed languages like C#, Go, Java, etc despite those all getting tens to hundreds of millions in investment while SBCL gets basically nothing (imagine what it could do with serious investment).
In my experience, LLMs instantly find the reason for the crash and fix. They don't even need to go through the debugging phase anymore. It doesn't matter if you're generating c++ lisp or cobol.
While the open source library ecosystem for CL pales in comparison to Python's or JS's or Java's, it's not like there's nothing. It really depends on what you're doing, but many sorts of common tasks have a library to help. https://github.com/CodyReichert/awesome-cl
Plus, it's straightforward to make use of libraries with a C API, there are a few slower ways to call out to Python libraries, and there's a whole CL implementation built on the JVM if you really need Java libraries...
I think half the article talks about why CL is good because it can resume from an exception, and half the article talks about why DSLs are good.
I think others have pointed out that most modern scripting languages can halt at exceptions without unwinding the stack. Python & Node both support this with core tooling.
As for DSLs, they constrain the LLM which generally helps with code quality. However why implement your DSL in the unconstrained chaos of CL? You can write DSLs in Rust which gives you static typing, a borrow checker, and clippy.
What’s missing in most languages is truly native hot reload. Python, for all its dynamicism and being lisp for dictionaries, surprisingly can’t really do it. Maybe node can, but as far as I can tell typescript can’t and I’m not letting the shoggoths touch plain js.
I've never understood this. You can't just combine the state of an old program with new code, without lots of weird things potentially happening. The algorithms and all intermediate products would have to match, at every step of the programs.
That’s why working with lisp feels like touching alien tech - it works, it’s designed for that exact workflow.
(It’s also essentially pointless when your unit of deployment is a disposable container and not a long running lisp system, but it at least makes development a bit nicer)
Almost nobody programs in functional style in Common Lisp, even though it’s possible. In the vast majority of CL programs there’s tons of global state and yet one learns quickly how to architect and structure the program in order to make image based development and runtime code modification a breeze.
So no, the magic sauce is not “functional”, it’s that CL stemming from the Lisp Machine paradigm is designed for that sort of experience whereas something like Python very much isn’t. And it shows (there’s no image based development in Python that doesn’t end up creating more problems than it solves but image based development is not only the norm in CL but also tremendously empowering).
Scratch is probably the most used programming language with hot reloading, allowed for by its persistent global state.
Web development supports hot reloading modules, but this is effectively just dynamic imports and relies on the app being written for a library/framework that encourages pure code; it doesn’t kill the old module.
I keep bouncing the idea around of building a core in C or Rust then writing everything atop it using Janet. I love how readable lisps are (with macros, you can really move up the abstraction ladder quickly) and working on a live image should make iteration a breeze in an LLM.
I built a OCaml core with a webserver - then made my scripting language on top a custom scheme. I love your idea! I think you would find that a solid interpreted language living in a webserver is an extremely useful think to have. Let me know if you build it!
What did you write with your webserver? Hosting a personal blog? I'm thinking of a good usecase for this. I'm thinking about a game of some sort but not sure yet.
I've just been dogfooding whatever I can to build it out.
Webpages, zettelkasten, todo, workout, diet tracker. It's a web framework! Creating an endpoint is just like making a function in emacs.
Since lisp is homoiconic the AST is just raw JSON. I save the AST in JSON stores in Postgres. But you can clone the AST down and then eval against a local copy of the REPl. So you get local eval for free.
Of course you have to rebuild git to manage a branching REPL in this way.
That works great. The compiler language doesn't matter that much but your language needs a 'runtime' and that is much less annoying to write in C (possibly with bits of assembly, depending on what you're doing) than almost anything else. To a good approximation, your 'compiler' or 'interpreter' amounts to mapping your real language onto a sequence of function calls backed by the 'runtime'.
Lisp allows for some really, really complex and spaghetti code bases. I have seen the ultimate macro-hell from top to bottom. I know syntax does not matter, but i just cant honestly say i read lisp code as clear as something like Go. I guess it boils down to style. I have seen 100 lisp styles, and only one Go style.
Lisp is a sharp tool and I think it struggles to scale with large organisations. You would need incredible tech leadership and a very considered org structure and architecture for it to work.
Culturally, I feel a place like Valve could make it work but you could turn around and ask why does an org need to be in service to and arrange itself around a tool.
I recently wrote a comment on another thread that I think fits even better here:
In my circles I've noticed it's very easy for us to rationalize why our previous favorite language is also the perfect language for the agent era.
If your favorite language before was Python, why, LLMs are fluent in it! So much training data! So many libraries! Home of machine learning! None of that pesky compile time, agents don't need compile time safety anyway, they write such good test coverage! It's The Perfect Agentic Coding Language.
If it was Rust, by jove, an agent can easily handle the headache of satisfying the borrow checker, and now you get the best of all worlds! Safety! Near-C runtime performance! Abstractions! The only reason people didn't use Rust before was it was Too Hard and there were Too Many Furries and now it's not hard and you don't have to interact with them, so get on board. It's The Perfect Agentic Coding Language.
If it was Golang, oh my goodness, what a choice. Pretty fast compile time and pretty fast runtime. Agents get a tight feedback loop with build->run->test->edit. Not very complicated, code has to be written in a straightforward banging-rocks-together way. Good stable ecosystem! Rob Pike designed the language for people he said were "not capable of understanding a brilliant language but we want to use them to build good software." That's an arrogant, demeaning way to describe your colleagues but if they're LLM agents it's dead on! It's The Perfect Agentic Coding Language.
I could go on and on. I'm not immune either! My own favorite language is F# and when I feel like self-justifying, I play the same game:
It has access to the .NET ecosystem like C#, but I don't have to constantly remind the agents to prefer a style with immutable data and pure functions, they idiomatically do that in F#. Files have to be in order and can only refer to symbols defined "earlier" in order, if you want mutually-referential types or functions they have to be declared as such in a joint statement, so spaghetti is hard to create: each project's codebase naturally ends up in a layered bottom-to-top architecture. The language is terse enough to be token efficient, without being symbol soup. FSX scripts can be generated during agentic code reviews to demonstrate repros for discovered issues. If there's any type of code that still warrants me jumping in and writing some myself, that code would be data type definitions/domain modelling, and F# is a joy to write those in. It's The Perfect Agentic Coding Language.
But no seriously F# might actually be the perfect agentic coding language. I had the good fortune of learning OCaml a few years ago as the models were getting good. Had a blast.
I think Lisp and an ML are kind of the dynamic (static) duo. I still think MLs are the best for complex projects, but interpreted languages are pretty neat too. Python, Rust, and Golang are all great, also.
I think a tree calculus language might be the actual best - but it's too soon to say
So now we're believing it's great that LLM's can write macros, thus further obfuscating human maintenance of the code-base, because LLM's can understand and implement it than better than humans.
This isn't the future I wanted, and is why I advocate for trade schools these days.
I miss the days I could listen to music and use my adhd/autism to its fullest potential reading documentation and figuring things out myself.
Of course, these are same people you'd want managing these AI agents in the first place, but reviewing code written by others always kind of sucks, especially when it's assumed the language model knows better than you do which isn't often the case!
Was the PRD perfected on the requirements? Only God knows, and I personally want to be there when it's written.
I must be proving your point because I'm a rust head and the argument you gave for rust just seems like the best one haha. And if I had the choice between two vibe-coded pieces of software, and one was written in rust and the other was written in python, I would far prefer to use the rust one.
Why, you are right on all of these. I used Rust a lot before AI, and I use it a lot after. But I recognise the shortcoming of long compilation: it doesn't just shorten how many times I can have feedback in one day, it is also difficult to fit compilation into constrained environments. Like when an ultra secure Kubernetes cluster doesn't let me compile source code anywhere near it, and I have to compile inside of it; my colleagues insistence on using Go here because "Rust is difficult" (it surely is 100% longer to type when prompting an LLM) really has the main benefit that this code compiles fast and easily within the cluster.
So picking languages for their compilation and especially runtime properties is key.
What I've learned, though, is that languages with reckless error handling produce more errors at runtime. My Rust programs just don't crash because I don't need it to be explicit about reaching a "total" approach.
If you're in a C# environment, I see a case for F#. And if you want fast but more solid than Python, why not Mojo? Although who reads code.
This article convinces me. But I suspect like my lack of domain knowledge of Lisp makes me spend time learning the runtime: how and when can I switch to JIT, what's the async story, how does the harness become part of the Lisp program, etc.
Yeah... no. I don't think there is a best programming language, but I do think dynamically typed languages are worse for AI to code in compared to statically typed ones.
This post reads like it was written by a Common Lisp fan who is looking for a reasons to say that Common Lisp is good for AI agents, rather than an AI agent user evaluating what languages are really best.
I've heard fans of both statically typed and dynamically typed languages advocate for their language. The static type fans say that the rigorous compilation process gives the LLM a fast iteration loop with clear messages from the compiler on what invariants aren't being upheld. The dynamically typed languages advocates talk about fewer tokens, the popularity of the language in the training data, and so forth. Guess what, these are the exact same arguments these communities made for human developers.
Personally, I don't know the answer. The industry has swung back and forth on this over the years, but before AI code-gen static languages were on the upswing for a variety of reasons, including runtime efficiency and much better ergonomics thanks to modern type inference engines. I suspect those reasons are still valid.
It seems like an assumption here is that any language will be equally performant at runtime, for a given number of tokens burned to produce the code. I have never used any functional language for anything real (except maybe Mathematica). Is that true?
Common Lisp is not a functional language. You can write functional-paradigm code in CL but it will happily let you write old-school imperative style, OOP, etc.
The de facto open source implementation, SBCL, has a solid compiler and garbage collector. It produces fast code.
SBCL can produce very fast code. It's really slept on from that point of view.
A couple of years back there was a paper doing the rounds about software execution efficiency, which a lot of people in my neck of the woods got interested by because of the potential for environmental impact at the sort of scale we operate at. SBCL ranked disturbingly highly for something that is culturally never going to happen for us.
Bad performance is much less excusable these days (https://danluu.com/perf-opt/) and usually it's more the result of architecture and data structure choice than presence/absence of things out of the box of some language (though in the case of C++, it seems that everyone who gets obsessed with performance just says to avoid the included stdlib no matter the compiler; so much for that box). Your question seems to me to betray an assumption that Common Lisp is particularly functional (and overall suffers from some sort of functional programming performance trade-offs), when it's not. Common Lisp is unopinionated, multi-paradigm, but at its core is mutation-friendly, compilation friendly (compile is a function available to call at runtime), and object-oriented (CLOS being the first ANSI standardized OOP system, it's more flexible than most OO systems and methods do not belong to classes). There are also many implementations of Common Lisp, including two long-standing commercial ones, so should performance (whether raw throughput, or memory size, or whatever your metrics) not quite be to your liking out of the box with one, you might consider another before giving up and choosing an entirely different language or spending time/tokens trying to better optimize your existing code. That said, SBCL is probably the most popular, and has a pretty sophisticated optimizing compiler while allowing you to specify your own assembly ops without actually having to write separate assembly files. See for instance https://www.stylewarning.com/posts/nbody/
365 comments
[ 1.5 ms ] story [ 23.1 ms ] threadDSL presumes agreement on semantics, and that's often the most difficult part.
- economies of scale no longer work, and you end up doing a custom ERP for your business from scratch.
- your business changes might invalidate your model quickly. You sell through distributors, but open an online shop -- and suddenly your customer is not one of few dozen well-known businesses with a known address and tax number, but user2252 who bought something late at night last night. And you want to understand the needs and behaviors of both.
- for the economies of scale, you might develop your custom solution 20x faster now, but you're still in competition with the established provides with templates for most of the cases (who btw have the same LLM capability at their disposal)
- for the change in business -- you can ask an LLM which changes this induces, and it will give you most typical impacts. And then you're back at square deciding if it's better to roll your custom DSL and the custom system downstream, or just use off-the-shelf stuff that covers 95% of it from day one (well, maybe day two or three)
When it comes to back office business programming, there’s just a lot of code tasked with copying a litany of bits of data from one structure to another.
Whether it’s copying a web form into a database, or converting Their JSON to Your JSON, it’s a lot of detail that does not abstract well. It’s all shapes and sizes and formats, and it almost always has to be enumerated in excruciating detail and, typically, twice.
Sure, there’s logic and whatnot involved, but it, too, is specific to some subdomain of the larger system and it, too, does not abstract well. Not in the large context of the overall system.
Accounts Payable and Accounts Receivable, at 10,000 feet look almost identical. They’re almost literally the same thing with the sign flipped. But in practice, they don’t share code well. You end up with two similar systems, but not similar enough where sharing is actually worthwhile.
At best they can leverage a common API to the GL.
Turns out a lot of languages can manifest a decent level of abstraction. But even then, folks push back.
Consider the love/hate relationship with ORMs. Or the annotation driven markup in Java programs and the underlying “magic” that they enable. Like scribing mystic runes onto things.
Those are both very powerful, yet folks experience that and toss their hands in the air and throw out the baby with the bath water and jump into something “magic free” like Go.
Just because you can use something like CL to “make your own magic”, doesn’t mean it’s a good idea. Doesn’t mean it scales. Doesn’t mean it communicates well to others. AI or no.
It’s not the AIs world yet. We already know that if the AIs want a better language suited to AI efficiency, they’ll come up with their own. I’ve already seen crass examples of “code only an AI could love”. Completely impenetrable, at least to me. May as well have represented it as a color image and collection of RGB values. Opaque to me, but the AI could “read” it.
There is much more to programming and systems than token density, and AI is still getting cheaper by the day, so less reason to even pre-optimize for it anyway.
Where I'm coming from: I have never used Common Lisp, closest was Erlang for a ~3 year project. I understood the appeal of functional programming, just didn't like it.
https://comp-348.github.io/lisp-debugging.html has an example of what it looks like. A toy example, to be sure, but the basics of a real example would still look the same. You're given a choice of several options, very reminiscent of the "Abort, Retry, Ignore?" choice that used to be oh-so-familiar in the days of DOS. Except this one is more useful, because it offers ways to specify how to resume. E.g., the toy project is halting on a `(print X)` call where the value of X is not defined. And the choices are:
0. Continue. (Retry using X).
In the toy example, this would fail, because nothing else has defined X. But in real code, the name might have been undefined because the data needed to define it hadn't arrived yet, from the database or the filesystem. In which case retrying the statement might work the second time.
1. Use-value. (Use specified value).
This one prompts you to enter a value for the undefined variable, and continues, but it does not modify the value of X in the program. The next time the program tries to use X, it will halt again with another "unbound variable" error.
2. Store-value. (Set specified value and use it).
This one, just like Use-value, will prompt you to enter a value to use... but then it will set X to that value and continue running the program. Next time the program tries to read the value of X, it will have one, and the program won't halt.
3. Abort. (Exit debugger, returning to top level).
This is what you would choose if there's no good way to fix the error, and you just have to quit the program and restart. Though note that choosing this option isn't going to exit the program you're debugging, just take you out of the debugger. You'll still need to kill-and-restart it some other way... or come back an hour later when the value is finally available, and then choose options 1 or 2.
Hopefully that gives you a taste for what the CL debugger is like to use in practice.
So, in practice, it might look like you hit any kind of runtime failure, and then the LLM writes some code to fix it, and the user's request completes successfully with no errors.
1. Have you had much success w/moar macros in the age of the LLM? I've been impressed by the models' ability to write good ones, but I can tell my taste/judgement for macros isn't quite there. But they tend to be pretty good at writing gnarly ones, and I would love to work more macros into my workflow. Would love your thoughts. (Have I taken "Simple Made Easy" too far and left macro value on the table?)
2. Do your models ever get confused with image-based dev, and state? It's seemed dumb to me to have models keep running `sed` to change files, but it is nice to have a human-readable, filesystem-backed record of definitions. Would love to hear your experience here.
I've seen a big improvement in LLMs writing macros since Opus 5.5 came out. What really helps I think is that I've written skill files with my own examples and instructions.
Same thing for image-based dev. without an agent.md file with good instructions on how to work with a live image it will do dumb things. this sort of workflow is jsut so far off the training distribution.
I think what changed recently isn't that LLMs got better at CL, they just got way better at taking my skill/agent.md files and reasoning through them.
There shall be one minimal, ultra-hardened, tiny attack surface, "majority gate" picking the answers that most implementation agrees on.
This shall not only detect a great many implementation issues but also it'll help find security issues and platform defects (say the Common Lisp, Haskell, Rust and Python all agree but the Java one fails: in rare case it'll be due to a JVM bug and finding that out shall be simplified).
Code shall be generated from specs in n languages and ran on n stacks. The gate shall return the answer as soon as a quorum is met and, later on, any bogus answer arriving shall be cause for enquiry.
We'll have such systems, it's just a matter of time.
I'm the author and I share the same philosophy, hahaha, my users told me about this post
I dont really understand what macros get you when llms exist since a llm doesnt really need to create dsls to get work done
Let's way you wanted to do a web application using LLMs. Using a web framework would cost less tokens than using the vanilla underlying language (Python, PHP, whatever..), which is again way less tokens than building up from assembly (an LLM should be able to do this given enough time and compute).
https://news.ycombinator.com/item?id=21232352 (Oct 2019)
https://news.ycombinator.com/item?id=4766191 (Nov 2012)
https://news.ycombinator.com/item?id=694700 (July 2009)
Edit: ok, this is what I get for not reading the article:
> For LLMs, less code means fewer tokens, and tokens are what you pay for so you spend less on development
Ironically, this comment of mine from a few days ago touches on a lot of the points from this article and was flagged to death:
https://news.ycombinator.com/item?id=49924013
Note also that WebAssembly's text format is S-expression based. For conciseness I presume?
There's a "zen" moment felt by folks who've written lots of macros (experienced in Lisps and Forths) where, when designed properly, you really feel like you've "grown" a language and have really walked up the abstraction ladder. My thesis is that macro heavy code when the author designs the macros well are very readable. That an agent's output when stacked upon macros can be a lot simpler to read and understand than in languages where the syntax is less fungible. And if you leave a project for a while and come back, an agent is a perfect tool to help you read your macros and familiarize yourself with the abstraction surface again.
Just a theory though.
Pithiness. A human might need to be able to read and understand the code. If the code is much longer than it could have been, it will take much longer to read and understand it.
> To my knowledge Common Lisp is the only mainstream language that does all of this.
Sounds like someone who has never used C# and Visual Studio? Even JavaScript is capable of doing this, honestly JavaScript might be the one language with the richest developer tooling of all time (possibly?), sad to say because there's nicer to work with languages out there.
Visual Studio has done this since the 90s for C++ code (and C# code for a good while also, just most people have debugger exception catching turned off).
How I know? Back in 2000-2001 we were working on a Dreamcast game with a 3dfx/Glide PC renderer, that 3dfx driver on WinNT was unstable and crashed the machine every 3rd-5th start of the game so being able to edit-continue was a damn lifesaver since a lot of common tweaking of custom stuff outside of what connected to the scripting system could be done at runtime.
This is one thing I think so makes people loyal to MSVC despite it having been behind on standards (gonna be interesting to see if they get reflection up to speed for the 2027 release, GCC's there but not yet Clang).
The leave instruction is required to exit a try, catch, or a filter block. At least last time I read the spec (which is a while ago, but still).
See https://news.ycombinator.com/item?id=23844283 for examples on CL.
No, the debugger (when using Visual Studio proper) breaks at the point of throwing (be it an explicit throw or something underlying like a NPE or out-of-bounds access), _I_ literally use this _all_ the time to fix code while in motion (set next statement on a line a bit back or even the same line to rewind just a tiny bit, change the code, fix errant variables, press F5 to continue and the IDE will rebuild and reposition instruction pointers and update stacks).
What the spec says about execution semantics of exceptions is one thing, what capabilities the debugger provides can be something entirely different.
Perhaps VSCode or some other environments don't have as good debuggers, but trust me when I say that it allows you do do exactly what you need.
Someone saying something in a comment doesn't negate a capability that person doesn't know about.
I don't see it print the "here" line in their example.
I shouldn't have been surprised, because that part of macro writing is purely-mechanical syntax transformations—it's just a "take these tokens and return those tokens" function that one would expect LLMs to be good at. But I was surprised, because I always thought of that as the hard part.
So now I can think up new macros to abstract over patterns in my code—the part of macro-writing that is actually enjoyable—and push a magic "implement this" button to just make it work.
Can lean generate small static binaries the same way Go/Rust/C/C++ can?
How practical is rewriting, say, grep in Lean?
> Can lean generate small static binaries the same way Go/Rust/C/C++ can?
Lean compiles to C and the binaries aren't huge, though haven't benchmarked this part yet.
> How practical is rewriting, say, grep in Lean?
Very, you should probably try it.
As for library support, yeah it's definitely not a mature ecosystem. But since it compiles to C, the FFI is great, so you can just ask an LLM to write bindings to your favorite rust/C/C++ library and they do just fine. I recently wanted (apache) arrow bindings for lean, and LLMs knocked out perfectly sufficient bindings just fine.
False. A partial ordering doesn’t guarantee a maximum element!
So Lisp is better than itself.
If the extension improves something without making anything else worse under the chosen criteria, that's a Pareto improvement. Otherwise, it's a tradeoff.
https://en.wikipedia.org/wiki/Pareto_efficiency
There’s the right tool for the job, there’s compliance with requirements, there’s personal preference.
Don’t let anyone ever tell you you are programming wrong.
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
But I find ideas like Carp very attractive.
Still common lisp is designed very good.
CL is very competitive with popular managed languages like C#, Go, Java, etc despite those all getting tens to hundreds of millions in investment while SBCL gets basically nothing (imagine what it could do with serious investment).
The models learned to code, the actual language is just a tiny adapter on top.
I think others have pointed out that most modern scripting languages can halt at exceptions without unwinding the stack. Python & Node both support this with core tooling.
As for DSLs, they constrain the LLM which generally helps with code quality. However why implement your DSL in the unconstrained chaos of CL? You can write DSLs in Rust which gives you static typing, a borrow checker, and clippy.
(It’s also essentially pointless when your unit of deployment is a disposable container and not a long running lisp system, but it at least makes development a bit nicer)
This is where the functional programming model really shines - there typically isn't a bunch of hidden state floating around to break hot-reloading
So no, the magic sauce is not “functional”, it’s that CL stemming from the Lisp Machine paradigm is designed for that sort of experience whereas something like Python very much isn’t. And it shows (there’s no image based development in Python that doesn’t end up creating more problems than it solves but image based development is not only the norm in CL but also tremendously empowering).
Are you sure this isn't just doing functional programming, and not realizing it?
It works fine and was designed for this use case.
Web development supports hot reloading modules, but this is effectively just dynamic imports and relies on the app being written for a library/framework that encourages pure code; it doesn’t kill the old module.
It also makes your agent wait for the compiler to finish.
Webpages, zettelkasten, todo, workout, diet tracker. It's a web framework! Creating an endpoint is just like making a function in emacs.
Since lisp is homoiconic the AST is just raw JSON. I save the AST in JSON stores in Postgres. But you can clone the AST down and then eval against a local copy of the REPl. So you get local eval for free.
Of course you have to rebuild git to manage a branching REPL in this way.
Folks have found it useful to just point gippity at the page and ask questions
Janet for orchestration, C for the frame loop.
[1] https://github.com/jennystats/janet-num
Astronaut 2: Always has been...
Culturally, I feel a place like Valve could make it work but you could turn around and ask why does an org need to be in service to and arrange itself around a tool.
In my circles I've noticed it's very easy for us to rationalize why our previous favorite language is also the perfect language for the agent era.
If your favorite language before was Python, why, LLMs are fluent in it! So much training data! So many libraries! Home of machine learning! None of that pesky compile time, agents don't need compile time safety anyway, they write such good test coverage! It's The Perfect Agentic Coding Language.
If it was Rust, by jove, an agent can easily handle the headache of satisfying the borrow checker, and now you get the best of all worlds! Safety! Near-C runtime performance! Abstractions! The only reason people didn't use Rust before was it was Too Hard and there were Too Many Furries and now it's not hard and you don't have to interact with them, so get on board. It's The Perfect Agentic Coding Language.
If it was Golang, oh my goodness, what a choice. Pretty fast compile time and pretty fast runtime. Agents get a tight feedback loop with build->run->test->edit. Not very complicated, code has to be written in a straightforward banging-rocks-together way. Good stable ecosystem! Rob Pike designed the language for people he said were "not capable of understanding a brilliant language but we want to use them to build good software." That's an arrogant, demeaning way to describe your colleagues but if they're LLM agents it's dead on! It's The Perfect Agentic Coding Language.
I could go on and on. I'm not immune either! My own favorite language is F# and when I feel like self-justifying, I play the same game:
It has access to the .NET ecosystem like C#, but I don't have to constantly remind the agents to prefer a style with immutable data and pure functions, they idiomatically do that in F#. Files have to be in order and can only refer to symbols defined "earlier" in order, if you want mutually-referential types or functions they have to be declared as such in a joint statement, so spaghetti is hard to create: each project's codebase naturally ends up in a layered bottom-to-top architecture. The language is terse enough to be token efficient, without being symbol soup. FSX scripts can be generated during agentic code reviews to demonstrate repros for discovered issues. If there's any type of code that still warrants me jumping in and writing some myself, that code would be data type definitions/domain modelling, and F# is a joy to write those in. It's The Perfect Agentic Coding Language.
I think Lisp and an ML are kind of the dynamic (static) duo. I still think MLs are the best for complex projects, but interpreted languages are pretty neat too. Python, Rust, and Golang are all great, also.
I think a tree calculus language might be the actual best - but it's too soon to say
Really, languages are great for LLMs.
This isn't the future I wanted, and is why I advocate for trade schools these days.
I miss the days I could listen to music and use my adhd/autism to its fullest potential reading documentation and figuring things out myself.
Of course, these are same people you'd want managing these AI agents in the first place, but reviewing code written by others always kind of sucks, especially when it's assumed the language model knows better than you do which isn't often the case!
Was the PRD perfected on the requirements? Only God knows, and I personally want to be there when it's written.
So picking languages for their compilation and especially runtime properties is key.
What I've learned, though, is that languages with reckless error handling produce more errors at runtime. My Rust programs just don't crash because I don't need it to be explicit about reaching a "total" approach.
If you're in a C# environment, I see a case for F#. And if you want fast but more solid than Python, why not Mojo? Although who reads code.
This article convinces me. But I suspect like my lack of domain knowledge of Lisp makes me spend time learning the runtime: how and when can I switch to JIT, what's the async story, how does the harness become part of the Lisp program, etc.
It's probably the best language for AI coding though. By far, as far as I can tell.
I've heard fans of both statically typed and dynamically typed languages advocate for their language. The static type fans say that the rigorous compilation process gives the LLM a fast iteration loop with clear messages from the compiler on what invariants aren't being upheld. The dynamically typed languages advocates talk about fewer tokens, the popularity of the language in the training data, and so forth. Guess what, these are the exact same arguments these communities made for human developers.
Personally, I don't know the answer. The industry has swung back and forth on this over the years, but before AI code-gen static languages were on the upswing for a variety of reasons, including runtime efficiency and much better ergonomics thanks to modern type inference engines. I suspect those reasons are still valid.
The de facto open source implementation, SBCL, has a solid compiler and garbage collector. It produces fast code.
A couple of years back there was a paper doing the rounds about software execution efficiency, which a lot of people in my neck of the woods got interested by because of the potential for environmental impact at the sort of scale we operate at. SBCL ranked disturbingly highly for something that is culturally never going to happen for us.