Python is a Turing machine

We build the rigorous bridge between concrete Python programs and abstract Turing machines through a chain of simulations: single-tape TM → multi-tape TM → RAM machine → modern computer → Python program → single-tape TM. This equivalence justifies using Python as the formal model of computation throughout the series, and anchors the result within the Church–Turing thesis.

44 min read

We have perfected the general-purpose computer for scientific calculations. Now we try to employ the same device with small modifications in a great variety of different situations for which it was not designed in the first place. In these circumstances, if it turned out that the basic logic of a machine designed for the numerical solution of differential equations coincided with the basic logic of a machine intended to make invoices for a department store, I would consider this the most astonishing coincidence I have ever encountered.

Howard Aiken1

The Turing machine, invented by Alan Turing in 19372, is a simple mathematical model that aims to represent the notion of an algorithm, which we discuss in detail in Section 2.2. This machine is one of the most relevant tools in computer science, since its simplicity allows powerful results to be obtained, such as the undecidability of the halting problem (Proposition 3.24), from which we can delve into the main question of our work: the incompleteness of mathematics. However, this formal power has a drawback, which we will see from a simple example (Example 2.11): defining simple programs in this model is complex and tedious.

This drawback is what motivates this chapter: in Section 2.3 we will prove the equivalence between computer programs (in our case, Python programs, which we explore in Section 2.1) and Turing machines. This will allow us to view a Python program as a representation of a Turing machine, eliminating the main drawback while retaining the mathematical power already mentioned.

It is important to emphasize that the results presented here are perfectly generalizable to any Turing-complete programming language. However, we chose to use this language for its ease of understanding and use.

Both the equivalence result we prove in this chapter and the observation above are especially interesting, as they bear a strong relation to the Church–Turing thesis, which we briefly discuss in Section 2.4.

2.1 #Python Programs

Python3 is a high-level, interpreted, object-oriented, open-source programming language, widely used today for a great variety of applications. One of its main characteristics is its readability and ease of use: anyone experienced in other languages will be able to understand and write Python programs with great ease.

Using Python, we can write a great variety of programs. For the purpose of our study, we focus on a specific class of programs: those that have exactly one character string as input and one character string as output. We call them SISO programs (from Single Input–Single Output; see Definition 2.2)4. Shortly, we will see that this class is not restrictive: these types of programs are as expressive as any other program. In fact, in this work we use the terms "program" and "SISO program" interchangeably, and in both cases we refer to a SISO-type Python program.

Definition2.1—SISO function

We define a SISO function as a Python function that accepts a single parameter of type str as input and returns a str.

Definition2.2—SISO Python program

With respect to a reference computer system CC, we define a SISO Python program as a character string PP such that:

  • PP is syntactically correct Python code;
  • PP defines at least one function, and the first of them is of SISO type. We call this function the main function.

Note that in the previous definition a computer system CC is specified but not defined explicitly. We can think of CC as any computer system, with any software and hardware configuration, and with as much memory as necessary.

We may also ask what we mean when we refer to a program as "syntactically correct," since this will depend on several factors, such as the version of Python installed on CC, as well as the operating system, architecture, etc. It is possible that a program runs on one system CC and fails or even produces a different output on another system C′C'. However, as we will see later, we use these programs as representations of Turing machines. The programs we present here are architecture- and memory-independent, and the proofs in this work are independent of all these characteristics.

We also need to define the output of a Python program.

Definition2.3—Output of a program

Let PP be a Python program with respect to a computer system CC. Let MM be the main function of PP, and let II be the input of PP, a character string. The output of PP with respect to input II, P(I)P(I), is produced by using CC to execute PP with input II, and is defined as:

  • If MM returns a character string OO, then P(I)=OP(I) = O.
  • If MM returns any other Python object, then P(I)P(I) is undefined.
  • If MM raises an exception,5 then P(I)P(I) is undefined.
  • If MM does not terminate, then P(I)P(I) is undefined.

Note. The above definition can easily be generalized to nn inputs. Replacing II by I1,I2,…,InI_1, I_2, \ldots, I_n yields the corresponding definition for P(I1,I2,…,In)P(I_1, I_2, \ldots, I_n).

In other words, the output of a program is the character string returned by its main function if it does so, and is undefined otherwise.

When defining the Python programs we use in this work, we focus on using character strings (also called strings) as input and output, and avoid using other Python objects that are not strings. Initially, we might think this imposes a limitation on the Python programs we can write, but this is not really the case. To see this, it suffices to observe that any object in Python can be properly encoded as a character string. There are native libraries that allow this, such as pickle.6 An example is shown in Program 2.1. The function function_with_encoded_object is a main function.

import pickle
from base64 import b64decode, b64encode
 
def function_with_encoded_object(encoded: str) -> str:
decoded = pickle.loads(b64decode(encoded.encode('ascii')))
return str(decoded)
 
adjacencies = {1: 2, 3: 4, 5: 3, 4: 2}
adjacencies_encoded = b64encode(pickle.dumps(adjacencies)).decode('ascii')
function_with_encoded_object(adjacencies_encoded)

pickle.dumps serializes the object to bytes, not to str. We use Base64 to encode those bytes as an ASCII string and decode them again before calling pickle.loads. The function also returns a string, so both its input and its output satisfy the SISO definition.

In summary, we have seen that the SISO condition for a Python program is not really a restriction. However, this class of problems will be very convenient for us, as we will see later.

2.2 #Turing Machines

Turing machines, first described by Alan Turing in 19362, are abstract and simple computational devices created to help investigate which problems can be computed. The "automatic machines"—as Turing called them in 1936—were specifically designed to compute real numbers. These machines were not named "Turing machines" until 1937, when Alonzo Church7 reviewed Turing's original publication. Today they are considered one of the fundamental models of computation theory, as they are the mathematical representation of an algorithm8.

Before understanding how these machines work, we need a few preliminary definitions.

Definition2.4—Alphabet, symbol, word, length, empty word

An alphabet AA is a finite set. Its elements are called symbols (or letters).

A word over alphabet AA is a finite sequence of elements of AA. That is, uu is a word over AA if and only if u=a1 a2 … anu = a_1\,a_2\,\ldots\,a_n, with ai∈Aa_i \in A for all i=1,2,…,ni = 1, 2, \ldots, n.

The set of all words over an alphabet is denoted A∗A^*.9

The length of a word uu is the number of symbols it contains, denoted ∥u∥\|u\|. For u=a1 a2 … anu = a_1\,a_2\,\ldots\,a_n, we have ∥u∥=n\|u\| = n.

The empty word is the word of length zero. It is the same for all alphabets, and is denoted ϵ\epsilon.

The first definition presented by Turing in 19362 differs from the one in Definition 2.5. However, both are equivalent.

Definition2.5—Turing machine

A Turing machine is a 7-tuple M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F), where:

  • Q={q0,…}Q = \{q_0, \ldots\} is a (finite) set of states.
  • AA is an input alphabet.
  • BB is a tape alphabet (or work alphabet), which includes AA (i.e., A⊂BA \subset B).
  • δ:Q×B⟶(Q×B×{L,R,S})∪{∅}\delta : Q \times B \longrightarrow (Q \times B \times \{L, R, S\}) \cup \{\emptyset\} is the transition function that assigns to each state q∈Qq \in Q and symbol b∈Bb \in B a value δ(q,b)\delta(q, b) which can be:
  • ∅\emptyset if undefined, or
  • a triple (p,c,M)(p, c, M), where p∈Qp \in Q, c∈Bc \in B, M∈{L,R,S}M \in \{L, R, S\}.
  • q0∈Qq_0 \in Q is the initial state.
  • ⊔∈B∖A\sqcup \in B \setminus A is the blank symbol.
  • F⊆QF \subseteq Q is the set of final states.

The above definition is somewhat abstract, and we can visualize it better in Figure 2.1. In it, we see the three "physical" parts of the machine: the control unit, the head, and the tape. The tape contains cells, each of which can store one symbol. Initially, the symbols are those of the input alphabet, but as the machine executes instructions the work alphabet can be used. These symbols include a blank symbol, denoted ⊔\sqcup, occupying all tape positions that do not encode the input word. The head can move left and right along the tape (or stay in place), informing the control unit of the symbols it reads. The symbol pointed to by the head is called the scanned symbol. The head can replace the scanned symbol with any other from the work alphabet, including the blank symbol. The control unit sends transitions to the head and changes between states based on the symbols read by the head.

qn¡1qnq0q1q2t010t::::::inputcontrolunitcurrentstatetapehead
Figure 2.1. Initial configuration of a Turing machine for input 010010.

It is important to clarify that a Turing machine does not really have any "control unit" or "head": it is a mathematical description. What we have done is provide an intuitive specification of how the algorithm represented by such a machine is executed.

To describe the machine at each moment, we need the concept of configuration.

Definition2.6—Configuration of a Turing machine

We specify the configuration of a Turing machine by two parameters:

  • The state q∈Qq \in Q in which the machine currently is.
  • The word w∈B∗w \in B^* present on the tape and the position of the read head, which we represent by boxing the symbol where the head is located. Since the tape extends infinitely on both sides, we omit blank characters to the left and right up to the head position.

We represent a configuration as

q  :  wq\;:\;w

with one of the symbols of ww suitably boxed.

All that remains is to specify the notion of "computation step" mentioned above. At each step, we apply a transition and change the configuration of the machine. This is what we call the computation process, which we formalize next.

Definition2.7—Computation process of a Turing machine

Given a Turing machine M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F) and an initial input u=u1 u2 … un∈A∗u = u_1\,u_2\,\ldots\,u_n \in A^*, we define its initial configuration as:

q0  :  u1 u2 ... unq_0\;:\;\boxed{u_1}\:u_2\:...\:u_n

We then define a computation step for each configuration of the machine:

q  :  b−n ... b−2 b−1 b0 b1 b2 ... bmq\;:\;b_{-n}\:...\:b_{-2}\:b_{-1}\:\boxed{b_0}\:b_1\:b_2\:...\:b_m

with q∈Qq \in Q, b=b−n ... b−2 b−1 b0 b1 b2 ... bm∈B∗b = b_{-n}\:...\:b_{-2}\:b_{-1}\:b_0\:b_1\:b_2\:...\:b_m \in B^*, depending on the value of the transition function δ(q,b0)\delta(q, b_0):

  • If δ(q,b0)\delta(q, b_0) is undefined (δ(q,b0)=∅\delta(q, b_0) = \emptyset), the machine halts.
  • If δ(q,b0)=(p,c,M)\delta(q, b_0) = (p, c, M) with p∈Qp \in Q, c∈Bc \in B, M∈{L,R,S}M \in \{L, R, S\}, the machine replaces the symbol b0b_0 at the head position by cc, moves the head one position according to MM (left, right, or not at all if MM is LL, RR, or SS, respectively), and changes its state to pp:
M=L⇒p  :  b−n ... b−2 b−1 c b1 b2 ... bmM=R⇒p  :  b−n ... b−2 b−1 c b1 b2 ... bmM=S⇒p  :  b−n ... b−2 b−1 c b1 b2 ... bm\begin{matrix} M=L & \Rightarrow & p\;:\;b_{-n}\:...\:b_{-2}\:\boxed{b_{-1}}\:c\:b_1\:b_2\:...\:b_m\\ M=R & \Rightarrow & p\;:\;b_{-n}\:...\:b_{-2}\:b_{-1}\:c\:\boxed{b_1}\:b_2\:...\:b_m\\ M=S & \Rightarrow & p\;:\;b_{-n}\:...\:b_{-2}\:b_{-1}\:\boxed{c}\:b_1\:b_2\:...\:b_m \end{matrix}

The computation process is defined as the succession of computation steps, starting from the initial configuration.

It is possible that the machine halts when it reaches a configuration for which the transition function is undefined.10 If this does not occur, the sequence of computation steps will be infinite and the machine will never halt.

In short, we see that the machine, depending on its current state and the scanned symbol, executes a certain transition defined in δ\delta, which tells it which state to move to, which symbol to replace the scanned one with, and which movement to perform on the tape.

An important note regarding undefined transitions. When describing a transition function, we define only the transitions for the states q∈Qq \in Q and symbols b∈Bb \in B for which they are defined. Otherwise, we may assume that δ(q,b)=∅\delta(q, b) = \emptyset. You can see this in Example 2.11, which we discuss in detail later.

We introduce a graphical notation for the transition function. We define it by means of a directed graph: we represent states as nodes (one for each state) and transitions as directed arcs.

The initial state is symbolized, in addition to the definition of the machine, by an arrow pointing to the state (it is not an arc, since it does not come from any node):

q0

For each transition δ(q,b)=(p,c,M)\delta(q,b)=(p,c,M), we insert a directed arc connecting the nodes corresponding to states qq and pp as follows:

qpb;c;M

If we have several transition functions between the same two states (δ(q,b)=(p,c,M)\delta(q,b)=(p,c,M) and δ(q,b′)=(p,c′,M′)\delta(q,b')=(p,c',M')), we place several lines on the arc to symbolize this:

qpb;c;Mb0;c0;M0

Finally, we also indicate final states by double-circling the node:

qF

We observe that a Turing machine, given an input, can:

  • halt and accept,
  • halt and reject, or
  • not halt (loop).

The machine halts when it reaches a configuration for which no transition is defined. When this happens, we can define an output from the symbols remaining on the tape.

Definition2.8—Output of a Turing machine

For a Turing machine MM, we define its output as the contents of the tape once the machine has halted, omitting blank symbols on both sides.

Definition2.9—Word accepted by a Turing machine

Given a Turing machine M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F) and an initial input u∈A∗u \in A^*, we say that MM accepts uu if MM halts on input uu in a state q∈Fq \in F. If it halts in a state q∉Fq \notin F, we say that MM rejects uu.

Having defined which words a given machine accepts, we introduce the concept of language. A language is nothing more than a set of words.

Definition2.10—Language accepted by a Turing machine

A language LL over an alphabet AA is a set of words over AA.

If M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F) is a Turing machine, the accepted language, denoted L(M)L(M), is the set of words accepted by MM, that is,

u∈L(M)  ⟺  M accepts uu \in L(M) \iff M \text{ accepts } u

Let us make all the concepts we have introduced precise through an example.

Example2.11

Let M#a>#bM_{\#a>\#b} be a Turing machine such that:

M#a>#b=({q0,qa,qb,qR,qF},{a,b},{a,b,X,⊔},δ,q0,⊔,{qF})M_{\#a>\#b} = (\{q_0, q_a, q_b, q_R, q_F\}, \{a, b\}, \{a, b, X, \sqcup\}, \delta, q_0, \sqcup, \{q_F\})

with δ\delta, its transition function, described by the graph:

q0qRqbqaqFX;X;Rt;t;Ra;X;Rb;X;La;a;RX;X;Rb;b;RX;X;Ra;a;Lb;b;LX;X;Lb;X;Ra;X;Lt;t;S
Figure 2.2. The transition function δ\delta of M#a>#bM_{\#a>\#b}.

Equivalently, we can define the transition function explicitly:

δ(q0,a)=(qa,X,R)δ(q0,b)=(qb,X,R)δ(q0,X)=(q0,X,R)δ(qa,a)=(qa,a,R)δ(qa,b)=(qR,X,L)δ(qa,X)=(qa,X,R)δ(qa,⊔)=(qF,⊔,S)δ(qb,a)=(qR,X,L)δ(qb,b)=(qb,b,R)δ(qb,X)=(qb,X,R)δ(qR,a)=(qR,a,L)δ(qR,b)=(qR,b,L)δ(qR,X)=(qR,X,L)δ(qR,⊔)=(q0,⊔,R)\begin{matrix} \delta(q_0, a) = (q_a,X,R) & \delta(q_0, b) = (q_b,X,R) & \delta(q_0, X) = (q_0,X,R) & \\ \delta(q_a, a) = (q_a,a,R) & \delta(q_a, b) = (q_R,X,L) & \delta(q_a, X) = (q_a,X,R) & \delta(q_a, \sqcup) = (q_F,\sqcup,S) \\ \delta(q_b, a) = (q_R,X,L) & \delta(q_b, b) = (q_b,b,R) & \delta(q_b, X) = (q_b,X,R) & \\ \delta(q_R, a) = (q_R,a,L) & \delta(q_R, b) = (q_R,b,L) & \delta(q_R, X) = (q_R,X,L) & \delta(q_R, \sqcup) = (q_0,\sqcup,R) \\ \end{matrix}

Let us see what this machine does. It starts at the beginning of the tape and scans symbols one by one, from left to right. There are three possibilities, depending on whether we see aa, bb, or ⊔\sqcup:

  1. If a bb is read:
  2. Replace the bb with an XX.
  3. Continue scanning to the right.
  4. If we find an aa, replace it with an XX. We will have replaced the same number of aa's as bb's, so we move to the beginning of the tape and restart the algorithm.
  5. If we do not find an aa, we have one extra bb, so we reject.
  6. If an aa is read:
  7. Replace the aa with an XX.
  8. Continue scanning to the right.
  9. If we find a bb, replace it with an XX. We will have replaced the same number of aa's as bb's, so we move to the beginning of the tape and restart the algorithm.
  10. If we do not find a bb, we have one extra aa, so we accept.
  11. If we find neither aa nor bb before the ⊔\sqcup at the end of the input: the number of replaced aa's equals the number of replaced bb's, so we reject.

The language accepted by this machine is the set of words over {a,b}∗\{a,b\}^* with more aa's than bb's:11

L(M#a>#b)={u∈{a,b}∗  :  #(u,a)>#(u,b)}L(M_{\#a>\#b}) = \left\{ u \in \{a,b\}^* \;:\; \#(u,a) > \#(u,b) \right\}

Here is an example of an accepted word: abbaaabbaa. The steps the Turing machine takes, configuration after configuration, are the following:

q0  :  a b b a a→qa  :  X b b a a→qR  :  X X b a a→qR  :  ⊔ X X b a a→q0  :  X X b a a→q0  :  X X b a a→q0  :  X X b a a→qb  :  X X X a a→qR  :  X X X X a→qR  :  X X X X a→qR  :  X X X X a→qR  :  ⊔ X X X X a→q0  :  X X X X a→q0  :  X X X X a→q0  :  X X X X a→q0  :  X X X X a→q0  :  X X X X a→qa  :  X X X X X ⊔→qF  :  X X X X X ⊔\begin{matrix} q_0\;:\;\boxed{a}\:b\:b\:a\:a & \rightarrow & q_a\;:\;X\:\boxed{b}\:b\:a\:a & \rightarrow & q_R\;:\;\boxed{X}\:X\:b\:a\:a & \rightarrow & q_R\;:\;\boxed{\sqcup}\:X\:X\:b\:a\:a \\ & \rightarrow & q_0\;:\;\boxed{X}\:X\:b\:a\:a & \rightarrow & q_0\;:\;X\:\boxed{X}\:b\:a\:a & \rightarrow & q_0\;:\;X\:X\:\boxed{b}\:a\:a \\ & \rightarrow & q_b\;:\;X\:X\:X\:\boxed{a}\:a & \rightarrow & q_R\;:\;X\:X\:\boxed{X}\:X\:a & \rightarrow & q_R\;:\;X\:\boxed{X}\:X\:X\:a \\ & \rightarrow & q_R\;:\;\boxed{X}\:X\:X\:X\:a & \rightarrow & q_R\;:\;\boxed{\sqcup}\:X\:X\:X\:X\:a & \rightarrow & q_0\;:\;\boxed{X}\:X\:X\:X\:a \\ & \rightarrow & q_0\;:\;X\:\boxed{X}\:X\:X\:a & \rightarrow & q_0\;:\;X\:X\:\boxed{X}\:X\:a & \rightarrow & q_0\;:\;X\:X\:X\:\boxed{X}\:a \\ & \rightarrow & q_0\;:\;X\:X\:X\:X\:\boxed{a} & \rightarrow & q_a\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} & \rightarrow & q_F\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} \end{matrix}

Since the machine halts in state qF∈Fq_F \in F, the word is accepted.

Here is an example of a word that is not accepted, namely abbabb:

q0  :  a b b→qa  :  X b b→qR  :  X X b→qR  :  ⊔ X X b→q0  :  X X b→q0  :  X X b→q0  :  X X b→qb  :  X X X ⊔\begin{matrix} q_0\;:\;\boxed{a}\:b\:b & \rightarrow & q_a\;:\;X\:\boxed{b}\:b & \rightarrow & q_R\;:\;\boxed{X}\:X\:b & \rightarrow & q_R\;:\;\boxed{\sqcup}\:X\:X\:b \\ & \rightarrow & q_0\;:\;\boxed{X}\:X\:b & \rightarrow & q_0\;:\;X\:\boxed{X}\:b & \rightarrow & q_0\;:\;X\:X\:\boxed{b} \\ & \rightarrow & q_b\;:\;X\:X\:X\:\boxed{\sqcup} \end{matrix}

Since the machine halts in state qb∉Fq_b \notin F, the word is not accepted.

The machine also allows the empty input (ϵ\epsilon, defined in Definition 2.4), in which case the sequence of configurations will be:

q0  :  ⊔\begin{matrix} q_0\;:\;\boxed{\sqcup} \end{matrix}

The machine halts immediately in q0∉Fq_0 \notin F, and ϵ\epsilon is not accepted. This is expected, since ϵ\epsilon has exactly the same number of aa's as bb's.

These examples are summarized in Table 2.1, together with the behavior of the machine for other words. Observe when each word is accepted, as well as the number of transitions after which the machine halts.

Once we have described precisely what a Turing machine is, we proceed to prove the most important result of this chapter (Theorem 2.23), which we anticipated in the introduction: the equivalence between the Turing machines just presented and the Python programs we defined in Section 2.1.

Input Initial config. Config. at halt Accepted? No. transitions
abbaaabbaa q0  :  a b b a aq_0\;:\;\boxed{a}\:b\:b\:a\:a →\rightarrow qF  :  X X X X X ⊔q_F\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} yes 18
abbabb q0  :  a b bq_0\;:\;\boxed{a}\:b\:b →\rightarrow qb  :  X X X ⊔q_b\;:\;X\:X\:X\:\boxed{\sqcup} no 7
ϵ\epsilon q0  :  ⊔q_0\;:\;\boxed{\sqcup} →\rightarrow q0  :  ⊔q_0\;:\;\boxed{\sqcup} no 0
abababab q0  :  a b a bq_0\;:\;\boxed{a}\:b\:a\:b →\rightarrow q0  :  X X X X ⊔q_0\;:\;X\:X\:X\:X\:\boxed{\sqcup} no 16
ababbababb q0  :  a b a b bq_0\;:\;\boxed{a}\:b\:a\:b\:b →\rightarrow qb  :  X X X X X ⊔q_b\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} no 17
ababaababa q0  :  a b a b aq_0\;:\;\boxed{a}\:b\:a\:b\:a →\rightarrow qF  :  X X X X X ⊔q_F\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} yes 18
abaaababaaab q0  :  a b a a a bq_0\;:\;\boxed{a}\:b\:a\:a\:a\:b →\rightarrow qF  :  X X X X a X ⊔q_F\;:\;X\:X\:X\:X\:a\:X\:\boxed{\sqcup} yes 23
Table 2.1. Examples of the behavior of M#a>#bM_{\#a>\#b} with various inputs.

2.3 #Equivalence

Our goal now is to understand why Turing machines have the same computational power as Python programs. In other words, we want to show that the problems we can solve in both are the same.12

To prove the equivalence between both models of computation, we use the concept of simulation. It is common for different computational models to simulate one another. An example is the Java Virtual Machine (JVM), which is an abstract model capable of executing any Java program13. Another example is the Linux operating system, which can run the JVM. Therefore, a Linux computer can execute any Java program. This is possible through the chain of simulations:

Linux⇝JVM⇝Java program\text{Linux} \leadsto \text{JVM} \leadsto \text{Java program}

We use the notation A⇝BA \leadsto B to indicate that AA simulates BB. The fact that AA simulates BB simply means that it is possible to perform the computation procedure of BB using exclusively the computation procedure of AA.

We proceed to prove the equivalence shown by means of the chain of simulations in Figure 2.6.

single-tapeTuringmachinemulti-tapeTuringmachinerandom-accessmachinePythonprogramVonNeumanncomputer
Figure 2.6. Chain of simulations.

The work we do in this section is arduous, and we need to introduce several computational models to reach our goal. However, once we prove all the simulations, we will know that all of them are equivalent, and we can use whichever model we prefer as convenient.

The first computational model we study is obtained by adding more tapes and heads to the Turing machine we defined in Section 2.214.

Definition2.12—Multi-tape Turing machine

A multi-tape Turing machine with kk tapes is a conventional Turing machine in which we have kk tapes and kk read heads. In this sense, the configuration of a multi-tape Turing machine is described as:

q  :  w1  ,  w2  ,  ...  ,  wkq\;:\;w_1\;,\;w_2\;,\;...\;,\;w_k

where each tape word has one of its symbols boxed, symbolizing the head position on each tape.

The function δ:Q×Bk⟶(Q×(B×{L,R,S})k)∪{∅}\delta : Q \times B^k \longrightarrow \left(Q \times (B \times \{L, R, S\})^k\right) \cup \{\emptyset\} will be of the form:

δ(q,b1,b2,...,bk)=∅\delta(q, b_1, b_2, ..., b_k) = \emptyset

for undefined transitions, and for defined ones:

δ(q,b1,b2,...,bk)=(p,c1,M1,c2,M2,...,ck,Mk),\delta(q, b_1, b_2, ..., b_k) = (p, c_1, M_1, c_2, M_2, ..., c_k, M_k),

with q,p∈Qq,p\in Q, bi,ci∈Bb_i, c_i \in B, Mi∈{L,R,S}M_i \in \{L, R, S\}, for all i∈{1,2,...,k}i \in \{1, 2, ..., k\}.

The computation process will be analogous to that of a single-tape Turing machine, keeping in mind that symbol bib_i is read by the ii-th head, that cic_i is the symbol replacing bib_i on the ii-th head, and MiM_i is the movement the ii-th head will follow.

The input of the machine is placed on the first tape, with the head on the first symbol of the word. The remaining heads point to the blank symbol, since all tapes except the first are filled with blank symbols.

Words accepted by the machine proceed analogously to the single-tape case, depending on whether the state where the machine halts is final or not (is in FF).

qn¡1qnq0q1q2t010t::::::inputtape1head1ttttt::::::tape2ttttt::::::tapek...head2headk...
Figure 2.7. Diagram of a multi-tape Turing machine in its initial configuration.

This definition is intuitive and expected. We simply incorporate more tapes and heads. From now on, we also use the term "single-tape Turing machine" to refer to the Turing machine we defined first.

One might think that these machines have more computational power than single-tape ones, but they are equivalent. It is evident that a multi-tape machine simulates a single-tape machine (Definition 2.5 is the same as Definition 2.12 for k=1k=1). In Proposition 2.13 we see that the converse also holds.

Proposition2.13

Every multi-tape Turing machine can be simulated by a single-tape Turing machine.

Proof

Proof.15 Let M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F) be a kk-tape Turing machine. We create a machine M~=(Q~,A,B~,δ~,q0,[⊔k],F)\widetilde{M} = (\widetilde{Q}, A, \widetilde{B}, \widetilde{\delta}, q_0, [\sqcup^k], F) with a single tape such that L(M)=L(M~)L(M) = L(\widetilde{M}).

To simulate kk tapes and kk heads with a single head and a single tape, we must extend the work alphabet B~\widetilde{B}. First, we introduce new symbols to know the position of each head. We define:

Bˇ={bˇ:b∈B}\widecheck{B} = \{\widecheck{b} : b \in B\}

Having done this, we define B~\widetilde{B}, the alphabet of M~\widetilde{M}, as:

B~={[b1 b2 ... bk]:b∈B∪Bˇ}∪{#}\widetilde{B} = \{[b_1\:b_2\:...\:b_k] : b \in B \cup \widecheck{B}\} \cup \{\#\}

Observe that the symbols of B~\widetilde{B} are either a delimiter #\#, or a group of characters from B∪BˇB \cup \widecheck{B}. In this way, we can simulate kk tapes and kk heads on a single tape with a single head. For example, if in MM we have the configuration:

q  :  0 1 0  ,  X X X  ,  Y Xq\;:\;0\:\boxed{1}\:0\;,\;X\:X\:\boxed{X}\;,\;\boxed{Y}\:X

One possible configuration in M~\widetilde{M} would be:

q  :  # [0 X Yˇ] [1ˇ X X] [0 Xˇ ⊔] #q\;:\;\fbox{\#}\:[0\:X\:\widecheck{Y}]\:[\widecheck{1}\:X\:X]\:[0\:\widecheck{X}\:\sqcup]\:\#
t010t......tXXXt......tYXtt......ttt01...0tt...tXXXt...ttt...ttttt...YXt...q:#[0XHY][H1XX][0HXt]#q:#[tXt][tXt][0HXt][H1tt][0tHY][ttX]#
Figure 2.8. Two configurations of M~\widetilde{M} for one configuration of MM.

Observe that several configurations of M~\widetilde{M} are possible, since the kk tapes are independent (it does not matter how we "stack" them, and this has no effect on the computation process). This feature of our encoding is shown in Figure 2.8. However, this independence means this peculiarity does not affect us: we simply let the machine keep computing and apply transitions and modify them as dictated.

Describing exactly the set of states Q~\widetilde{Q} and the transition function δ~\widetilde{\delta} is an arduous process. In this proof we limit ourselves to explaining its behaviour; the full construction is given in Proposition 2.14 below.

The machine begins with the initial configuration for the input u=u1 u2 ... un∈A∗u = u_1\:u_2\:...\:u_n \in A^*:

q0  :  # [u1ˇ ⊔ˇ ... ⊔ˇ⏟k−1] [u2 ⊔ ... ⊔⏟k−1] ... [un ⊔ ... ⊔⏟k−1] #q_0\;:\;\fbox{\#}\:[\widecheck{u_1}\:\underbrace{\widecheck{\sqcup} \:... \:\widecheck{\sqcup}}_{k-1}]\:[u_2\:\underbrace{{\sqcup} \:... \:{\sqcup}}_{k-1}]\:...\:[u_n\:\underbrace{{\sqcup} \:... \:{\sqcup}}_{k-1}]\:\#

It then applies the movements of δ\delta tape by tape and head by head. We call these tapes and heads "virtual tapes" and "virtual heads," respectively.

To do this, it begins by scanning the entire tape from the left delimiter #\#, and stores the values of the virtual heads in states of Q~\widetilde{Q}. The set Q~\widetilde{Q} is enlarged to perform this "memorization" action. Then we return to the beginning of the tape and carry out each of the actions of MM for each virtual tape. If at any point, when shifting each virtual head, it is placed on a delimiter #\#, we replace the tape position with the blank symbol of M~\widetilde{M}, [⊔k][\sqcup^k], and shift all symbols of the virtual tape left or right as necessary.

Once this is done, we change the initial state to the corresponding state according to δ\delta, and so on. The set FF is the same, since we pass through the same states of QQ in Q~\widetilde{Q}.

It is easy to see that L(M)=L(M~)L(M) = L(\widetilde{M}), since we have created a machine that perfectly imitates the computation process of MM, and if it halts, it halts in a state of QQ. The accepted words are those in which, once the machine has halted, the state is in FF.

∎

The details of that construction — the work alphabet, the auxiliary states, and every clause of δ~\widetilde{\delta} — are as follows.

Proposition2.14

Every multi-tape Turing machine can be simulated by a single-tape Turing machine.

Proof

Let M=(Q,A,B,δ,q0,⊔,F)M=(Q, A, B, \delta, q_0, \sqcup, F) be a Turing machine with kk tapes. We will construct the machine M~=(Q~,A,B~,δ~,q0,[⊔k],F)\widetilde{M}=(\widetilde{Q}, A, \widetilde{B}, \widetilde{\delta}, q_0, [\sqcup^k], F). To this end, we first define Bˇ\widecheck{B} as:

Bˇ={bˇ:b∈B}\widecheck{B} = \{\widecheck{b} : b \in B\}

To define B~\widetilde{B}, note that a symbol in an alphabet need not be a single character. To represent a single compound symbol, we surround it with brackets ([ ][\:]). Therefore, the following alphabet makes sense:

B~={[u1 u2 ... uk]:ui∈B∪Bˇ}∪{#}\widetilde{B} = \{[u_1\:u_2\:...\:u_k]:u_i\in B\cup\widecheck{B}\}\cup\{\#\}

In B~\widetilde{B} we also include a delimiter symbol #\#.

We make the state set Q~\widetilde{Q} larger than QQ. To do so, we enumerate all states of QQ:

Q={q0,q1,...,qm}Q=\{q_0,q_1,...,q_m\}

and define the new state set as:

Q~=Q∪(⋃i=0m⋃j=1k{q~i,j, q~i,j(R), q~i,j(D), q~i,j(Ds), q~i,j(I), q~i,j(Is)})\widetilde{Q}=Q\cup\left(\bigcup_{i=0}^m\bigcup_{j=1}^k\left\{\widetilde{q}_{i,j},\:\widetilde{q}_{i,j}^{(R)},\:\widetilde{q}_{i,j}^{(D)},\:\widetilde{q}_{i,j}^{(Ds)},\:\widetilde{q}_{i,j}^{(I)},\:\widetilde{q}_{i,j}^{(Is)}\right\}\right)

We define the initial configuration of the machine on input u=u1 u2 ... un∈A∗u=u_1\:u_2\:...\:u_n\in A^* as16:

q0  :  # [u1ˇ ⊔ˇ ... ⊔ˇ⏟k−1] [u2 ⊔ ... ⊔⏟k−1] ... [un ⊔ ... ⊔⏟k−1] #q_0\;:\;\fbox{\#}\:[\widecheck{u_1}\:\underbrace{\widecheck{\sqcup} \:...\:\widecheck{\sqcup}}_{k-1}]\:[u_2\:\underbrace{{\sqcup}\:...\:{\sqcup}}_{k-1}]\:...\:[u_n\:\underbrace{{\sqcup}\:...\:{\sqcup}}_{k-1}]\:\#

We describe δ~\widetilde{\delta} step by step. First, we want to start from the same state as the multi-tape machine (this will allow the accepted language to be the same, as we will see later). As soon as the machine is in this state, we move right and begin searching for the head position of the first tape. We use state q~i,j\widetilde{q}_{i,j} to search for the jj-th head while the multi-tape machine is in state qiq_i. To do this, we add the following transitions, for each i=0,1,...,mi=0,1,...,m:

δ~(qi,#)=(q~i,1,#,R)\widetilde{\delta}(q_i, \#) = (\widetilde{q}_{i,1}, \#, R)

To search for the jj-th head while the multi-tape machine is in state qiq_i, we keep moving right as long as no symbol from Bˇ\widecheck{B} is found at position jj. We add, for each i=0,1,...,mi=0,1,...,m and j=1,2,...,kj=1,2,...,k:

δ~(q~i,j,[w1 w2 ... wl ... wk])=(q~i,j,[w1 w2 ... wl ... wk],R)     with     wj∈B,  wl∈B∪Bˇ for l≠j\widetilde{\delta}(\widetilde{q}_{i,j}, [w_1\:w_2\:...\:w_l\:...\:w_k]) = (\widetilde{q}_{i,j}, [w_1\:w_2\:...\:w_l\:...\:w_k], R)\;\;\text{ with }\;\;w_j\in B,\; w_l \in B\cup\widecheck{B} \text{ for } l \neq j

Note that we explicitly require wj∉Bˇw_j\notin\widecheck{B} (unless stated otherwise, wl∈B∪Bˇw_l\in B\cup\widecheck{B} for each ll), so we keep moving right until a checked symbol appears at position jj of the current tape cell.

We are now in a position to incorporate the transitions of δ\delta into δ~\widetilde{\delta}. Given a transition of δ\delta:

δ(qi,b1,b2,...,bk)=(qj,c1,M1,c2,M2,...,ck,Mk)\delta(q_i, b_1, b_2, ..., b_k) = (q_j, c_1, M_1, c_2, M_2, ..., c_k, M_k)

for each such transition, we must insert rules into δ~\widetilde{\delta}. These transitions depend on how each head moves: if a head moves right or left and reaches the delimiter #\#, we must extend the tape of M~\widetilde{M} with a blank [⊔k][\sqcup^k] and shift the delimiter #\# one position. For each tape l=1,2,...,kl=1,2,...,k:

  • If Ml=RM_l=R: we insert several transitions. First, the transition that replaces the symbol under head ll, namely blb_l, by the new symbol clc_l:
δ~(q~i,l,[w1 ... wl−1 blˇ wl+1 ... wk])=(q~i,l(D),[w1 ... wl−1 cl wl+1 ... wk],R)\widetilde{\delta}(\widetilde{q}_{i,l}, [w_1\:...\:w_{l-1}\:\widecheck{b_l}\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(D)}, [w_1\:...\:w_{l-1}\:c_l\:w_{l+1}\:...\:w_k], R)

Next, we simulate the ll-th head moving right. If we have not reached the delimiter #\#, the transition is direct: we mark the new head position and return to the first #\# using the return state (R)^{(R)} (described below):

δ~(q~i,l(D),[w1 ... wl−1 wl wl+1 ... wk])=(q~i,l(R),[w1 ... wl−1 wlˇ wl+1 ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,l}^{(D)}, [w_1\:...\:w_{l-1}\:w_l\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(R)}, [w_1\:...\:w_{l-1}\:\widecheck{w_l}\:w_{l+1}\:...\:w_k], L)

If we reach the delimiter #\#, we initiate a subroutine indicated by states (Ds)^{(Ds)}, in which we insert a blank symbol and shift the delimiter to the right, then proceed to the return state (R)^{(R)}:

δ~(q~i,l(D),#)=(q~i,l(Ds),[⊔ ... ⊔⏟l−1 ⊔ˇ ⊔ ... ⊔⏟k−l],R)\widetilde{\delta}(\widetilde{q}_{i,l}^{(D)}, \#) = (\widetilde{q}_{i,l}^{(Ds)}, [\underbrace{{\sqcup}\:...\:{\sqcup}}_{l-1}\:\widecheck{\sqcup}\:\underbrace{{\sqcup}\:...\:{\sqcup}}_{k-l}], R)
δ~(q~i,l(Ds),[⊔k])=(q~i,l(R),#,L)\widetilde{\delta}(\widetilde{q}_{i,l}^{(Ds)}, [\sqcup^k]) = (\widetilde{q}_{i,l}^{(R)}, \#, L)
  • If Ml=LM_l=L: we add analogous transitions using substates (I)^{(I)} and (Is)^{(Is)}:
δ~(q~i,l,[w1 ... wl−1 blˇ wl+1 ... wk])=(q~i,l(I),[w1 ... wl−1 cl wl+1 ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,l}, [w_1\:...\:w_{l-1}\:\widecheck{b_l}\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(I)}, [w_1\:...\:w_{l-1}\:c_l\:w_{l+1}\:...\:w_k], L)
δ~(q~i,l(I),[w1 ... wl−1 wl wl+1 ... wk])=(q~i,l(R),[w1 ... wl−1 wlˇ wl+1 ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,l}^{(I)}, [w_1\:...\:w_{l-1}\:w_l\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(R)}, [w_1\:...\:w_{l-1}\:\widecheck{w_l}\:w_{l+1}\:...\:w_k], L)
δ~(q~i,l(I),#)=(q~i,l(Is),[⊔ ... ⊔⏟l−1 ⊔ˇ ⊔ ... ⊔⏟k−l],L)\widetilde{\delta}(\widetilde{q}_{i,l}^{(I)}, \#) = (\widetilde{q}_{i,l}^{(Is)}, [\underbrace{{\sqcup}\:...\:{\sqcup}}_{l-1}\:\widecheck{\sqcup}\:\underbrace{{\sqcup}\:...\:{\sqcup}}_{k-l}], L)
δ~(q~i,l(Is),[⊔k])=(q~i,l(R),#,R)\widetilde{\delta}(\widetilde{q}_{i,l}^{(Is)}, [\sqcup^k]) = (\widetilde{q}_{i,l}^{(R)}, \#, R)
  • If Ml=SM_l=S: the transition is more immediate, since we do not need to move the head — we simply replace the symbol and switch to (R)^{(R)}:
δ~(q~i,l,[w1 ... wl−1 blˇ wl+1 ... wk])=(q~i,l(R),[w1 ... wl−1 clˇ wl+1 ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,l}, [w_1\:...\:w_{l-1}\:\widecheck{b_l}\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(R)}, [w_1\:...\:w_{l-1}\:\widecheck{c_l}\:w_{l+1}\:...\:w_k], L)

It remains to describe how we switch from one tape to the next. We begin by adding a return state that repositions the head at the first delimiter #\#. For each i=0,1,...,mi=0,1,...,m and j=1,2,...,kj=1,2,...,k we add:

δ~(q~i,j(R),[w1 w2 ... wl ... wk])=(q~i,j(R),[w1 w2 ... wl ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,j}^{(R)}, [w_1\:w_2\:...\:w_l\:...\:w_k]) = (\widetilde{q}_{i,j}^{(R)}, [w_1\:w_2\:...\:w_l\:...\:w_k], L)

To advance to the next tape, we simply add these transitions for each i=0,1,...,mi=0,1,...,m and j=1,2,...,k−1j=1,2,...,k-1:

δ~(q~i,j(R),#)=(q~i,j+1(R),#,R)\widetilde{\delta}(\widetilde{q}_{i,j}^{(R)}, \#) = (\widetilde{q}_{i,j+1}^{(R)}, \#, R)

Once we have processed the last tape, we update the state to one of those in QQ according to the transition function. Revisiting δ\delta once more, for each transition δ(qi,b1,b2,...,bk)=(qj,c1,M1,c2,M2,...,ck,Mk)\delta(q_i, b_1, b_2,...,b_k) = (q_j, c_1, M_1, c_2, M_2,...,c_k, M_k), we add the state-update transition:

δ~(q~i,k(R),#)=(qj,#,S)\widetilde{\delta}(\widetilde{q}_{i,k}^{(R)}, \#) = (q_j, \#, S)

We have now defined all the necessary transitions. We verify that the simulation is correct, i.e., that L(M)=L(M~)L(M)=L(\widetilde{M}). Indeed, we have constructed a machine M~\widetilde{M} that carries out the same computation as MM, passing through the same states QQ of MM after modifying the tape. In particular, whenever MM halts, M~\widetilde{M} will be in a state q∈Qq\in Q (by construction). Therefore, since both machines share the same set of accepting states FF, their accepted languages coincide.

∎

Let us pause for a moment on the consequences of these two models of computation being equivalent. First, it gives us an idea of the scope of the single-tape Turing machine: by introducing more complexity in its description, we cannot aspire to create more complex algorithms, since we can always find an equivalent algorithm on a single-tape machine. This, as we mentioned at the beginning of the chapter, gives us a hint of one of the most relevant theses in computer science today, which we discuss in Section 2.4.

Second, this gives us an advantage when we want to solve problems: being able to describe a program in a model that gives us greater flexibility is much simpler.

As we advance in this section, it is important to keep these two observations in mind. Now we introduce a new computational model: the random-access machine4.

Definition2.15—Random-access Turing machine

A random-access Turing machine with kk tapes is a kk-tape multi-tape Turing machine with two additional tapes, bounded on the left:

  • An address tape, which stores a natural number n∈Nn \in \mathbb{N}.17
  • A tape called RAM, which stores symbols on each of its tracks.

We denote RAM[n]\text{RAM}[n] as the nn-th track of the RAM.

The function δ:Q×N×B×Bk⟶(Q×N×B×(B×{L,R,S})k)∪{∅}\delta : Q \times \mathbb{N} \times B \times B^k \longrightarrow \left(Q \times \mathbb{N} \times B \times (B \times \{L, R, S\})^k\right) \cup \{\emptyset\} will be of the form:

δ(q,b1,b2,...,bk)=∅\delta(q, b_1, b_2, ..., b_k) = \emptyset

for undefined transitions, and for defined ones:

δ(q,n,r,b1,b2,...,bk)=(p,m,s,c1,M1,c2,M2,...,ck,Mk),\delta(q, n, r, b_1, b_2, ..., b_k) = (p, m, s, c_1, M_1, c_2, M_2, ..., c_k, M_k),

with q,p∈Qq,p\in Q, n,m∈Nn, m \in \mathbb{N}, r,s,bi,ci∈Br, s, b_i, c_i \in B, Mi∈{L,R,S}M_i \in \{L, R, S\}, for all i∈{1,2,...,k}i \in \{1, 2, ..., k\}.

The defined transition is executed if the value of RAM[n]=r\text{RAM}[n] = r, and after executing it the value of RAM[n]\text{RAM}[n] changes to ss, and the value of the address tape changes from nn to mm. That is, in a single transition, it can access the RAM position, modify it, and change the RAM position.

A configuration of the machine is specified by:

q  :  r ; w1 , w2 , ... , wkq\;:\;r\:;\:w_1\:,\:w_2\:,\:...\:,\:w_k

with one symbol from each tape w1,w2,...,wkw_1, w_2, ..., w_k suitably boxed, and one symbol of rr boxed, specifically the one pointed to by the address tape.

We can see an example of such a configuration in Figure 2.9.

q3  :  1 a b 2 ; 2 X X , 1 0 , ... , wkq_3\;:\;1\:a\:b\:\boxed{2}\:;\:2\:\boxed{X}\:X\:,\:\boxed{1}\:0\:,\:...\:,\:w_k
4tttt...addresstapeXX1bt.........tapek...currentstateq31ab2c...RAMtt1Y0......tape2t2cXt......tape1
Figure 2.9. Random-access Turing machine.

Proving that a random-access Turing machine can be simulated by a multi-tape Turing machine is straightforward but tedious. Proposition 2.16 explains the routines that would be necessary for such a simulation.

Proposition2.16

Every random-access Turing machine with kk tapes can be simulated by a multi-tape Turing machine.

Proof

To prove this, we need a (k+2)(k+2)-tape Turing machine. The last two tapes will be the address tape (k+1k+1-st tape) and the RAM (k+2k+2-nd tape). We see that:

  1. We can create a routine that replaces the symbol at the head position on tape k+2k+2 with another.
  2. We can create a routine that modifies the address on tape k+1k+1.
  3. We can create a routine that, given an address on tape k+1k+1, moves the head of tape k+2k+2 to the corresponding position. We could implement a routine that counts up to the number on tape k+1k+1 and, each time it counts, moves the head of tape k+2k+2 one position to the right, starting at the beginning.

Implementing the additional atomic operation of the random-access Turing machine on this multi-tape machine is straightforward: we can access the value of RAM[n]\text{RAM}[n], rr, by seeing where the head is on tape k+2k+2. Based on this, we execute the necessary transition. After this, we modify RAM[n]\text{RAM}[n] to ss by executing routine (1), and change the address on tape k+1k+1 by executing (2). Finally, we modify the head position on tape k+2k+2 by executing routine (3).

∎

Next, we continue the chain of simulations. To do so, we introduce a definition of computer. This is a simplification of a computer's architecture, but in practical terms all modern computers follow the structure specified in Definition 2.1718.

Definition2.17—Modern computer

We define a modern computer as one equipped with the following elements:

  • Registers. A register stores exactly one unit of information, in binary. Registers have a fixed size (on most current computers it is 64 bits). This unit of information can represent characters, instructions, etc.
  • RAM. RAM or random-access memory is the computer hardware that stores information in use by the computer.
  • ROM. ROM or read-only memory is computer hardware that stores information but cannot be modified. Normally, ROM stores fixed programs that tell the computer basic actions such as loading programs at startup.
  • Instruction set. Each CPU contains a fixed set of actions it can execute. Each of these actions is called an instruction, and the set of all possible instructions is the instruction repertoire. These instructions are usually arithmetic and conditional operations on registers, loading from RAM to registers, etc.

For our work, knowing details about what types of instructions a computer can execute is irrelevant. What matters is knowing that, regardless of the programming language in which it is written, a program can be "translated"19 to this set of instructions.

  • Disk. A computer has one or more disks (for simplicity, we speak of only one). The disk allows reading and writing, and stores program inputs and outputs, as well as programs to be executed.

Simulating this computer by means of a random-access Turing machine is immediate: we can simulate all components by means of tapes and RAM.

Proposition2.18

A modern computer can be simulated by a random-access Turing machine.

Proof

Let us see how we can simulate each element of computer CC by means of a random-access Turing machine MM. Figure 2.10 symbolizes the following statements.

  • The ROM of CC can be simulated by including the necessary states and transitions in MM.
  • Each instruction of CC can be implemented in the same way, by means of transitions and states.
  • The registers of CC used to store memory addresses can be simulated by the address tape of MM.
  • Other general-purpose registers of CC can be simulated by incorporating additional tapes into MM.
  • The RAM of CC can be simulated with the RAM of MM.
  • The disk of CC can be simulated with a tape in MM (which we can call the input/output tape).
∎

The above proof is not strict, and we do this on purpose: defining concretely how to implement each computer component is outside the scope of this work, and would be more relevant if we were discussing computer architecture. We illustrate this simulation through an example.

Example2.19

Let us simulate an instruction of a modern computer on a random-access Turing machine. Let ADD be the addition instruction:

ADD R0, R1, R2\texttt{ADD R0, R1, R2}

This instruction takes the binary numbers in registers R0 and R1 and stores their sum in register R2. For example, if we execute

ADD r4, r5, r9\texttt{ADD r4, r5, r9}

we store the sum of registers r4 and r5 in register r9.

Before performing the simulation, observe the following: if the size of each register is nn bits, we can make the RAM consist of mm registers of nn bits and have the address tape make the RAM head point to the first bit of the corresponding register. With that said, to perform the simulation it suffices to follow this process:

  1. Place on the address tape the address corresponding to register R0. This way, the RAM head will point to the first bit of register R0.
  2. Copy the contents of the RAM (the nn bits starting at the RAM head position) onto a new tape, which we call C0C_0.
  3. Repeat the previous steps to copy register R1 onto tape C1C_1.
  4. Repeat the following steps:
  5. Check whether the number stored on tape C1C_1 is 00. If so, proceed to step 5.
  6. If not, decrement by 11 the number stored on tape C1C_1 and increment by 11 the number stored on tape C0C_0. Adding and subtracting 11 from a binary number on a tape is an operation easily implementable on a Turing machine.
  7. At this point, tape C1C_1 stores 00 and tape C0C_0 stores the result of the sum.
  8. Now place on the address tape the address corresponding to register R2. This way, the RAM head points to register R2.
  9. Copy the contents of tape C0C_0 to the RAM (the nn bits starting at the RAM head position, which will point to register R2).

Note that the simulation we have performed does not modify registers R0 and R1; it modifies only R2.

The result we seek is around the corner. Proposition 2.20, Proposition 2.21 close the circle and allow us to prove Theorem 2.23144.

Proposition2.20

A Python program can be simulated by a modern computer.

Proof

The proof is immediate: once we can simulate a single-core modern computer, we can simulate any software, including Python programs.

Another way to see this is to imagine configuring a computer to run a Python program right at startup. This way, we can think of the computer as a piece of hardware that simulates the Python program.

∎
ROM(containstheinstructions)CPU(centralprocessingunit)circuitrytoexecuteinstructionsaddressregistersotherregistersRAM(mainmemory)diskmoderncomputerrandom-accessTuringmachineinput/outputtapeRAMaddresstape...othertapes......
Figure 2.10. Simulation of a modern computer through a random-access Turing machine.
Proposition2.21

A single-tape Turing machine can be simulated by a Python program.

Proof

To prove this, we must create a Python program that, given the description of a single-tape Turing machine MM, is capable of applying all computation steps and returning the configuration in which the machine halts.

Program2.2—simulate_turing.py
import utilities
from turing import Turing
 
def simulate_turing(input):
machine_encoding, machine_input = utilities.UAM(input)
 
# create the Turing machine from the SISO function input
turing_machine = Turing(machine_encoding, machine_input)
 
# execute the machine
turing_machine.run()
 
# if we reach here, the machine has halted: return its configuration
return str(turing_machine)

We can see the simulation in Program 2.2. The accompanying programs live in simulate_turing.py and turing.py. We explain each line.

First, the Turing class is imported from the turing package. You can find the code in turing.py. This class is responsible for instantiating a Turing machine and incorporates the functions needed to perform the simulation.

Next we have our main function, which accepts a string as input. To encode several inputs, we use the MAU and UAM functions from the utilities library. Given a list of strings, the MAU function (Multiple to One) converts them into a single string using :: as separator. The UAM function performs the reverse process, converting a string into a list of strings by splitting on ::.

Thus, the input string will have the form

ENCODING+’::’+INPUT\texttt{ENCODING} + \texttt{'::'} + \texttt{INPUT}

That is, it consists of the strings corresponding to the Turing machine encoding and the input, joined by ::.

Encoding a Turing machine MM as a string is straightforward. We do it by means of files, which are nothing more than character strings. We encode each state and symbol with strings, and create a character string by joining the following strings with newline characters, in this order:

  • The states, separated by spaces.
  • The symbols of the input alphabet, separated by spaces.
  • The symbols of the work alphabet, separated by spaces.
  • The blank symbol.
  • The initial state.
  • The final states, separated by spaces.
  • One line for each transition
δ(CS,RS)=(NS,WS,M)\delta(\texttt{CS}, \texttt{RS}) = (\texttt{NS}, \texttt{WS}, \texttt{M})

(where CS is the current state, RS is the read symbol, NS is the next state, WS is the write symbol, and M is the movement, which can be L, R, or S), with the following format:

CS, RS : NS, WS, M\texttt{CS, RS : NS, WS, M}

On the line with UAM, we split the input and store the machine encoding in machine_encoding and the input in machine_input, using the UAM function explained above.

On the line that creates the Turing instance, we create an instance of the Turing class with the machine encoding and input. When creating the instance, functions run that check whether the description is correct. If there is an error, the machine will not execute any step and the output will be of error type.

On the line that calls run, we run the machine, applying transition after transition until it halts. If it does not halt, the Python program will not halt either.

On the return line, we return the configuration of the machine upon halting, and whether it accepts or rejects.

In this way we have simulated MM by means of a Python program.

∎

The program simulate_turing is implemented in simulate_turing.py. We show an example of the encoding explained above and how to use it in Example 2.22.

Example2.22

An encoding of the machine M#a>#bM_{\#a>\#b} from Example 2.11 can be:20

q_0 q_a q_b q_R q_F     # states
a b                     # input alphabet
a b X _                 # work alphabet
_                       # blank symbol
q_0                     # initial state
q_F                     # final states
 
# transitions
q_0, a : q_a, X, R
q_0, b : q_b, X, R
q_0, X : q_0, X, R
q_a, a : q_a, a, R
q_a, b : q_R, X, L
q_a, X : q_a, X, R
q_a, _ : q_F, _, S
q_b, a : q_R, X, L
q_b, b : q_b, b, R
q_b, X : q_b, X, R
q_R, a : q_R, a, L
q_R, b : q_R, b, L
q_R, X : q_R, X, L
q_R, _ : q_0, _, R

This encoding is in turing_machines/more_as_than_b.mt. To test simulate_turing.py, clone intro-to-incompleteness, open a Python interpreter in its root folder, and execute:

from utilities import read, MAU
from simulate_turing import simulate_turing
encoding = read('./turing_machines/more_as_than_b.mt')
input_str = 'aababb'

In this case we used aababbaababb as input, but it can be replaced by any input from {a,b}∗\{a,b\}^*. To execute simulate_turing, we simply do:

simulate_turing(MAU(encoding, input_str))
'q_0 : X X X X X X [_] (rejects)'

We see that the Python program returns the string 'q_0 : X X X X X X [_] (rejects)'. That is, the Turing machine M#a>#bM_{\#a>\#b} halts in the configuration

q0  :  X X X X X X ⊔q_0\;:\;X\:X\:X\:X\:X\:X\:\boxed{\sqcup}

which is not a final state (q0∉Fq_0 \notin F), and therefore the input aababbaababb is rejected.

It is possible to test with any input and with other Turing machines from turing_machines/. Some of the files correspond to incorrect encodings, to see how simulate_turing.py is a Python program as we defined it (SISO, without errors, etc.).

Once we have proved the sequence of simulations in Figure 2.6, the following theorem is established.

Theorem2.23—Equivalence between Python programs and Turing machines

Given a Python program PP, there exists a (single-tape) Turing machine MM that computes the same function as PP.

We have just shown that Turing machines and Python programs are equal in terms of the programs we can write in them. This will be useful when we define the concept of problem in Chapter 3.

2.4 #Relationship to the Church–Turing Thesis

The work we have done in this chapter has been arduous, but the result we have obtained is extremely interesting. However, we must remember that the choice of the Python programming language was completely arbitrary: we could replace Python with any other programming language capable of encoding a Turing machine.

What does this tell us about what can be computed? We can see it in two ways: on the one hand, Python programs (as well as computers themselves) are nothing more than Turing machines with "many shortcuts" and extensions. On the other (and more interestingly), the Turing machine, which may initially seem innocuous because of the simplicity of its description, has enormous computational potential, since it can execute any computer program.

In Chapter 1 we already presented the Church–Turing thesis. For convenience, we reference it again below.

Church–Turing Thesis. Every computable function (algorithm) can be computed by a Turing machine.

As we have said, this is a thesis—that is, a statement that is assumed but not proved. Proving this result is practically impossible, but there is evidence supporting it, such as what we have just proved: we have seen how a computational model that is apparently much more complex than the Turing machine turns out to be equivalent to it.

However, there is always the possibility that advances in our understanding of how the universe behaves will open the door to new and more powerful models of computation. This is not the case for quantum computation, which can speed up the resolution of some problems but does not extend the range of computable functions21.

This thesis has profound implications. We conclude this chapter with some questions.

  1. Is the human mind a Turing machine?
  2. Is the universe a Turing machine? That is, are the laws governing the universe (the laws of physics) computable?
  3. If this is not the case, is it possible to create a machine more powerful than a Turing machine?

Regarding the first question, it is important to make a historical note: Turing claimed that the answer was affirmative—that is, that the human mind could be simulated on a computer. This thesis was central to his work on Artificial Intelligence22, in which he argued that the development of this discipline was only a matter of time.

Footnotes

  1. Aiken, H. (1956). Elektronische Rechenmaschinen und Informationsverarbeitung. Nachrichtentechnische Fachberichte, 2:32–34. (English translation in MacCormick4.)

    ↩
  2. Turing, A.M. (1937). On computable numbers, with an application to the Entscheidungsproblem. Proceedings of the London Mathematical Society, 42(1):230–265.

    ↩ ↩2 ↩3
  3. Martelli, A. (2006). Python in a Nutshell. O'Reilly Media.

    ↩
  4. MacCormick, J. (2018). What Can Be Computed?: A Practical Guide to the Theory of Computation. Princeton University Press.

    ↩ ↩2 ↩3 ↩4
  5. We refer to exceptions that can be defined implicitly in the language or that can result from errors in its use, since in the definition of PP the program is syntactically correct.

    ↩
  6. The pickle library can encode and decode a great variety of data types, including all native ones. It has some limitations for more complex objects, but since all objects can be reduced to these data types, we can state that any object can be encoded. See Lutz23.

    ↩
  7. Church, A. (1937). Review of Turing 1936–37. Journal of Symbolic Logic, 2(1):42–43.

    ↩
  8. De Mol, L. (2021). Turing machines. In E.N. Zalta (ed.), Stanford Encyclopedia of Philosophy. https://plato.stanford.edu/entries/turing-machine/

    ↩
  9. This notation comes from the Kleene closure operation.

    ↩
  10. Note that this definition allows the machine to keep changing its configuration even after reaching a final state. Normally, Turing machines halt upon reaching a final state, and we can ensure this simply by making δ(q,b)=∅\delta(q,b)=\emptyset for all q∈Fq \in F, b∈Bb \in B.

    ↩
  11. We use the notation: #(u,a)\#(u, a) equals the number of occurrences of symbol aa in uu.

    ↩
  12. We will not introduce the concept of "problem" precisely until Chapter 3—for now, we keep the intuitive notion of a problem, without further explanation.

    ↩
  13. Evans, B., & Flanagan, D. (2018). Java in a Nutshell. O'Reilly Media.

    ↩
  14. Sipser, M. (2012). Introduction to the Theory of Computation (3rd ed.). Cengage Learning.

    ↩ ↩2
  15. Based on the proof of Theorem 3.13 of Sipser14.

    ↩
  16. It is always possible to change the initial head position of a Turing machine by executing the necessary transitions.

    ↩
  17. We assume 0∉N0 \notin \mathbb{N}.

    ↩
  18. Nisan, N., & Schocken, S. (2005). The Elements of Computing Systems: Building a Modern Computer from First Principles. MIT Press.

    ↩
  19. Through compilation and/or interpretation processes.

    ↩
  20. We include comments and blank lines, which we ignore.

    ↩
  21. Kaye, P., Laflamme, R., & Mosca, M. (2007). An Introduction to Quantum Computing. Oxford University Press.

    ↩
  22. Turing, A.M. (1950). Computing machinery and intelligence. Mind, 59(236):433–460.

    ↩
  23. Lutz, M. (2013). Learning Python. O'Reilly Media.

    ↩