We Learned Nothing from Dijkstra


A split illustration: on the left, an elderly Dijkstra at a writing desk beside a stack of his real books (“A Discipline of Programming,” “The Humble Programmer”) and a handwritten note reading “Simplicity is prerequisite for reliability, E.W. Dijkstra,” in front of a chalkboard reading “EWD 540, Two Views of Programming: THE HARD VIEW, Programming is an intellectual discipline about reasoning (understanding)” versus “THE EASY VIEW, Programming is a technical task about notation (transformation)”; on the right, a glowing digital AI head beside an “AI CODE GENERATOR” panel showing a short authentication function with an output reading “Plausible,” a mug reading “PROMPT. GENERATE. SHIP,” and a sticky note reading “SPECS + TESTS = TRUST.” A banner across the bottom reads “50 years of chasing the easy view. Still missing the hard part.”

In 1975, Edsger W. Dijkstra published a short essay called Two Views of Programming, EWD 540. It is only a few pages long, but it contains one of the most enduring observations ever written about software engineering. Reading it today is a strange experience because it does not feel like a historical document. It feels like commentary on the current state of the AI industry.

Dijkstra argued that programmers had divided themselves into two fundamentally different schools of thought. One viewed software engineering as an inherently difficult intellectual discipline. The difficulty was not an accident of primitive languages or slow computers; it was a consequence of asking humans to construct systems whose behavior must remain correct across an enormous space of possibilities. Programming, in this view, is fundamentally an exercise in reasoning.

The other school believed programming was only temporarily difficult. Better languages, higher levels of abstraction, more sophisticated tools, or improved management techniques would eventually reduce software development to little more than describing what you wanted. Every technological advance was seen as another step toward making programming accessible to everyone.

Fifty years later, it is difficult to avoid the conclusion that we learned absolutely nothing from Dijkstra.

The AI industry likes to describe today’s revolution as unprecedented. This time, we are told, software can simply be generated. Requirements become prompts. Design becomes conversation. Implementation becomes autocomplete. The software engineer’s job is no longer to build systems but to supervise an increasingly capable machine that builds them instead.

The marketing is certainly new. The underlying idea is not.

The software industry has spent decades reinventing the same promise under different names. COBOL was supposed to let people write software in something resembling ordinary English. Dijkstra was characteristically blunt about the result, writing in the same year as EWD 540 that “the use of COBOL cripples the mind; its teaching should, therefore, be regarded as a criminal offence.” Fourth-generation languages promised developers would specify what they wanted instead of how to build it. CASE tools generated applications from diagrams. Model-driven development declared that models, not source code, were the real artifact. Low-code and no-code platforms promised to democratize software development by eliminating the need for programmers altogether.

Now we have large language models, prompt engineering, vibe coding, spec-driven development, and harness-based development. The technology has advanced dramatically. The sales pitch has barely changed. Every generation believes it has finally solved software engineering, and every generation eventually discovers that it has merely automated a different part of programming.

The reason this cycle keeps repeating is that we continue to misunderstand what programmers actually do. We imagine that programming consists primarily of writing code because code is the most visible product of the profession. From the outside, programmers appear to spend their days typing. If an AI can type faster than a human, then surely the problem has been solved.

But typing has never been the bottleneck. We have spent fifty years trying to automate the part of software engineering that was never the hard part.

The difficult work begins long before the first line of code is written. Requirements are incomplete, contradictory, and constantly changing. Users often do not know what they actually want until they see what they asked for. Every feature interacts with every other feature in ways nobody anticipated. Every simplification hides assumptions that eventually surface as bugs. The challenge has never been translating ideas into syntax. The challenge has always been understanding the problem well enough to know what should be translated in the first place.

That distinction matters because AI is extraordinarily good at one of those tasks and only marginally helpful at the other.

An LLM can produce thousands of lines of plausible code in seconds. What it cannot do is eliminate ambiguity. It cannot discover every unstated requirement that nobody thought to mention. It cannot reliably determine whether two conflicting business goals can both be satisfied. It cannot establish that the specification itself is complete or coherent. Those problems do not disappear because code generation became cheaper. They simply become easier to overlook.

This is why so much of the current conversation feels strangely backwards. We celebrate dramatic improvements in code generation while paying comparatively little attention to understanding. Organizations proudly report increases in developer productivity because more pull requests are merged, more stories are completed, or more lines of code are produced. These metrics measure activity, not comprehension. This publication has made a version of this argument before: a harness that reports every test passing is a proxy for verification, not verification itself, and the gap between the two is exactly where the understanding Dijkstra described is supposed to live.

If Dijkstra were alive today, I suspect he would ask a different question: Has our ability to understand software improved?

That is the only question that really matters.

Modern software systems already exceed the ability of any individual engineer to fully comprehend them. AI unquestionably increases our ability to generate additional complexity. Whether it increases our ability to reason about that complexity is far less obvious. In many organizations, it appears to be having the opposite effect. We can now create systems faster than we can understand them. That should concern us, and it compounds a related problem this publication has written about separately: the entry-level work that once taught engineers to reason about systems in the first place is being automated away before it can produce the judgment AI itself cannot supply.

Another revealing word appears frequently in discussions of AI-generated software: plausible. That should not surprise us. Large language models are designed to generate statistically plausible continuations of the material they are given. They do not begin with an independent understanding of the system, derive the correct implementation, and then express it as code. They predict code that resembles the code most likely to appear in that context.

Often, the result is correct. Sometimes, it is remarkably correct. But correctness is not what the underlying process directly optimizes. The model produces code that looks appropriate, follows familiar patterns, and fits the specification as represented in its context. Detailed specifications, automated tests, code review, and validation harnesses are then expected to close the gap between statistical plausibility and dependable software.

This also explains why hallucinations should not be viewed merely as bugs waiting to be eliminated. They are the visible symptom of a deeper design characteristic. When a system is built to generate statistically plausible outputs rather than independently establish their truth, most of its outputs may be useful, some may be extraordinary, and a few may be spectacularly wrong. All of them arise from the same underlying process. Hallucinations are not a separate failure mode. They are the inevitable consequence of a system optimized for plausibility rather than understanding.

The distinction matters because software engineering has never been about producing code that merely appears correct. It has always been about reasoning our way toward code that is correct, or, more realistically, about assembling enough evidence, understanding, and disciplined skepticism to justify trusting it. Plausibility describes the appearance of correctness. Software engineering requires considerably more.

A comprehensive test suite increases confidence, but it does not prove correctness. Specifications reduce ambiguity, but they cannot eliminate it. Harnesses are valuable engineering tools, yet they verify only the behaviors we thought to verify. They cannot test requirements that were never written, assumptions that were never recognized, or failure modes no one imagined. None of these techniques can substitute for understanding. They provide evidence that our current understanding has survived another round of testing.

There is nothing wrong with using AI to eliminate tedious work. Compilers eliminated tedious work. Modern IDEs eliminated tedious work. Static analyzers prevent countless defects. Version control transformed collaboration. Every generation of tools has allowed engineers to spend less time fighting syntax and more time solving problems.

AI belongs in that tradition.

The mistake begins when we confuse a better tool with the elimination of the discipline itself. A calculator did not make mathematics obsolete. CAD software did not eliminate mechanical engineering. A nail gun did not replace carpentry. Likewise, an LLM does not replace software engineering simply because it produces software-shaped text.

Dijkstra understood something the AI industry still struggles to accept. Programming is not fundamentally about writing instructions for computers. It is about constructing systems that humans can understand, reason about, modify, and trust. Code is merely the artifact left behind by that process.

This was not a passing complaint. Thirteen years after EWD 540, Dijkstra made the same case again in On the Cruelty of Really Teaching Computing Science, EWD 1036, arguing that programming is a branch of mathematics, and that the goal is to reason rigorously about why a program works, not merely to discover whether it happens to. Dijkstra was never objecting to COBOL, CASE tools, or AI specifically. He was objecting to the recurring belief that software engineering could be reduced to a problem of notation instead of reasoning.

For fifty years, our industry has repeatedly convinced itself that programming is primarily a translation problem. Express your intent more naturally. Raise the level of abstraction. Generate more of the implementation automatically. This time, surely, software engineering itself will become easy.

Every decade, the promise returns wearing different clothes. Every decade, reality wins.

AI is an extraordinary technological achievement, and it will undoubtedly become another indispensable tool in the engineer’s toolbox. But if we mistake faster code generation for the solution to software engineering, we will make exactly the same mistake our industry has been making since long before anyone had heard of transformers or large language models.

The syntax has changed. The interfaces have changed. The models have changed. The venture capital has certainly changed.

The central misunderstanding has not.

We still confuse generating software with engineering software. We still confuse passing tests with understanding systems. We still confuse statistical plausibility with correctness. Fifty years later, we are making the same mistake with better hardware and far more venture capital.

We did not outgrow Dijkstra.

We simply learned nothing from him.