Ahora mismo estoy pensando en cómo convencerme de que las máquinas de Turing son un modelo general de computación. Estoy de acuerdo en que el tratamiento estándar de la tesis de Church-Turing en algunos libros de texto estándar, por ejemplo, Sipser, no es muy completo. Aquí hay un bosquejo de cómo podría pasar de las máquinas de Turing a un lenguaje de programación más reconocible.
Considere un lenguaje de programación estructurado en bloque con if
y while
declaraciones, con funciones definidas no recursivas y subrutinas, con variables aleatorias booleanas nombradas y expresiones booleanas generales, y con una única matriz booleana sin límites tape[n]
con un puntero de matriz entera n
que puede incrementarse o disminuirse, n++
o n--
. El puntero n
es inicialmente cero y la matriz tape
es inicialmente cero. Por lo tanto, este lenguaje de computadora puede ser similar a C o Python, pero es muy limitado en sus tipos de datos. De hecho, son tan limitados que ni siquiera tenemos una forma de usar el puntero n
en una expresión booleana. Asumiendo quetape
es solo infinito a la derecha, podemos declarar un "error del sistema" de flujo inferior del puntero si n
alguna vez es negativo. Además, nuestro lenguaje tiene una exit
declaración con un argumento, para generar una respuesta booleana.
Entonces, el primer punto es que este lenguaje de programación es un buen lenguaje de especificación para una máquina Turing. Puede ver fácilmente que, a excepción de la matriz de cintas, el código solo tiene muchos estados posibles: el estado de todas sus variables declaradas, la línea de ejecución actual y su pila de subrutinas. Este último solo tiene una cantidad finita de estado porque las funciones recursivas no están permitidas. Se podría imaginar un "compilador" que crea una máquina de Turing "real" a partir de un código de este tipo, pero los detalles no son importantes. El punto es que tenemos un lenguaje de programación con una sintaxis bastante buena, pero tipos de datos muy primitivos.
El resto de la construcción es convertir esto a un lenguaje de programación más habitable con una lista finita de funciones de biblioteca y etapas de precompilación. Podemos proceder de la siguiente manera:
Con un precompilador, podemos expandir el tipo de datos booleanos a un alfabeto de símbolos más grande pero finito como ASCII. Podemos suponer que tape
toma valores en este alfabeto más grande. Podemos dejar un marcador al comienzo de la cinta para evitar el desbordamiento del puntero, y un marcador móvil al final de la cinta para evitar que el TM patine hasta el infinito en la cinta accidentalmente. Podemos implementar operaciones binarias arbitrarias entre símbolos y conversiones a declaraciones booleanas if
y while
. (En realidad, también if
se puede implementar while
si no estuviera disponible).
kkiik
Designamos una cinta como "memoria" con valor de símbolo y las otras como "registros" o "variables" sin signo y con valor entero. Almacenamos los enteros en binario little-endian con marcadores de terminación. Primero implementamos copia de un registro y decremento binario de un registro. Combinando eso con el incremento y la disminución del puntero de memoria, podemos implementar la búsqueda de acceso aleatorio de la memoria de símbolos. También podemos escribir funciones para calcular la suma binaria y la multiplicación de enteros. No es difícil escribir una función de suma binaria con operaciones bit a bit, y una función para multiplicar por 2 con desplazamiento a la izquierda. (O realmente desplazamiento a la derecha, ya que es little-endian.) Con estas primitivas, podemos escribir una función para multiplicar dos registros usando el algoritmo de multiplicación larga.
Podemos reorganizar la cinta de memoria de una matriz de símbolos unidimensionales symbol[n]
a una matriz de símbolos bidimensionales symbol[x,y]
usando la fórmula n = (x+y)*(x+y) + y
. Ahora podemos usar cada fila de la memoria para expresar un entero sin signo en binario con un símbolo de terminación, para obtener una memoria unidimensional, de acceso aleatorio y con valor entero memory[x]
. Podemos implementar la lectura desde la memoria a un registro entero, y la escritura desde un registro a la memoria. Ahora se pueden implementar muchas funciones con funciones: aritmética de punto flotante y firmado, cadenas de símbolos, etc.
Solo una instalación básica más requiere estrictamente un precompilador, es decir, funciones recursivas. Esto se puede hacer con una técnica que se usa ampliamente para implementar lenguajes interpretados. Asignamos a cada función recursiva de alto nivel una cadena de nombre, y organizamos el código de bajo nivel en un while
bucle grande que mantiene una pila de llamadas con los parámetros habituales: el punto de llamada, la función llamada y una lista de argumentos.
En este punto, la construcción tiene suficientes características de un lenguaje de programación de alto nivel que una mayor funcionalidad es más el tema de los lenguajes de programación y compiladores que la teoría de CS. También es fácil escribir un simulador de máquina de Turing en este lenguaje desarrollado. No es exactamente fácil, pero ciertamente estándar, escribir un autocompilador para el idioma. Por supuesto, necesita un compilador externo para crear la TM externa a partir de un código en este lenguaje similar a C o Python, pero eso se puede hacer en cualquier lenguaje de computadora.
Tenga en cuenta que esta implementación esbozada no solo admite la tesis de Church-Turing de los lógicos para la clase de función recursiva, sino también la tesis de Church-Turing extendida (es decir, polinomial), ya que se aplica al cálculo determinista. En otras palabras, tiene una sobrecarga polinómica. De hecho, si nos dan una máquina RAM o (mi favorito personal) una cinta de árbol TM, esto puede reducirse a una sobrecarga poligarítmica para el cálculo en serie con memoria RAM.