I prefer letting agents write code
I think this is one of the divides between people who like AI and people who don't: I love programming in the sense of thinking through algorithms, architectures, data structures, data flow, concurrency, control flow, parallelism, time and space complexity, error handling, tests — but I don't care for writing code. I've used Vim (Evil mode) and then Emacs (god-mode) for years, and I spent a lot of time trying to learn how to use their most advanced features in an attempt to make text editing easier, so it's not like I'm using bad tools. It's just too fiddly and busywork.
It's not speed, it's motivation and cognitive friction
It really isn't, for me, even mostly about the inconvenience of the typing aspect of writing the initial code.
It isn't a bottleneck in terms of speed or "effort" in an absolute sense; it's a bottleneck in terms of motivation. It's about the inconvenience of:
- trying to munge text from one state to another (the ability to have agents shift and rearrange code in large "code actions," focused on the output I want to achieve instead of getting into the weeds on the details of how to move characters around to get there, is just far too good to turn down.)
- handling big refactors that require a lot of little mostly rote changes in a lot of places
- dealing with APIs or libraries where I don't want to have to constantly remind myself what functions to use, what to pass as arguments, what config data I need to construct to pass in
- spending hours trawling through docs to figure out how to do something with a library when I can just feed its source code directly to an LLM and have it figure it out and report back, and explain concepts to me, then implement my plan in light of them
And a lot of other things like that: there's a lot of unnecessary friction to writing code beyond just typing but below the level of actually solving the relevant problems, that nevertheless has nothing to do with having come up with a wrong abstraction and trapping yourself in some boilerplate, or using a "bad language."
Conversely, I also really enjoy taking something that isn't quite good enough, that's maybe 80% of the way there, and doing the careful polishing and refactoring necessary to get it to 100%. I love refactoring, clearing things up, adding types, error handling, filling out features. Getting to that first 80% is often the most grueling and annoying part for me; not in terms of planning how I'm going to do it — that's where a lot of the novelty of any project comes from — but the in-the-weeds execution where it's just me forgetting a lot of little things over and over, fixing minor bugs, fighting with the type checker (or, worse, the lack thereof!) etc.
This combines with the fact that reading code is pretty easy for me — in fact, I often spot errors, problems, or omissions sooner when I'm reading code back then when writing it, it just used to be hard to convince myself to read any of my code — to mean that dealing with an agent's final output diffs feels like a reward. Additionally, I often feel like, once I've designed an architecture, I get a better sense for the high level, for how everything fits together and flows, by reading through the code a few times, than by implementing my design, because when I'm implementing my design I'm only looking at one tiny piece of the puzzle at a time, and every time I move to a new thing, the old thing gets completely GC'd from memory.
I'm also very good at quickly putting algorithms and architectures I have in my head into words, and I'm a very fast prose typist, so prompting has incredibly low friction for me; moreover, to be honest, I often find writing out what I'm thinking in English prose clarifies the high level idea and whether it makes sense at all far more than just diving into the middle and writing the code for it; I've found a lot of fundamental architectural bugs and infeasabilities before I ran face first into them thanks to agentic coding that I would've had to pancake into while writing code by hand.
Language is not a panacea to this
You could say that LLMs are only felt to be helpful to deal with boilerplate and annoying text-munging because we're all programming in Blub, and if we all saw the light of the true language, we wouldn't need all these gross language models.
The problem with Lisp as a solution to boilerplate is that, first, there is always boilerplate scaffolding to do, even with the most macrotastic Lisp — a Lisp macro DSL is not going to help with automating refactors, automatically iterating to take care of small compiler issues or minor bugs without your involvement so you can focus on the overall goal, remembering or discovering specific library APIs or syntax, etc.; second, you actually have to write that high level DSL to get Lisp to look "as high level as a prompt" — which then I'd want to use AI for, to write that initial boilerplate, from a high level description of what the DSL should do — and most DSLs are not going to be able to be as concise and abstract as a natural language description of what you want, nor as easy to debug and verify when they go wrong, since they're expanding to much dirtier lower level code, instead of their "expansion" just being first-class language source code. let's be real, dedicating an entire language feature, and switching your entire language, just to save typing and code editing, despite the significant tradeoffs in clarity, debuggability, and so on that macros bring, actually begins to look a bit silly; and third, that Lisp itself is not really the best language (in terms of ecosystem, toolchain, runtime, performance) for many or most tasks someone like me might want to do, and languages adapted to the runtime and performance constraints of their domain may be more verbose.
While I find Common Lisp and to a lesser degree Scheme to be fascinating languages, the state of the library ecosystem, documentation, toolchain, and IDEs around them just aren't satisfactory to me, and they don't seem really well adapted to the things I want to do. And yeah, I could spend my time optimizing Common Lisp with `declare`s and doing C-FFI with it, massaging it to do what I want, that's not what I want to spend my time doing. I want to actually finish writing tools that are useful to me.
Likewise, Lisp's S-expressions and the structural editing they enable are a not an antidote to all our text editing woes. Even if/when I'm using Lisp and have all the best structural editing capabilities at my disposal, I'd still prefer to have an agent do my code-writing for me; I'd just be 30% more likely to jump in and write code myself on occasion. This is ultimately because, even with structural editing, you're still thinking about how to apply this constrained set of operations to manipulate a tree of code to get it to where you want, and then having to go through the grunt work of actually doing that, instead of thinking about what state you want the code to be in directly. You could argue tree-sitter might offer a way out, but I think this is also false.
Which means that, yes, we're using languages that have more boilerplate and scaffolding to do than strictly ideally necessary, which is part of why we like LLMs, but that's just the thing: LLMs give you the boilerplate eliminating benefits of Lisp without having to give up the massive benefits in other areas of whatever other language you wanted to use, and without having to write and debug macro soup and deal with private languages.
Hammock-driven development 2.0
There's also how staying out of the code writing oar wells changes how you think about code as well:
Some programmers like to say that they think using code, while they code; but I think this is generally a mistake, and leads to much worse code, or a lot of after the fact damage control and ad hoc refactoring, which they might not even realize they could've avoided if they'd sat down and thought through the problem holistically from the start. Obviously, you can't plan for every little thing, and there's often a lot of iteration and experimentation that goes into actually implementing a high level specification, both in terms of how the final product looks and works, and the specific way it's implemented, so it's important to recognize the scale at which to write the spec. But still, having a high level understanding of what you're trying to achieve and how to approach it is very important. If you think through a problem as you're writing the code for it, you're going to end up the wrong creek because you'll have been furiously head down rowing the entire time, paying attention to whatever local problem you were solving or whatever piece of syntax or library trivia or compiler satisfaction game you were doing instead of the bigger picture, simply because the human brain doesn't have the space to hold both at once in working memory.
You could argue that code is still the best language for doing this kind of software design and specification, but I don't think that's actually true. The problem is that you actually can't really model or describe a lot of the things that I do with my specifications using code without just ending up fully writing the actual, specific, granular code that does the thing, or near to it — so you'll just end up getting lost in the details again. The whole fundamental benefit of natural language "programming" is that it lets you specify what you want at exactly the level of abstraction you intend, without being limited by the specific level of abstraction a language allows, or mangling the language to represent things in types in a way it's really not intended for in order to get around that. The vast majority of languages are mostly prescriptive (interested in the precise how of achieving things), whereas a natural language spec can be purely descriptive, explaining the what and why, because natural language lets you leave assumptions, ambiguities, and non-specified aspects where you want them, to allow you future freedom in implementation, but specify the important things clearly. Most languages don't have a type system that actually lets you describe the logic and desired behavior of various parts of the system and which functions should call which other functions and what your concurrency model is and so on without just writing the specific code that does it; in fact, I think the only languages that would allow you to do something like that would have to be like dependently typed languages or languages adjacent to formal methods. This is literally what the point of pseudocode and architecture graphs and so on are for.
Obviously, before starting writing, you could sit down and write a software design document that worked out the architecture, the algorithms, the domain model, the concurrency, the data flow, the goals, the steps to achieve it and so on; but the problem with doing that without an agent is then it becomes boring. You've basically laid out a plan ahead of time and now you've just got to execute on the plan, which means (even though you might even fairly often revise the plan as you learn unknown unknowns or iterate on the design) that you've kind of sucked all the fun and discovery out of the code writing process. And it sort of means that you've essentially implemented the whole thing twice.
Meanwhile, with a coding agent, you can spend all the time you like building up that initial software design document, or specification, and then you can have it implement that. Basically, you can spend all the time in your hammock thinking through things and looking ahead, but then have that immediately directly translated into pull requests you can accept or iterate on instead of then having to do an intermediate step that repeats the effort of the hammock time. It basically means that, if, for you as it is for me and Paul Graham, writing is thinking, then agents let you transform your process of iteratively thinking through a problem, expressing it at just the right level of abstraction to make progress, directly into code diffs you can iterate on.
Of course, writing this specification up front without knowing the affordances of the technologies you plan to use, and building prototypes, can often be a bad idea; however, LLMs also make this easier: as I mentioned above, you can feed entire codebases of libraries and frameworks to LLMs and have them assess the feasibility of doing a given thing with them, or give you a look at the concepts and features, for you, and then integrate that knowledge into the spec; likewise, you can use vibe coding as a way to follow a line of flight, exploring various prototypes before banging out a specification.