Not Everyone Can Code

4 min read Original article ↗

Counterfactual reasoning is a technique where you assume alternative scenarios that could have happened, but did not in fact happen. You assume something is true when it is, in fact, false. A question involving a counterfactual is "What if dinosaurs had not gone extinct?" Reasoning with counterfactuals is considered an advanced cognitive skill, and it comes in quite handy when you are debugging code.

To debug effectively, you have to entertain insulting falsehoods. You must look at your own logic and ask: "Suppose this invariant I spent three days designing failed. Suppose this pointer is null, even though I checked it two lines ago. How does that world exist?" You have to temporarily decouple the state of the machine from your own intentions.

A surprising proportion of the adult population has extreme difficulty with counterfactual reasoning. They get actively frustrated or hostile when asked to reason from a premise they know to be untrue. Psychological studies (Dulit 1972, Keating 1979, Neimark 1975) estimate that roughly 20% of adults experience severe difficulty with counterfactual thinking, and another 20% experience moderate difficulty. This means that 40% or more of people are fundamentally anchored to the concrete, observable world. These people are not unintelligent; they simply possess a cognitive architecture that rejects hypothetical fictions. But because software state is entirely abstract, they will find diagnosing bugs an agonizing, borderline impossible task.

In causal inference, Judea Pearl laid out three levels of reasoning:

  1. Association: What is? What is the correlation between X and Y?
  2. Intervention: What if? What happens to Y if we intervene and set X to x?
  3. Counterfactuals: Why? Why did Y happen when X was x?

A programmer who cannot entertain counterfactuals will be able to answer the first two questions, but not the third. They won't be able to debug any but the most trivial programs.

Debugging is pure counterfactual reasoning. It isn't just observing the present, it is abductive reasoning over hypothetical pasts. You have the actual world: The pointer is null. You must construct a counterfactual world: In order for the pointer to be null, the allocation must have failed, or the loop must have been skipped, etc. If someone cannot reason counterfactually, they cannot diagnose a bug deductively.

This is not to say that someone bad at counterfactual reasoning cannot appear to be a programmer. You see them all the time. They don't simulate the system in their head, they do pattern matching. They copy a boilerplate framework or prompt an LLM or crib some code from StackOverflow (if it still exists). They glue libraries together using syntactic recipes. They operate strictly at Pearl's levels 1 and 2, association and intervention, but never reach level 3, counterfactuals.

Not everyone can debug multithreaded code

Reasoning about multithreaded code is counterfactual reasoning on steroids. In a single-threaded program, you trace a single path of what happened versus what might have happened. In concurrent systems, you must simultaneously evaluate an exponential explosion of alternate histories. You have to reason about every possible interleaving of thread execution, asymmetric cache invalidations, and preemption points that could occur, even if they have never yet been observed in testing.

To the concrete mind, if a race condition does not manifest across a thousand test runs, it does not exist. To the concurrency engineer, the untested interleaving is a lurking ghost. You are not debugging a machine; you are debugging a multiverse. It requires maintaining a swarm of mutually contradictory realities in working memory all at once—a cognitive burden so unnatural that very few humans can do it well without the aid of formal models or pure architectural restraint.

Regardless of how you feel about the slogan "Everyone should learn to code", some people will have vastly harder times than others.