Home/Blog/Programming/Unsolvable Problems: Insights into the Halting Problem and Beyond
Home/Blog/Programming/Unsolvable Problems: Insights into the Halting Problem and Beyond

Unsolvable Problems: Insights into the Halting Problem and Beyond

Imran Farid Khan
May 31, 2023
13 min read

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

It's fair to assume that those who read this blog have experience in writing programs to solve problems. They've likely solved enough problems to justifiably believe that any problem that requires programming is solvable, as long as it’s well-defined. With the advent of generative AI like ChatGPT—that can significantly aid in solving programming problems—this belief will likely become even more pervasive.

However, there are limitations on what can be solved through programming! This blog will discuss the exciting and profound field of computer science, which aims to study computability itself.

Some foundational questions#

Let's ask some obvious but important questions that are foundational.

Are all well-defined problems solvable?#

Given a well-defined problem, is there always an algorithm to solve it?

The answer to the question is no, not always!

This isn't just about limitations of resources, like processing power or memory constraints, or even about the underlying physics of the computers. Some problems just can’t be solved algorithmically in any sort of computer, whether classical or quantum, irrespective of the hardware specifications.

Are these fringe problems?#

Are these problems esoteric, residing only in the minds of theoreticians and of no practical significance?

No, they aren’t. They are very real and relevant problems, such as automated software testing, determining algorithmically whether a specific piece of code in a program is ever executed, or determining whether a given program ever halts.

But there are static analysis tools that warn us about unused code, infinite loops, and the like—don’t they follow an algorithm to determine that?

When a problem is said to be algorithmically unsolvable, this is not to say that it’s impossible to solve all instances of that problem. Some special instances are rather easy to solve. For example, the code reachability problem can be solved for cases where there’s no terminating condition in a loop. Clearly, any code that appears after the loop won’t ever be executed.

However, when a problem is said to be solved algorithmically, that means that there’s an algorithm that can solve all instances of that problem. For example, a sorting program that can sort arrays of only specific types can’t claim to have solved the general sorting problem.

How does a programmer determine if they can't write a program?#

It's simple to tell when a problem can be solved algorithmically. The algorithm is written and tested, or the programmer can utilize techniques learned in an analysis of algorithms course. But how can one know that there’s no algorithm for a problem?

A formal answer to this question would have to begin by defining a computational model, like Alan Turing's Turing machines, Alonzo Church's lambda calculus, or Kurt Gödel's recursive functions. However, the focus will remain on computer programs for the purposes of this blog.

Setting up the framework#

A framework is necessary to answer these questions. The scope will be limited to specific types of problems and programs. This will help keep the explanations crisp and will avoid needless details.

The problems#

This discussion will be limited to decision problems, where the objective is to test whether a given instance of the problem satisfies certain properties. For example, the following problems are decision problems:

  1. Determine whether a given number pp is a prime number, where the number pp defines an instance of the problem.

  2. Determine whether pp divides nn, where the pair pp and nn defines an instance of the problem.

  3. Determine whether the list L2L_2 is a sorted version of a list L1L_1, where the pair L1L_1 and L2L_2 defines an instance of the problem.

The programs#

This discussion will be limited to programs with a specific structure because these suffice for the decision problems. Our programs will have the following features:

  1. They take only one input parameter in string format.

  2. Their return type is boolean, i.e. True/False.

  3. They can simulate another program on an input if an encoding of both the program and its input are passed as an input to our program.

  4. They have no limitations on their memory usage.

The first condition doesn't limit generality because multiple inputs of different types can be consolidated by casting and encoding them all in one string with a delimiter distinguishing between different parameters. This condition is relaxed in some of the examples below to keep the explanations simple. The third assumption should also not cause any distress. Programs can be easily passed as inputs to other programs. Think of compilers that take the source code of another program as input, or of functions that can be passed as parameters in programming languages in which functions are first-class citizens. If we were working with Turing machines, this assumption would be justified by describing the mechanics of a universal Turing machine that can easily simulate other Turing machines.

Writing programs to solve decision problems #

Decision problems are natural candidates for the kind of programs described above. Given an instance of a decision problem, a program can be written that returns True if the instance has the specific properties and otherwise, False.

Importantly, if there's no such program that can solve a decision problem, it's definitely unsolvable.

Halting vs non-halting programs#

Note that when such a program is run on an input, there can be three kinds of results:

  1. It halts and returns True.

  2. It halts and returns False.

  3. It doesn't halt and continues computation forever.

This third condition holds, for instance, if there is a loop that never terminates.

Naturally, when a program solves a decision problem, it should either return True or False, and it shouldn't run into any infinite computation on any input.

The halting problem#

Given a program PP and the input ss, can it be algorithmically decided—via a program HH—whether PP would halt on ss?

HH takes two inputs (encoded as one, as discussed above): a program PP and a string ss. HH returns True if the program PP halts on ss with either True or False, and HH returns False if the program PP never halts on input ss.

The unsolvability of the halting problem#

The halting problem isn't solvable algorithmically.

Suppose such a program HH existed. It can be used to write another program, DD , with the following behavior:

DD would take a program PP as input. It’d then call HH and pass PP as both the input program and the input string. That is, it'd call H(P,P)H(P, P) (forget the “why” part for now, and just know that it’s possible to pass a program as an input).

  • If HH tells DD that PP would not halt on PP as input, DD returns True.

  • If HH tells DD that PP would halt on PP as input, DD enters an infinite loop.

The program D
The program D

It’s a straightforward matter to write DD provided that HH exists.

Now, to address the question "Why was DD designed this way?", let's consider what happens if we pass DD itself as input to DD.

Note: If it seems strange seeing a program being passed as input to itself, think of passing the code of a compiler to itself as input when using it to compile itself!

Replacing DD for PP above,

D(D)={TrueIf D does not halt on DLoops foreverIf D halts on DD(D) = \begin{cases} \text{True}& \text{If }D \text{ does not halt on }D \\\text{Loops forever}& \text{If }D \text{ halts on } D \end{cases}

That is,

  • If DD doesn’t halt on input DD, DD halts on input DD.
  • If DD halts on input DD, DD loops forever on input DD.

Both of these possibilities are self-contradictory. Therefore, they can’t happen. Logic dictates that such a program DD can’t exist. But every step in writing DD is clearly doable, assuming that HH exists. Hence, HH can’t exist.

Thus, the halting problem is unsolvable!

What's achieved here is quite remarkable. The above section just demonstrated that it is impossible to solve a problem algorithmically!

D for diagonalization#

This argument employs the diagonalization argument that was initially presented by Georg Cantor to prove that real numbers are uncountable. That is, there're so many real numbers that it’s impossible to even specify a way of listing them all in one infinite list, as opposed to, say, natural numbers that can be listed, like 1,2,3,1, 2, 3, \cdots.

Consequences#

Now, it’s time to face the consequences! This impossibility leads to other impossibilities.

The code reachability problem#

Suppose there’s a program PP, a piece of code CC that appears in PP, and an input string ss. Can someone write a program to determine whether the code CC is ever executed during PP’s execution on input ss?

The answer is no.

The unsolvability of the code reachability problem#

We'll demonstrate below that if the code reachability problem is solvable, so is the halting problem. As was illustrated above, the halting problem isn’t solvable, this would imply that the code reachability problem isn’t solvable either. In other words, we'll demonstrate that the halting problem is reducible to the code reachability problem, thereby establishing it isn't solvable.

Suppose a program RR existed that solved the code reachability problem. We’ll write another program HH to solve the halting problem as follows:

HH would take a program PP and a string ss as input to determine whether PP halts on ss or not.

As the first step, HH would generate another program QQ (yes, a program writing another program) that would simulate PP on ss. After the code for simulation, some dummy code CC is added (like a useless initialization).

The program H solves the halting problem using the program R
The program H solves the halting problem using the program R

Now, HH runs the decider RR for the code reachability problem on the input QQ, CC, ss.

  • If RR returns True, that means the code CC is executed, implying that the simulation of PP on ss terminates. So, HH should return True since PP halts on ss.

  • If RR returns False, that means the code CC is never executed, implying that the simulation of PP on ss doesn't terminate. So, HH should return False since PP doesn't halt on ss.

In other words, HH solves the halting problem, which is impossible. Ergo, RR can't exist. And the code reachability problem is unsolvable!

One can use similar reductions to show that almost every meaningful question about computer programs is unanswerable programmatically.

Software verification problem#

The software verification problem asks: given a bunch of formally written requirements, FF, and a program PP , is there a program VV that can tell whether the program PP satisfies all of the requirements FF?

If such an algorithm existed, quality assurance departments all over the world would have to fight hard to justify their existence! To test whether the programs are working according to the given requirements, the requirements would need to be formally specified and the verifier VV would need to be called on the requirements and a program claiming to satisfy them all. And if VV okays it, the program can be put directly into production.

But unfortunately, this problem can’t be solved either. Here’s why:

The unsolvability of the software verification problem#

Assume the software verification problem is solvable, that is, the program VV indicated above exists. We’ll write a program HH to solve the halting problem.

HH would take a program PP and a string ss as inputs.

Just like before, it will construct another program QQ that simulates PP on ss, and after the simulation code, HH prints some unique string uu. Then it writes a formal requirement FF that the program must print the unique string uu, and passes both FF and QQ to VV, which supposedly solves the software verification problem.

The program H solves the halting problem using the program V
The program H solves the halting problem using the program V
  • Now, if VV determines that the requirement FF is met by the program QQ, it’d imply the simulation of PP on ss ends (as uu is printed only afterward). This implies that PP halts on ss. So, HH returns True in this case.

  • On the other hand, if VV determines QQ doesn't meet the requirement FF, the string uu is never printed, indicating that the simulation of PP on ss never ends . This implies that that PP doesn't halt on ss. So, HH returns False in this case.

Clearly, this shows that the halting problem can be solved, which is impossible. Hence, the software verification problem is unsolvable as well.

The Entscheidungsproblem#

This is arguably the most ambitious question ever asked in the history of mathematics. In 1928, David Hilbert and Wilhelm Ackermann—based on a musing of Leibniz—asked whether it was possible to automate the process of finding proofs of mathematical statements. That is, given a mathematical claim, is it possible to determine algorithmically whether it’s provable or unprovable? Thereby reducing the jobs of mathematicians to asking meaningful questions and relegating the grunt work of proving or disproving them to this algorithm. This never came to be as this problem too is unsolvable. It was shown to be unsolvable by Alan Turing in the same paper that introduced Turing machines.


  

Free Resources