You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
One of the most profound results in the whole of Computer Science is that there exist problems which no computer can ever solve — not because our machines are too slow or our memory too small, but because no algorithm to solve them can possibly exist. The flagship example is the halting problem, which Alan Turing proved undecidable in 1936, in the very same paper that introduced the Turing machine. This lesson lays out the halting problem carefully, walks through Turing's proof-by-contradiction in an accessible but rigorous form, and draws the sharp distinction between problems that are merely hard and problems that are genuinely impossible. The result is humbling: it tells us that the dream of a perfect, fully automatic program-analyser — one that could decide for any program whether it will crash, loop, or behave correctly — is not just difficult but provably unattainable.
This lesson addresses the Theory of Computation strand of the AQA A-Level Computer Science specification (7517), in the area concerned with a model of computation and its limits (around section 4.4.5). You are expected to understand the halting problem — its precise statement, and the idea that it is undecidable; to follow and reproduce, in an accessible form, the proof by contradiction that no algorithm can solve it; to distinguish computable from non-computable problems and functions, and decidable from undecidable problems; and to appreciate the consequences of undecidability for real software (perfect debuggers, virus scanners and verifiers are impossible in general). The treatment below is descriptive and conceptual rather than a verbatim quotation of the specification.
Before the halting problem itself, we need the vocabulary precisely.
| Classification | Definition | Example |
|---|---|---|
| Decidable problem | A yes/no problem for which there is an algorithm (equivalently, a Turing machine) that, on every possible input, halts and gives the correct answer | "Is this whole number prime?" |
| Undecidable problem | A yes/no problem for which no such algorithm exists — for any proposed algorithm there are inputs on which it gives a wrong answer or never halts | The halting problem |
| Computable function | A function for which an algorithm exists that computes its output for every input and always halts | f(n)=n2; the greatest common divisor of two numbers |
| Non-computable function | A function for which no algorithm can produce the correct output for all inputs | The function that, given a program, returns 1 if it halts and 0 otherwise |
The phrase "an algorithm exists" is made exact by the Church-Turing thesis from the previous lesson: it means precisely "a Turing machine exists". So when we prove that no Turing machine can solve the halting problem, we have proved that no algorithm whatsoever can — on any present or future hardware. A decidable problem is one whose algorithm has two guarantees: it always halts, and it always gives the right answer. Drop either guarantee and the problem may slip into undecidability. The halting problem will turn out to fail the first guarantee in an unavoidable way.
It is worth stressing at the outset what undecidable does not mean. It does not mean "we have not found an algorithm yet"; it means we have proved that none can exist. And it does not mean "no instance can be answered" — for plenty of individual programs we can easily see whether they halt. What is impossible is a single general method that works for every program-and-input pair.
Given a description of a program P and an input I for it, decide whether P will eventually halt (terminate) when run on I, or whether it will run forever.
Phrased as a demand for an algorithm: does there exist an algorithm H such that, for every program P and every input I,
"halts" if P halts on input I, and"loops" if P runs forever on input I,with H itself always terminating and giving the correct verdict? The crucial subtlety is that H must answer for all programs and inputs, and must do so without simply running P — because if P loops forever, naively running it would mean H never returns, and then H would not be the always-halting decider we require. Turing's theorem is that no such H exists.
The tempting amateur answer is: "to find out whether P halts on I, simply run P on I and watch." This semi-works in one direction only. If P does halt, then running it will eventually reveal that — after some finite number of steps it stops, and we can report "halts". But if P runs forever, watching it never lets us conclude "loops": at every moment we have only seen finitely many steps, and we can never be sure it will not halt on the very next one. There is no point at which "it has not halted yet" becomes "it will never halt". So "run and watch" is not a decider — it fails to halt precisely on the looping inputs, which are exactly the ones where we needed a definite "loops" answer.
Turing proved that no decider H can exist using a proof by contradiction built on a self-reference / diagonalisation trick. The argument is short, and worth following slowly because it is a classic examined topic.
Step 1 — Assume the decider exists. Suppose, for contradiction, that an always-halting algorithm H(P,I) exists that correctly answers "halts" or "loops" for every program P and input I.
Step 2 — Build a deliberately contrary program D. Using H as a building block, construct a new program D that takes a single program P as its input and does the opposite of what H predicts about P run on itself:
PROGRAM D(P):
IF H(P, P) = "halts" THEN
loop forever # if H says P(P) halts, D deliberately loops
ELSE
halt # if H says P(P) loops, D deliberately halts
ENDPROGRAM
Here H(P, P) asks the perfectly legitimate question "does program P halt when given its own description as input?". Since H is assumed to decide every program-input pair, it certainly decides this one, so D is a well-defined program: it always consults H (which always halts) and then either loops or halts accordingly.
Step 3 — Feed D its own description. Now ask the fatal question: what does D do when run on D itself — that is, what is the behaviour of D(D)? Inside that call, the test evaluated is H(D, D), which by definition reports whether D(D) halts. There are only two possibilities, and we examine each.
| Suppose H reports... | Then by D's code, D(D) actually... | But H had claimed D(D)... | Result |
|---|---|---|---|
H(D, D) = "halts" | enters the infinite loop → does not halt | halts | Contradiction |
H(D, D) = "loops" | takes the halt branch → halts | loops | Contradiction |
Step 4 — Conclude. In both cases D(D) does the exact opposite of what H predicted, so H is wrong about D(D) — contradicting the assumption that H is always correct. The only assumption we made was that H exists, so that assumption must be false. Therefore no algorithm H that decides the halting problem can exist; the halting problem is undecidable. ■
The heart of the trick is self-reference: by letting a program operate on its own description, we engineer a question that any supposed all-knowing decider must get wrong. It is the computational cousin of the liar paradox ("this sentence is false") and of Cantor's diagonal argument that the real numbers are uncountable — the same logical machine, applied to programs.
Once the halting problem is known to be undecidable, we rarely repeat the diagonalisation from scratch. Instead we use reduction: to show a new problem X is undecidable, we assume an algorithm for X exists and show it could be used to build a halting-problem decider — which we know is impossible, so the algorithm for X cannot exist either. The schematic is always the same:
Problem X: given any program P and input I, decide whether P ever produces any output when run on I.
Assume an algorithm AX(P,I) exists that returns "yes" if P produces output on I and "no" otherwise, always halting. We use it to solve the halting problem as follows. Given an arbitrary program P and input I whose halting we wish to decide, construct a modified program P′ that behaves exactly like P but is silent throughout — it produces no output at all — except that, immediately before P would terminate, P′ prints a single character, say "done":
PROGRAM P'(I):
run the body of P on I, suppressing any output it would make
# control only reaches the next line if P halts
print "done"
ENDPROGRAM
Now run AX(P′,I). Because P′ produces output only if and when P halts:
"yes" (output is produced), then P must have halted on I;"no" (no output ever), then P never reached the print, i.e. P runs forever on I.So AX would let us decide whether P halts on I — a halting-problem decider. Since none can exist, our assumed AX cannot exist either, and "does it ever produce output?" is undecidable. This reduction pattern — wrap the unknown program so that some easy-to-test event happens exactly when it halts — is the workhorse for proving undecidability, and a manageable version of it is a realistic exam task.
It is worth seeing the same technique applied once more, because recognising the pattern is what makes reduction questions approachable. Problem Y: given any program P, decide whether P (run on empty input) ever prints the exact string "hello". Assume a decider AY exists. Given an arbitrary program P and input I whose halting we want to decide, build a wrapper P′′ that ignores its own input, runs P on I with all of P's normal output suppressed, and — only if P halts — prints "hello" and stops:
PROGRAM P''():
run the body of P on I, suppressing all of P's output
# reached only if P halts
print "hello"
ENDPROGRAM
Then P′′ prints "hello" exactly when P halts on I. Running AY(P′′) therefore decides the halting problem — impossible — so AY cannot exist and "does it ever print hello?" is undecidable. The mechanics are identical to the previous reduction; only the "easy-to-spot event" has changed (from any output to a specific string). Once you see that any observable behaviour can be rigged to coincide with halting, it becomes intuitive why essentially every behavioural question is undecidable — which is precisely what Rice's theorem, next, asserts in full generality.
A subtle distinction sharpens understanding here. Undecidability is about whether an algorithm exists at all, not about how long an algorithm takes. Consider the question "does this program halt within one million steps on this input?" — superficially close to the halting problem. This bounded version is perfectly decidable: simply simulate the program for up to a million steps and report whether it stopped. The simulation always terminates (after at most a million steps), so an algorithm exists. The unbounded halting problem is undecidable precisely because removing the step limit removes the guaranteed stopping point of any "simulate and watch" approach. The general lesson is that adding a finite bound to a resource (steps, memory, time) very often turns an undecidable question into a decidable — though possibly intractable — one. Keeping this straight prevents the common error of assuming any halting-flavoured question must be undecidable: it is specifically the unbounded, all-inputs version that is impossible.
The previous reduction is not a lucky one-off. Rice's theorem generalises the situation dramatically:
Any non-trivial property of the function (the input-output behaviour) computed by a program is undecidable.
"Non-trivial" means simply that the property is true of some programs and false of others (a property true of every program, or of none, is trivially decidable). So essentially every interesting semantic question you might ask about what an arbitrary program does is undecidable in general:
"hello"?Key insight: We can always determine such a property for a specific program by analysis or by running it. What is impossible is a single general algorithm that correctly answers the question for every possible program. The undecidability is about the demand for universality, not about any individual case.
Rice's theorem reframes our whole intuition: undecidability is not a rare pathology lurking only in a contrived halting question — it is the default status of questions about program behaviour. Decidable behavioural questions are the exceptions.
Turing machines let us refine "solvable" into finer grades, and the halting problem sits revealingly between them.
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.