What does “Undecidable” mean, anyway
What does "Undecidable" mean, anyway
An explainer for people who don't know computer science and are mildly curious
Systems Distributed
I'll be speaking at Systems Distributed next month! The talk is brand new and will aim to showcase some of the formal methods mental models that would be useful in mainstream software development. It has added some extra stress on my schedule, though, so expect the next two monthly releases of Logic for Programmers to be mostly minor changes.
What does "Undecidable" mean, anyway
Last week I read Against Curry-Howard Mysticism, which is a solid article I recommend reading. But this newsletter is actually about one comment:
I like to see posts like this because I often feel like I can’t tell the difference between BS and a point I’m missing. Can we get one for questions like “Isn’t XYZ (Undecidable|NP-Complete|PSPACE-Complete)?”
I've already written one of these for NP-complete, so let's do one for "undecidable". Step one is to pull a technical definition from the book Automata and Computability:
A property P of strings is said to be decidable if ... there is a total Turing machine that accepts input strings that have property P and rejects those that do not. (pg 220)
Step two is to translate the technical computer science definition into more conventional programmer terms. Warning, because this is a newsletter and not a blog post, I might be a little sloppy with terms.
Machines and Decision Problems
In automata theory, all inputs to a "program" are strings of characters, and all outputs are "true" or "false". A program "accepts" a string if it outputs "true", and "rejects" if it outputs "false". You can think of this as automata studying all pure functions of type f :: string -> boolean
. Problems solvable by finding such an f
are called "decision problems".
This covers more than you'd think, because we can bootstrap more powerful functions from these. First, as anyone who's programmed in bash knows, strings can represent any other data. Second, we can fake non-boolean outputs by instead checking if a certain computation gives a certain result. For example, I can reframe the function add(x, y) = x + y
as a decision problem like this:
IS_SUM(str) {
x, y, z = split(str, "#")
return x + y == z
}
Then because IS_SUM("2#3#5")
returns true, we know 2 + 3 == 5
, while IS_SUM("2#3#6")
is false. Since we can bootstrap parameters out of strings, I'll just say it's IS_SUM(x, y, z)
going forward.
A big part of automata theory is studying different models of computation with different strengths. One of the weakest is called "DFA". I won't go into any details about what DFA actually can do, but the important thing is that it can't solve IS_SUM
. That is, if you give me a DFA that takes inputs of form x#y#z
, I can always find an input where the DFA returns true when x + y != z
, or an input which returns false when x + y == z
.
It's really important to keep this model of "solve" in mind: a program solves a problem if it correctly returns true on all true inputs and correctly returns false on all false inputs.
(total) Turing Machines
A Turing Machine (TM) is a particular type of computation model. It's important for two reasons:
-
By the Church-Turing thesis, a Turing Machine is the "upper bound" of how powerful (physically realizable) computational models can get. This means that if an actual real-world programming language can solve a particular decision problem, so can a TM. Conversely, if the TM can't solve it, neither can the programming language.1
-
It's possible to write a Turing machine that takes a textual representation of another Turing machine as input, and then simulates that Turing machine as part of its computations.
Property (1) means that we can move between different computational models of equal strength, proving things about one to learn things about another. That's why I'm able to write IS_SUM
in a pseudocode instead of writing it in terms of the TM computational model (and why I was able to use split
for convenience).
Property (2) does several interesting things. First of all, it makes it possible to compose Turing machines. Here's how I can roughly ask if a given number is the sum of two primes, with "just" addition and boolean functions:
IS_SUM_TWO_PRIMES(z):
x := 1
y := 1
loop {
if x > z {return false}
if IS_PRIME(x) {
if IS_PRIME(y) {
if IS_SUM(x, y, z) {
return true;
}
}
}
y := y + 1
if y > x {
x := x + 1
y := 0
}
}
Notice that without the if x > z {return false}
, the program would loop forever on z=2
. A TM that always halts for all inputs is called total.
Property (2) also makes "Turing machines" a possible input to functions, meaning that we can now make decision problems about the behavior of Turing machines. For example, "does the TM M
either accept or reject x
within ten steps?"2
IS_DONE_IN_TEN_STEPS(M, x) {
for (i = 0; i < 10; i++) {
`simulate M(x) for one step`
if(`M accepted or rejected`) {
return true
}
}
return false
}
Decidability and Undecidability
Now we have all of the pieces to understand our original definition:
A property P of strings is said to be decidable if ... there is a total Turing machine that accepts input strings that have property P and rejects those that do not. (220)
Let IS_P
be the decision problem "Does the input satisfy P"? Then IS_P
is decidable if it can be solved by a Turing machine, ie, I can provide some IS_P(x)
machine that always accepts if x
has property P, and always rejects if x
doesn't have property P. If I can't do that, then IS_P
is undecidable.
IS_SUM(x, y, z)
and IS_DONE_IN_TEN_STEPS(M, x)
are decidable properties. Is IS_SUM_TWO_PRIMES(z)
decidable? Some analysis shows that our corresponding program will either find a solution, or have x>z
and return false. So yes, it is decidable.
Notice there's an asymmetry here. To prove some property is decidable, I need just to need to find one program that correctly solves it. To prove some property is undecidable, I need to show that any possible program, no matter what it is, doesn't solve it.
So with that asymmetry in mind, do are there any undecidable problems? Yes, quite a lot. Recall that Turing machines can accept encodings of other TMs as input, meaning we can write a TM that checks properties of Turing machines. And, by Rice's Theorem, almost every nontrivial semantic3 property of Turing machines is undecidable. The conventional way to prove this is to first find a single undecidable property H
, and then use that to bootstrap undecidability of other properties.
The canonical and most famous example of an undecidable problem is the Halting problem: "does machine M halt on input i?" It's pretty easy to prove undecidable, and easy to use it to bootstrap other undecidability properties. But again, any nontrivial property is undecidable. Checking a TM is total is undecidable. Checking a TM accepts any inputs is undecidable. Checking a TM solves IS_SUM
is undecidable. Etc etc etc.
What this doesn't mean in practice
I often see the halting problem misconstrued as "it's impossible to tell if a program will halt before running it." This is wrong. The halting problem says that we cannot create an algorithm that, when applied to an arbitrary program, tells us whether the program will halt or not. It is absolutely possible to tell if many programs will halt or not. It's possible to find entire subcategories of programs that are guaranteed to halt. It's possible to say "a program constructed following constraints XYZ is guaranteed to halt."
The actual consequence of undecidability is more subtle. If we want to know if a program has property P, undecidability tells us
- We will have to spend time and mental effort to determine if it has P
- We may not be successful.
This is subtle because we're so used to living in a world where everything's undecidable that we don't really consider what the counterfactual would be like. In such a world there might be no need for Rust, because "does this C program guarantee memory-safety" is a decidable property. The entire field of formal verification could be unnecessary, as we could just check properties of arbitrary programs directly. We could automatically check if a change in a program preserves all existing behavior. Lots of famous math problems could be solved overnight.
(This to me is a strong "intuitive" argument for why the halting problem is undecidable: a halt detector can be trivially repurposed as a program optimizer / theorem-prover / bcrypt cracker / chess engine. It's too powerful, so we should expect it to be impossible.)
But because we don't live in that world, all of those things are hard problems that take effort and ingenuity to solve, and even then we often fail.
-
To be pendantic, a TM can't do things like "scrape a webpage" or "render a bitmap", but we're only talking about computational decision problems here. ↩
-
One notation I've adopted in Logic for Programmers is marking abstract sections of pseudocode with backticks. It's really handy! ↩
-
Nontrivial meaning "at least one TM has this property and at least one TM doesn't have this property". Semantic meaning "related to whether the TM accepts, rejects, or runs forever on a class of inputs".
IS_DONE_IN_TEN_STEPS
is not a semantic property, as it doesn't tell us anything about inputs that take longer than ten steps. ↩
If you're reading this on the web, you can subscribe here. Updates are once a week. My main website is here.
My new book, Logic for Programmers, is now in early access! Get it here.
What's Your Reaction?






