Lesson 23: What prohibition-as-design teaches us about correctness
I covered Haskell's type system back in Lesson 17 — specifically how it encodes what you know rather than just what you have. But a recent piece on language design reframed something I'd been thinking about ever since, and it's worth revisiting from a different angle.
The reframe comes from a DEV Community essay published in July that opens with a Robert Martin observation from Clean Architecture: "Each of the paradigms removes capabilities from the programmer. None of them adds new capabilities." The structured paradigm removed goto. Object-oriented removed the wild indirect jump. Functional programming removed assignment — a value, once bound, never changes again.
That last one is Haskell's founding act. And when you see it as a confiscation rather than a constraint, the whole type system starts to make more sense.
The Bug That Can't Happen If the Value Can't Change
Most bugs in imperative programs share a common ancestor: something changed when you weren't looking. A function you called mutated shared state. A variable you thought was stable got updated by a concurrent thread. A reference you passed around got modified somewhere downstream.
Haskell's answer is to make this structurally impossible. Pure functions, by definition, cannot modify state outside their own scope. Immutable values cannot be changed after binding. The type system enforces both — not as a style guide, but as a compile-time guarantee. As the DEV essay frames it, functional programming "removes assignment" entirely: the entire category of mutation-driven bugs disappears not because programmers are more careful, but because the language refuses to compile the dangerous thing.
This is the same logic behind Rust's ownership model — which the same essay identifies as a fifth paradigm that Martin's framework didn't anticipate, removing shared mutation specifically to eliminate data races. Haskell got there earlier and more radically: no shared mutation because no mutation at all.
The practical consequence is that a Haskell function's behavior is determined entirely by its inputs. Call it twice with the same arguments, get the same result. This property — referential transparency — means you can reason about a function in isolation, without needing to reconstruct the global state that surrounded it when it ran. For debugging, testing, and refactoring, that's not a minor convenience. It's a different mode of thinking about correctness.
What "Nearly a Million Lines" Actually Proves
Theory is one thing. H-E-B's engineering blog published a production account in July that grounds this in something more concrete: eight years of Haskell running critical supply chain systems across more than 435 stores, with nearly a million lines of code maintained by multiple teams.
Joshua Miller's account is honest about the tradeoffs — Haskell is not an easy sell as a business solution, and the organizational challenges were real. But the systems are still in production. That longevity matters. Supply chain software is exactly the domain where subtle mutation bugs are catastrophic: inventory counts that drift, state that diverges between services, race conditions in order processing. The type system's guarantees aren't academic in that context.
What's interesting about the H-E-B case is what it reveals about the maintenance story. When you have teams across different domains working on a shared codebase over nearly a decade, the value of the type system compounds. New engineers can't accidentally introduce a class of bugs that the type system prohibits. Refactoring is safer because the compiler catches the places where your changes break assumptions. The upfront cost of learning to think in types pays dividends over years, not sprints.
The Deeper Design Lesson
There's a pattern worth naming here, because it shows up across the languages we've covered in this series. The most durable design decisions in programming languages tend to be prohibitions — things the language refuses to let you do — rather than features it adds.
Recent formal work on type safety is pushing this further: researchers are now building unified frameworks that treat memory safety, data-structure invariants, and flow-sensitive typing as components of a single type logic, with the goal that "well-typed programs must never abort." The ambition is to make the set of things that can't go wrong as large as possible — not by adding runtime checks, but by making the wrong states unrepresentable at compile time.
Haskell pioneered this instinct. The type system doesn't just describe what your data is; it describes what operations are valid, what effects are permitted, what states are reachable. The bugs it prevents aren't caught — they're made structurally impossible.
That's a different relationship between programmer and language than most of us grew up with. Most languages hand you a full toolkit and trust you not to misuse it. Haskell hands you a smaller toolkit and guarantees that the missing tools were the dangerous ones.
Whether that tradeoff is worth it depends on your domain and your team. But understanding why the tradeoff exists — that language progress is often subtraction, not addition — changes how you evaluate every language you work in, not just Haskell.
