They say that coding is easy and software engineering is hard. What I think that means is that writing code is something you can pick up in weeks, while becoming a proficient software developer is a process that takes years. This is something I feel not many people get, and something that has post-ChatGPT turned into a significant source of unrealistic expectations.
It seems that many non-developers imagine turning ideas into execution is a bit like translating a novel from one language to another: you turn human language into something that the computer understands. Now that we can with LLMs automatically generate this “machine speak”, it would seem that anyone can build a product. With AI, you don’t need a CS degree just to “talk to computers”, hence you can build anything yourself. That's a huge source of buzz, the most extreme takes being along the lines of “now that that’s out of the way, you can just fire the entire engineering team”.
But software engineering is not about writing code. Not really. Writing code is a part of it, yes, and that part may look very different from 2020 to 2030, but if you really go high level the whole business is about something else.
At the end of the day, computer programs are mathematical objects. Humans, on the other hand, operate in terms of natural language. What I believe the whole industry is about is managing this relationship. It’s the job of an engineering team, the job of the entire technical company, in fact, to convert a fuzzy input into a mathematically precise definition (i.e. a computer program), and make sure this definition is up to date with its use-cases.
Natural language and complexity
AI enables a computer to understand fuzzy input — a task that used to be exclusively human —, and it's amazing how well it does sometimes. However, at the end of the day, vibe coding is limited by the language itself. Natural language is terrible for managing complexity, and managing complexity is perhaps the most challenging aspect of building software. Usually the product doesn't need to do anything super difficult. The thing is that it has to do a hundred medium difficulty things at once and remain usable and maintainable. I have previously written about this by saying that AI can code, but it can't build software, and that there is no free lunch in vibe coding, but there I didn't give any concrete examples. I'll try to amend this here.
I tried to find an example of a piece of code that would have a terrible code-to-description ratio, and I think that the fixed-point combinator is a good extreme example. Consider this JavaScript snippet:
const Y = (f) => ((x) => x(x))((x) => f((y) => x(x)(y)));
The function Y allows one to define recursion without self-referential definition. For example, the following snippet defines a factorial function using Y.
const factorialLogic = (recurse) => (n) => {
if (n === 0) return 1;
return n * recurse(n - 1);
};
const factorial = Y(factorialLogic);
console.log(factorial(5)); // Output: 120
I asked Gemini to express the exact logic of Y in terms of natural language, and this is the best response I got:
The function $Y$ accepts a functional argument $f$ and returns the result of applying a self-application function—which takes an input $x$ and returns $x(x)$—to a generator function that takes an input $x$ and returns $f$ applied to a delayed execution wrapper; this wrapper accepts a value $y$ and, upon invocation, performs the self-application of $x$ to $x$ and applies the resulting function to $y$, thereby creating a recursive loop where $f$ is continuously provided with a functional version of itself that uses the stored "blueprint" $x$ to recreate the entire execution structure for each subsequent call.
Now, perhaps it is a matter of taste whether the quote above or the one-liner
const Y = (f) => ((x) => x(x))((x) => f((y) => x(x)(y)));
is "better", but I, for one, can't make any sense out of the text version. For sure, the code is kind of a brain-melter too, but at least it can be ran, it can be debugged and it can be tested. In this sense, language is much more opaque.
Perhaps this was an unfair example. The significance of Y is mostly educational. You would probably not encounter it in any production code, and hence it's something just to prove a point. To be a bit more practical, we could tune this down a notch and instead consider, for example this:
const diff = (obj1, obj2) =>
Object.keys(obj2).reduce((acc, key) => {
if (obj1[key] === obj2[key]) return acc;
acc[key] = (typeof obj2[key] === 'object' && obj1[key])
? diff(obj1[key], obj2[key])
: obj2[key];
return acc;
}, {});
This snippet returns (nested) changes between two objects, for example:
const currentProfile = {
id: 1,
metadata: {
lastLogin: '2023-01-01',
tags: ['admin', 'editor']
}
};
const updatedProfile = {
id: 1,
metadata: {
lastLogin: '2026-04-26', // Changed
tags: ['admin', 'editor'] // Same
}
};
const profileDiff = diff(currentProfile, updatedProfile);
console.log(profileDiff);
// Output: { metadata: { lastLogin: '2026-04-26', tags: {} } }
In this case, Gemini turns this into natural language as follows.
Iterate through the keys of the second object using a reducer. For every key, check if the value is identical to the first object's value at that key; if so, skip it. If it's not identical, check if the value is itself an object and if that key also exists as an object in the first. If both are objects, recursively restart this entire process for those sub-objects and assign the result to the current key in our accumulator. Otherwise, just take the value from the second object. Finally, return the accumulated differences.
Again, I could not make any sense out of the text version, but the code one with a couple of examples, I can understand quite comfortably.
Complexity management is code
What I’m trying to demonstrate with the above examples is that, when you are trying to describe something highly precise and difficult, natural language quickly becomes intractable in comparison to the equivalent code. To keep things maintainable, you introduce some rules; perhaps definitions follow some sort of template format, are arranged to sub-definitions, or perhaps you introduce some notation to maintain readability. Suddenly you realize, your “no code” workflow becomes “low code”, and from there you’d start to approach some sort of scripting.
I view this as a natural phenomenon. We have already witnessed the same thing with mathematics over hundreds of years: mathematics is highly formal (i.e. “difficult”) not because some cabal of academic gatekeepers want to make things intentionally esoteric, but because the level of rigour is what the subject matter requires. In the same way, programming is the way it is, because the problem it’s trying to solve, automation in a broad sense, is very difficult. You can only make it so much easier, before you are not solving the same problem at all. I think this is where we are currently at with vibe coding: people are trying to find the correct level to operate on so that they get the benefits of AI without compromising on generality.
What AI may well do is to move the goalposts. It can raise the level of abstraction the technical team operates on. That fits into the wider historical development where we have, since the 50’s, gradually moved away from the metal, towards a more abstract expression.
The technical teams, however, have only been growing.


