MathLabs

Foundations of mathematics

Recursive functions and Turing machines

Formal models that define exactly which functions can be computed by an algorithm.

IntuitionWhat can a machine compute?

A calculator can add and multiply; a compiler can check types; an AI can (sometimes) answer questions. But is there a function that no algorithm, however clever, can ever compute? Alan Turing's answer — yes — came from a precise mathematical model of "algorithm": the Turing machine, a tape, a read/write head, and a finite table of rules. Two seemingly different formalizations, recursive functions (built from simple pieces by composition and recursion) and Turing machines (a mechanical step-by-step process), turn out to compute exactly the same class of functions — strong evidence for the Church–Turing thesis that this really is "everything computable".

Directed graph of Turing machine states with labeled transition edges.
State-transition graph of a small Turing machine: nodes are states, edges are transitions labeled by (read symbol → write symbol, move).

UndergraduatePrimitive recursive and μ\mu-recursive functions

Definition: Primitive recursive functions

The primitive recursive functions are the smallest class of functions Nk→N\mathbb N^k \to \mathbb N containing the zero function, the successor S(n)=n+1S(n)=n+1, and all projections, and closed under composition and primitive recursion: given g:Nk→Ng:\mathbb N^k\to\mathbb N and h:Nk+2→Nh:\mathbb N^{k+2}\to\mathbb N, the recursion f(x⃗,0)=g(x⃗)f(\vec x,0)=g(\vec x), f(x⃗,n+1)=h(x⃗,n,f(x⃗,n))f(\vec x,n+1)=h(\vec x,n,f(\vec x,n)) defines a new primitive recursive ff. Addition, multiplication, exponentiation, and every "for-loop" program with a fixed bound are all primitive recursive — and every primitive recursive function is total (defined on all inputs) and halts on a bounded number of steps for each input.

f(x⃗,0)=g(x⃗),f(x⃗,n+1)=h(x⃗,n,f(x⃗,n))f(\vec x,0)=g(\vec x), \qquad f(\vec x,n+1)=h(\vec x,n,f(\vec x,n))

To capture all computable functions — including ones that might not halt — we add one more operator. The **μ\mu-recursive (general recursive) functions add unbounded minimization**: μy. [P(x⃗,y)=0]\mu y.\,[P(\vec x,y)=0] returns the least yy such that P(x⃗,y)=0P(\vec x,y)=0, searching y=0,1,2,…y=0,1,2,\dots — and simply never returns if no such yy exists. This is exactly what makes μ\mu-recursive functions capable of not halting, and Kleene's theorem shows they compute exactly the same class of (partial) functions as Turing machines.

μy. [P(x⃗,y)=0]=min⁡{y:P(x⃗,y)=0}\mu y.\,[P(\vec x,y)=0] = \min\{y : P(\vec x,y)=0\}
Primitive recursive vs. general (μ\mu-)recursive vs. Turing-computable
ClassBuilt fromAlways halts?Example
Primitive recursiveComposition + bounded recursionYes, always total+,×,+,\times, exponentiation
General (μ\mu-)recursivePrimitive recursion + unbounded μ\muNo, may loop foreverAckermann function AA
Turing-computable (partial)State + tape + transition rulesNo, equals μ\mu-recursive exactlyAny algorithm whatsoever

There is no algorithm H(e,x)H(e,x) that, for every program index ee and input xx, always halts and correctly outputs whether φe(x)\varphi_e(x) (running program ee on input xx) halts.

Why is it true?

This is the mathematical reason why no antivirus, compiler, or IDE can ever perfectly detect infinite loops, dead code, or "this function always crashes" in full generality — not a limitation of today's engineering, but a hard mathematical wall.

Proof

Suppose, for contradiction, that such a decider HH exists: H(e,x)=1H(e,x)=1 if φe(x) ⁣↓\varphi_e(x)\!\downarrow (halts) and H(e,x)=0H(e,x)=0 if φe(x) ⁣↑\varphi_e(x)\!\uparrow (runs forever), and HH itself always halts with the correct answer.

Using HH, build a new program DD that, on input ee: computes H(e,e)H(e,e); if H(e,e)=1H(e,e)=1, then DD enters an infinite loop; if H(e,e)=0H(e,e)=0, then DD halts immediately. DD is built effectively from HH (just HH plus an if-statement and a loop), so it has some program index dd, i.e. D=φdD=\varphi_d.

Now ask the self-referential question: does φd(d)\varphi_d(d) halt?

Case 1: if φd(d)\varphi_d(d) halts, then by correctness of HH, H(d,d)=1H(d,d)=1. But by the definition of DD, H(d,d)=1H(d,d)=1 makes DD loop forever on input dd — i.e. φd(d)\varphi_d(d) does not halt. Contradiction.

Case 2: if φd(d)\varphi_d(d) does not halt, then by correctness of HH, H(d,d)=0H(d,d)=0. But by the definition of DD, H(d,d)=0H(d,d)=0 makes DD halt on input dd — i.e. φd(d)\varphi_d(d) does halt. Contradiction.

Both cases are contradictory, so the assumption that HH exists is false. The Halting Problem is undecidable. ■\blacksquare

AdvancedRice's theorem

The Halting Problem is just one example of a much broader phenomenon. Call a property PP of partial computable functions semantic if it depends only on the function φe\varphi_e computed by program ee, not on the source code itself, and nontrivial if some computable function has it and some does not.

For every nontrivial semantic property PP of partial computable functions, the set {e:φe has property P}\{e : \varphi_e \text{ has property } P\} is undecidable.

Why is it true?

This single theorem instantly rules out algorithms for "does this program compute the zero function", "does this program compute a total function", "are these two programs equivalent", and countless other natural questions about program behavior — all in one stroke, without a separate diagonal argument for each.

Proof

WLOG assume the everywhere-undefined function ∅\emptyset (computed by a program that never halts on any input) does not have property PP — otherwise argue with the complement property ¬P\lnot P, which is decidable exactly when PP is. Since PP is nontrivial, fix some program e0e_0 whose function φe0\varphi_{e_0} does have property PP.

Suppose, for contradiction, that PP is decidable by some algorithm DD (given a program index, DD halts and correctly reports whether that program's function has property PP). We reduce the Halting Problem to PP, contradicting Theorem 1.

Given any pair (e,x)(e,x), effectively construct (by simple textual manipulation — Kleene's s-m-n theorem) a new program e′e' that, on any input yy: first simulates program ee running on input xx; if that simulation ever halts, then e′e' goes on to simulate program e0e_0 on input yy and outputs whatever it outputs.

Now examine the two possibilities. If ee halts on xx: the simulation of ee on xx finishes, so e′e' then behaves exactly like e0e_0 on every input, i.e. φe′=φe0\varphi_{e'}=\varphi_{e_0} — which has property PP (since PP is semantic, it only depends on the function computed, and φe0\varphi_{e_0} has PP). If ee does not halt on xx: the simulation of ee on xx never finishes, so e′e' never reaches the e0e_0-simulation step on any input yy; hence φe′\varphi_{e'} is the everywhere-undefined function ∅\emptyset, which by assumption does not have property PP.

So: ee halts on xx   ⟺  \iff φe′\varphi_{e'} has property PP   ⟺  \iff D(e′)D(e') answers "yes". Since (e,x)↦e′(e,x)\mapsto e' is computable, the algorithm "compute e′e' from (e,x)(e,x), then run D(e′)D(e')" would decide the Halting Problem — contradicting Theorem 1. So no such DD exists: PP is undecidable. ■\blacksquare

UndergraduateReal-World Applications and Worked Examples

Compiler optimizers must decide things like "is this code reachable?" or "does this variable's value matter?" — by Rice's theorem, these are undecidable in full generality, which is exactly why real compilers use conservative approximations (they may keep some genuinely dead code rather than risk deleting live code). The Busy Beaver function BB(n)BB(n) — the largest number of steps an nn-state halting Turing machine can take before stopping — is a concrete, uncomputable function: known values are BB(1)=1BB(1)=1, BB(2)=6BB(2)=6, BB(3)=21BB(3)=21, BB(4)=107BB(4)=107, and in 2024 the collaborative Busy Beaver Challenge (led by Tristan Stérin and collaborators, with a Coq-verified proof from a contributor using the pseudonym "mxdys") established BB(5)=47,176,870BB(5)=47{,}176{,}870 — showing that even this "simple" combinatorial question is only computable case-by-case, never by a general algorithm.

Example: Unfolding the Ackermann function A(2,2)A(2,2)

Using the rules A(0,n)=n+1A(0,n)=n+1, A(m,0)=A(m−1,1)A(m,0)=A(m-1,1) for m>0m>0, and A(m,n)=A(m−1,A(m,n−1))A(m,n)=A(m-1,A(m,n-1)) for m,n>0m,n>0, compute A(2,2)A(2,2) step by step, and explain why the Ackermann function is total but not primitive recursive.

Solution

A(2,2)=A(1,A(2,1))A(2,2)=A(1,A(2,1)) by the third rule. We first need A(2,1)=A(1,A(2,0))A(2,1)=A(1,A(2,0)), and A(2,0)=A(1,1)A(2,0)=A(1,1) by the second rule.

A(1,1)=A(0,A(1,0))=A(0,A(0,1))=A(0,2)=3A(1,1)=A(0,A(1,0))=A(0,A(0,1))=A(0,2)=3 (unfolding twice with the second and first rules). So A(2,0)=A(1,1)=3A(2,0)=A(1,1)=3, hence A(2,1)=A(1,3)A(2,1)=A(1,3).

A(1,3)=A(0,A(1,2))A(1,3)=A(0,A(1,2)), and A(1,2)=A(0,A(1,1))=A(0,3)=4A(1,2)=A(0,A(1,1))=A(0,3)=4. So A(1,3)=A(0,4)=5A(1,3)=A(0,4)=5. Hence A(2,1)=5A(2,1)=5.

Back to the top: A(2,2)=A(1,A(2,1))=A(1,5)=A(0,A(1,4))A(2,2)=A(1,A(2,1))=A(1,5)=A(0,A(1,4)); unfolding A(1,4)=A(0,A(1,3))=A(0,5)=6A(1,4)=A(0,A(1,3))=A(0,5)=6; so A(1,5)=A(0,6)=7A(1,5)=A(0,6)=7. So A(2,2)=7A(2,2)=7.

The Ackermann function is proven total (it always eventually reduces to case A(0,n)A(0,n)), so it belongs to the general recursive functions — but it grows faster than every primitive recursive function (e.g. A(3,n)=2n+3−3A(3,n)=2^{n+3}-3, A(4,n)A(4,n) is already a tower of exponents). Since one can show every primitive recursive function is eventually dominated by some fixed A(k,⋅)A(k,\cdot), no primitive recursive function can equal AA itself — a diagonal-style domination argument, not the μ\mu-operator, is what puts Ackermann outside primitive recursion despite being total.

Example: Reducing the Halting Problem to "does this program print hello?"

Show that the problem "given a program ee, does running ee (on no input) ever print the string `hello`?" is undecidable, by a direct reduction from the Halting Problem — without invoking Rice's theorem.

Solution

Suppose, for contradiction, an algorithm Q(e)Q(e) decides whether running ee ever prints `hello`. We use QQ to decide the Halting Problem, contradicting Theorem 1.

Given any program ee and input xx, effectively build a new program e′e' (no input needed) that: simulates ee running on xx; if/when that simulation halts, e′e' then prints `hello` and stops.

If ee halts on xx: the simulation finishes, so e′e' reaches the print step and does print `hello`. If ee does not halt on xx: the simulation never finishes, so e′e' never reaches the print step and never prints `hello`.

So ee halts on xx   ⟺  \iff Q(e′)Q(e') answers "yes". Since (e,x)↦e′(e,x)\mapsto e' is computable, "build e′e' then run Q(e′)Q(e')" decides the Halting Problem — contradicting Theorem 1. So QQ cannot exist: the "prints hello" problem is undecidable. (This is exactly the compiler-analysis pattern: "is this line of code ever reached" has the same shape.)

Using A(1,n)=n+2A(1,n)=n+2, what is A(1,3)A(1,3)?

Which of these is a consequence of the undecidability of the Halting Problem for compiler design?

Which property is Rice's theorem NOT applicable to?

In the diagonal proof of Halting undecidability, what leads to the contradiction?

References

  1. Wikipedia contributors (2024). Halting problem
  2. Wikipedia contributors (2024). Rice's theorem
  3. Wikipedia contributors (2024). Ackermann function