When AI Writes the Code, Who Preserves the Meaning?
AI does not eliminate programming. It changes where programming happens.
When AI writes more of the code, the human task becomes describing the system with greater precision.
The claim
I hear this frequently:
We don’t need programmers anymore. We have AI now.
The argument usually goes something like this:
I can write the requirements.
I can describe the overall structure.
I can create tests to verify that the AI succeeded.
But then the real question becomes:
How do we write those requirements?
How do we describe the overall structure?
How do we write those tests?
That does not sound like programming disappearing. It sounds like programming moving up a level.
English is not precise enough
Since LLMs understand English, it is tempting to write requirements in English.
But is plain English enough?
Plain English is excellent for intention. It is excellent for context, purpose and explanation. It is how we tell each other why something matters.
But systems are not built only from intention.
Sooner or later, the description needs to become more precise.
What are the concepts?
What data exists, and what can change?
What must never change?
What happens when facts arrive late or contradict each other?
What is a decision, what is derived state, and what is merely presentation?
What can be replayed, and what must be remembered?
At that point, ordinary English starts to become slippery.
The difficult part is not merely describing what the system should do. It is preserving its meaning as the system evolves.
A more precise language
So we need something more precise than plain English.
Not necessarily more implementation detail, and not necessarily more ceremony, but a language structured enough to describe data, behaviour, transformations and rules.
A language where the shape of the system remains visible.
A language where domain concepts can be described directly.
A language where AI can inspect and change structure, not merely rewrite text.
And if such a language exists, is that not a programming language?
Perhaps not your favourite programming language of today. Perhaps not a language designed mainly for implementation. But still a programming language in the deeper sense. One that helps keep the meaning of a system precise while the system changes.
The Python shortcut
This is where I think some of the current Python enthusiasm misses an important distinction.
Python is a wonderful language for exploration, scripting, automation and glue code. I have encouraged people who are not professional programmers to use Python to get things done since the 1990s.
And now that is happening. Non-programmers are getting useful things done in Python with the help of AI.
But there is a difference between a language that is easy for AI to generate and a language that helps humans describe systems in a form AI can preserve.
The question is whether Python is the right language when the human task is to describe concepts, rules, transformations and system shape clearly enough that AI does not lose its grip as the system evolves.
Python is not the problem. The shortcut is confusing ease of generation with suitability for preserving system meaning.
Large systems are about meaning
In a large system, the hard parts are not the lines of code.
The hard parts are the concepts, rules, boundaries, exceptions and history. The data that arrives late. The facts that contradict other facts. The decisions that must still make sense years later.
That is the meaning the system has to preserve.
We need a way to describe the shape of the problem.
Data-oriented programming
To me, that starts with making the important data explicit.
Not database tables first.
Not objects hiding internal state.
Not technical structures that exist mainly because a framework expects them.
Data-oriented programming, as I use the term here, means that simple data structures are used to describe the important parts of both the problem and the solution.
This does not mean that everything is “just data” in a trivial sense. It means that the central concepts of the system are represented as explicit values that can be inspected, tested, stored, compared and transformed.
Facts as data.
Events as data.
Commands as data.
Decisions as data.
Rules made explicit.
Examples that prove the behaviour.
Instead of hiding the meaning of the system inside object graphs, framework lifecycles or scattered mutable state, the important forms are visible.
The program then becomes a set of transformations and interpretations of those forms.
What do we know?
What happened?
What can be derived?
What rules must hold?
What examples prove the behaviour?
This makes the system easier to inspect, test, replay and change.
It also gives AI something more stable than prose to work with. Instead of generating code from a vague wish, it can help preserve and transform a visible model of the problem.
Immutability is closer to reality
This is also where immutability matters.
If data carries the meaning of the system, it matters whether that data can change.
Immutability means that values do not change after they have been created. Once a value exists, you can trust that it will not change behind your back.
Immutability is sometimes presented as a technical preference from functional programmers. I think that undersells it.
Immutability is often closer to reality.
A paid invoice was not “never unpaid”. It was paid.
A cancelled booking was not “never booked”. It was cancelled.
A changed listing was not “always in its latest state”. It changed.
History is not changed by overwriting it. History is changed by adding new facts.
Imperative systems often spread state across many variables in many objects. The state of the system becomes the accidental sum of many moving parts, often hidden deep inside protective layers.
That may sound normal, because it is how many systems are built.
But it also means that there is no single thing you can point to and say: this is the state of the system. The state is distributed across places that can change independently.
Functional programming often takes a different view. State can be represented as one immutable value transformed over time. It may be structurally complex, but it is still treated as a value. The state transition becomes explicit and controlled.
Master of puppets
Object-oriented systems without immutable values can easily become a marionette theatre. The movement is visible, but the strings are not.
When you pass an object reference to a function, you hand someone else another handle to the puppet, with strings attached. The object you pass is not necessarily an isolated thing. It may carry references to other objects, which carry references to other objects, and so on.
You passed a puppet with strings still attached to the rest of the theatre.
What if you passed it to code you do not control?
Did it keep the reference?
Will it call back later?
Will it change the object?
Will it follow one of its references and change something deeper in the system?
Will some part of your system move when you least expect it to?
Encapsulation is often presented as a way to manage complexity. But encapsulated mutable domain state adds a new kind of hidden complexity: the possibility of change through paths you can no longer see. The hidden state can be changed through any path that still holds a reference to it, possibly through code outside your control.
Modern systems are full of code we do not fully control.
Frameworks.
Libraries.
Plugins.
Callbacks.
Generated code.
Code written by other teams.
Code written by AI.
That is normal. It is how much of the world builds software.
When mutable objects are passed into code we do not control, we are no longer only sharing data. We are sharing access to future change.
Those are the hidden strings.
This occurs frequently in systems where references to mutable objects are passed around freely, in languages such as Python, C# or Java.
Functional programming reduces moving parts
Functional programming turns this inside out.
The imperative parts are pushed to the edge of the system: receiving input, calling external systems, storing data and sending output. The core is described in terms of immutable values and transformations between them.
There is a state.
Something happens.
A new state is derived.
That idea is simple, but deeply useful when systems become complex. It makes state easier to reason about, reconstruct, test and verify. You control when and how change enters the system.
Functional programming matters here because it reduces the moving parts.
Instead of sharing access to future change, the program becomes more about transforming meaningful values. Change still exists, but it is made explicit and pushed toward the boundaries, where it is easier to see and control.
That is not just a matter of taste. It changes what kind of system we are describing.
Static types as mechanical review
Static typing adds another kind of precision.
A type system can make parts of the system explicit in the language: data shapes, alternatives, states and constraints that a compiler can check.
That is useful for AI-assisted programming.
Generated code can look plausible while still misunderstanding the model. A compiler becomes a mechanical reviewer. It does not get impressed by code that merely looks reasonable. It just says no.
This is not only true in languages such as F# or Haskell. It is also increasingly visible in Java and C#. Records, sealed types and pattern matching make it easier to express data shapes, alternatives and states directly.
But static typing also has a cost.
Types are good at protecting a shape. They can be less good when the shape itself needs to change.
The more expressive the type system becomes, the more tempting it is to move complexity into the types themselves. At some point, understanding the program requires understanding not only the domain model, but also a second language of type-level encodings, constraints and abstractions.
The compiler can then prove more, but the human may see less.
Types can preserve some kinds of meaning, but they can also hide meaning behind machinery.
So the question is not simply whether static typing is better than dynamic typing. The question is where we need mechanical precision, and where we need the problem to remain visible.
The Lisp Empire Strikes Back
This is where Lisp-style languages become interesting again.
Not because the Lisp idea is old, or because it has a romantic history in symbolic AI, but because Lisp has a property that fits this problem unusually well.
A Lisp is, simply put, a programming language where source code is expressed as data structures. The code is not merely text that hides a structure. The structure is visible in the text, and easy for tools and AI systems to inspect.
In most languages, source code starts as text. Before compilers, editors, refactoring tools or AI systems can work with it safely, that text has to be parsed into a data structure. In Lisp-style languages, code starts much closer to that data structure.
That has surprisingly positive consequences.
There is less distance between the text humans read, the structure tools inspect, and the transformations AI can suggest. Code can be treated less like a stream of characters and more like a meaningful shape.
For example:
(-> request
normalize
validate
decide
emit-events)
This is not much code. It is just a list of six symbols. But it says something important:
There is input that is normalized. On which rules are applied before decision is made. The result is not hidden object state, but emitted events.
That is close to the shape of many real systems.
It is not a controller, a service, a repository or a framework lifecycle. It is a description of what happens.
This is why modern functional Lisps feel relevant in the age of generative AI. They offer little ceremony, visible structure and a natural way to move between data, code and domain language.
What these languages taught me
This is not an argument I arrived at from one language alone.
During the last fifteen years, in a career spanning more than thirty years, I have worked seriously with several general-purpose languages.
Java and C# have given me large-scale, statically typed object-oriented systems, and both languages have become much better at expressing data shapes and alternatives than they used to be.
Python has shown the value of low ceremony. It is easy to start with, easy to read, and extremely useful for exploration, automation, AI and data work.
F# has shown me the strength of ML-style functional programming: immutable values, discriminated unions, pattern matching and a type system that can remove whole classes of mistakes before the program runs.
Clojure has shown me something different again.
It keeps the ceremony low, but makes data, transformation and language-building central. It does not force the shape of the domain into classes first. It lets the system grow from data, functions and explicit transformations.
That combination is why Clojure stands out to me in this discussion.
Why Clojure stands out
The most sophisticated systems I have written have been written in Clojure.
Clojure stands out to me because it combines several of these ideas without making the language itself too large.
It is a Lisp, so code is close to data and the structure of the program remains visible. It is functional with strong immutability, so values and transformations are central. It is dynamic, so the ceremony stays low and the model can grow as understanding improves.
But Clojure also has another important property: the system is usually developed while it is running.
The REPL is not just a console. It is a way of working with a live program. You evaluate expressions, inspect values, redefine functions and grow the system from within.
And this does not have to happen in a toy environment. On the JVM, it can happen inside a running Java process. With ClojureScript, the same style of interaction can also reach JavaScript runtimes.
That matters.
The feedback loop becomes very short. You do not only write code and wait for the whole application to restart. You ask the running system a question and inspect the answer.
In that sense, the Clojure REPL experience feels surprisingly close to working with an AI prompt.
There is a question.
There is a response.
You inspect the result.
You refine the next step.
The difference is that the conversation is with the running system itself. The feedback is not only plausible text. It is executable behaviour.
Connect the repl to an AI prompt, this gives “vibe coding” a different meaning.
The human asks.
The AI suggests.
The REPL answers.
The running system disagrees when the idea is wrong.
That is a stronger loop than code generation alone.
You can start with ordinary data.
You can write simple functions that transform it.
You can make rules explicit.
You can describe flows as data.
You can let the domain shape the language, instead of forcing the domain into a framework too early.
Clojure does not remove complexity.
It helps keep the important complexity visible and close enough to touch.
The human still owns the meaning
If AI writes more of the implementation, the human role does not disappear. It moves.
The human still needs to decide what the system is, what the concepts mean, what data represents, what transformations are valid, what rules must hold, what examples prove the behaviour, and what must remain understandable over time.
That is not outside programming. That is programming. It may not look like writing Java classes all day. It may not look like wiring together a framework. It may not look like manually producing every line of implementation code.
But it is still the work of shaping a system so that it preserves meaning.
AI can help even here.
It can suggest concepts, rules, examples and models.
But if no human cares what the system is supposed to mean, then the real question is not whether AI can build it.
The question is why it should be built at all.
Programming moves closer to the problem
Perhaps the future of programming with AI is not that programming disappears.
Perhaps it is that more of the work moves closer to the problem itself.
If AI writes more of the implementation, humans need to become better at describing what the implementation is supposed to preserve: the concepts, rules, examples, events, transformations and boundaries of the system.
That work may not always look like programming as we know it. It may be less centered on classes, frameworks and individual lines of code, and more centered on shaping a model clearly enough that both humans and AI can work with it.
For me, this is why Clojure remains interesting.
Not necessarily as the language where every final implementation has to live, but as a language that keeps data, transformations and domain concepts visible while the system is still being shaped.
If AI changes programming, I do not think it removes the need for programmers.
It makes the meaning of the system harder to hide