When AI Writes the Code, Who Preserves the Meaning?

AI makes code cheaper to produce. But it does not make the meaning of the system cheaper to describe.

Quite the opposite.

When implementation becomes easier to generate, it becomes more important that humans can describe the problem in a form that is more precise than ordinary prose, but still close to the domain.

Plain English is excellent for context, purpose and explanation. It is how we tell each other why something matters.

But when a system grows, the description has to carry more weight.

What data exists? 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?

At that point, English starts to slip.

This is where I think the current Python enthusiasm misses something important.

Python is a good language for getting things done. It is easy to start with, easy to read, and easy for AI to generate. That matters.

But ease of generation is not the same as suitability for preserving system meaning.

It may be tempting to say that we can simply connect an LLM where the system needs more intelligence.

Sometimes we can.

But connecting an LLM is not the hard part. The hard part is preserving the meaning of what it does inside the system.

For small programs, scripts and exploration, the difference may not matter much. The problem is close, the feedback is quick, and the whole thing can often fit in one person’s head.

Large systems are different.

They have concepts, rules, boundaries, exceptions and history. They have data that arrives late, decisions that must still make sense years later, and changes that must not erase what the system used to mean.

A more precise language

So we need something more precise than ordinary prose.

Not necessarily more detailed.
Not more ceremonial.
Not a bigger pile of implementation code.

But a form that can hold the important parts of a system clearly enough to survive change.

It needs to describe data, rules, transformations and decisions.

It needs to describe what changes, and what must not change.

It should be close enough to the domain that humans can read it.

It should be structured enough that tools and AI can inspect it.

It should keep the shape of the problem visible.

For me, that starts with data.

Data-oriented programming

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 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.

The important forms are visible.

They are not hidden inside object graphs, framework lifecycles or scattered mutable state.

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 gives AI something more stable than prose to work with. A visible model of the problem, not just a vague wish for code.

Immutability is closer to reality

This is also where immutability matters.

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.

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 changed by adding new facts, not by overwriting it.

If data carries the meaning of the system, it matters whether that data can change.

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.

Functional programming often takes a different view. State can be represented as one immutable value transformed over time. It’s usually structurally complex, but it is still treated together as one single value.

State transitions become explicit and controlled.

Master of puppets

Object-oriented systems without immutable values can become a marionette theatre.

When you pass an object to a function, you are passing a handle to something that can still move.

That object may be part of a larger graph of objects. It may refer to other objects, which refer to other objects, which refer to other objects.

Any object reached through that graph can potentially change.

Not only by you.
Not only now.
Not only in the place where you are looking.

What if the function keeps the reference?
What if it passes it on?
What if it uses it later?
What if it follows one of the references and changes something deeper in the system?

You passed a puppet with strings still attached to the rest of the theatre. And you don’t know who holds the control bar.

The problem is not that objects can refer to other objects. The problem is that objects can change, and controlling that change is difficult.

Immutable objects also have references. The difference is that immutable references to immutable objects are paths to more structure, not paths to sudden change.

The shape may still be complex, but the values do not suddenly change behind your back. You are in control.

Encapsulation is often presented as a way to manage complexity. But encapsulated mutable domain state adds another kind of complexity: the possibility of change through paths you can no longer see. That is pure complexity.

Modern systems are full of code we do not fully control.

Frameworks.
Libraries.
Plugins.
Callbacks.
Generated code.
Code written by others, who do not fully understand the shapes.
Code written by AI.

That is the normal condition of modern software.

Mutable objects cross those boundaries with the hidden strings still attached.

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.

That is where a functional system deals with mutable state. At the edge, rather than scattered through objects deep inside the core.

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. It has a place at the boundary of the system, outside the core, 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, useful for AI-assisted programming.

Generated code can look plausible while still misunderstanding the model. A compiler for a strong static type system 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, states and constraints directly, in a form the compiler can check.

But static typing also has a cost.

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.

This becomes difficult when the shapes themselves need to change.

The compiler can prove more, but the human may see less.

Types can preserve some kinds of meaning, but they can also hide meaning behind machinery.

And static types do not solve the whole problem.

A type system can reject many impossible shapes. It can make certain misunderstandings mechanically visible before the program runs.

But many important mistakes are not type errors.

A paid invoice, a cancelled booking, or a changed listing are not only type-level facts. They are domain facts. The difficult question is not only which states are possible, but when transitions between them are valid, what history must be preserved, and what the system should mean when facts arrive late or contradict each other.

Those rules still have to be described.

They have to be made visible in data, examples, tests and transformations.

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 compiler is one kind of reviewer. It is not the only one.

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 a programming language where source code is expressed as data structures. The code is not merely text that hides a structure. The structure is clearly 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 shaped, checked and turned into a decision. The result is not hidden object state. The result is something visible that happened in the domain.

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.

The language I use most today is Python. That does not mean I think it is the best language for every kind of thought. Far from it.

It takes experience to compare programming languages by what they make possible, without letting one of them become the answer too early.

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. Both languages have become much better at expressing data shapes and alternatives than they used to be.

Python has shown me the value of low ceremony. It is easy to start with, easy to read, and very useful for exploration, automation 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 matters for the argument of this article.

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 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.

This is not the same strength as a strong static type system.

You are not only standing outside the program, editing text and restarting the application.

You work from within the running system.

You can inspect values that already exist. You can evaluate expressions in the context of the program. You can redefine functions. You can try ideas against real data and real behaviour while the system keeps running.

The REPL is the connection into that live system.

It is not just a console. It is a way to shorten the distance between idea, change and feedback.

Clojure also makes it natural to keep important structures visible, and to let small functions transform them. Those structures can be inspected, described, validated and tested directly.

That matters for AI-assisted programming.

The AI suggestion is not only judged as text. It can be brought into contact with data, rules, examples and executable behaviour quickly.

Connected to an AI prompt, this gives “vibe coding” a different meaning.

The human asks. The AI suggests. The running system answers. The idea is adjusted.

When the idea is wrong, the system can disagree immediately.

That is a stronger loop than code generation alone.

You can start with ordinary data. You, or AI, 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.

Clojure does not remove complexity by hiding it behind leaky abstractions. It helps keep the important complexity visible and close enough to touch.

Programming moves closer to the problem

The hard part was never only telling the computer what to do.

The hard part was building systems that humans can continue to understand while the world changes around them.

AI makes vague programming more dangerous.

When implementation becomes easier to generate, it also becomes easier to generate systems whose meaning nobody really owns.

Code appears.
Tests pass.
The demo works.

But the shape of the problem may already be disappearing into accidental structure.

This is why the old questions about data, change and language become sharper again.

Data-first design, functional programming and Lisp-style languages are not interesting here as separate traditions. They are interesting because they keep asking where the shape of the system is, how change is represented, and how close the language stays to the problem.

That is why the human task moves closer to the problem itself.

Someone still has to decide what the concepts mean, what the data represents, what rules must hold, what changes are allowed, and what must remain understandable over time.


All #ai #architecture #art #clojure #csharp #data-structures #database #datomic #emacs #fortran #fsharp #functional #gpt #haskell #history #immutability #java #jit #jmm #lambdas #lisp #pioneers #poetry #programming #programming-philosophy #randomness #rant #reducers #repl #smalltalk #sql #threads #unix #women