Estou pensando agora em como me convencer de que as máquinas de Turing são um modelo geral de computação. Concordo que o tratamento padrão da tese de Church-Turing em alguns livros-texto padrão, por exemplo, Sipser, não é muito completo. Aqui está um esboço de como eu poderia passar das máquinas de Turing para uma linguagem de programação mais reconhecível.
Considere uma linguagem de programação estruturada em bloco com if
e while
instruções, com sub-rotinas e funções definidas não recursivas , com variáveis aleatórias booleanas nomeadas e expressões booleanas gerais e com uma única matriz booleana ilimitada tape[n]
com um ponteiro de matriz inteira n
que possa ser incrementado ou decrementado n++
ou n--
. O ponteiro n
é inicialmente zero e a matriz tape
é inicialmente zero. Portanto, essa linguagem de computador pode ser do tipo C ou Python, mas é muito limitada em seus tipos de dados. Na verdade, eles são tão limitados que nem sequer temos uma maneira de usar o ponteiro n
em uma expressão booleana. Assumindo quetape
é infinito apenas para a direita, podemos declarar um "underflow" de ponteiro como "erro do sistema", se n
algum dia for negativo. Além disso, nossa linguagem possui uma exit
declaração com um argumento para gerar uma resposta booleana.
O primeiro ponto é que essa linguagem de programação é uma boa linguagem de especificação para uma máquina de Turing. Você pode ver facilmente que, exceto para a matriz de fitas, o código possui apenas muitos estados possíveis: O estado de todas as suas variáveis declaradas, a linha de execução atual e a pilha de sub-rotinas. Este último possui apenas uma quantidade finita de estado porque funções recursivas não são permitidas. Você pode imaginar um "compilador" que cria uma máquina de Turing "real" a partir de um código desse tipo, mas os detalhes não são importantes. O ponto é que temos uma linguagem de programação com uma sintaxe muito boa, mas com tipos de dados muito primitivos.
O restante da construção é convertê-lo em uma linguagem de programação mais habitável, com uma lista finita de funções da biblioteca e estágios de pré-compilação. Podemos proceder da seguinte forma:
Com um pré-compilador, podemos expandir o tipo de dados booleanos para um alfabeto de símbolos maior, porém finito, como ASCII. Podemos assumir que tape
assume valores neste alfabeto maior. Podemos deixar um marcador no início da fita para impedir o fluxo insuficiente do ponteiro e um marcador móvel no final da fita para impedir que a TM patine acidentalmente no infinito da fita. Podemos implementar operações binárias arbitrárias entre símbolos e conversões em instruções if
e while
instruções booleanas . (Na verdade, também if
pode ser implementado while
, se não estiver disponível.)
kkiik
Designamos uma fita como "memória" com valor de símbolo e as outras como "registradores" ou "variáveis" não assinadas e com valor inteiro. Armazenamos os números inteiros no binário little-endian com marcadores de terminação. Primeiro, implementamos a cópia de um registro e o decremento binário de um registro. Combinando isso com incremento e decremento do ponteiro de memória, podemos implementar a busca de acesso aleatório da memória de símbolo. Também podemos escrever funções para calcular a adição binária e a multiplicação de números inteiros. Não é difícil escrever uma função de adição binária com operações bit a bit e uma função para multiplicar por 2 com o deslocamento à esquerda. (Ou mudança para a direita, uma vez que é pouco endian.) Com essas primitivas, podemos escrever uma função para multiplicar dois registradores usando o algoritmo de multiplicação longa.
Podemos reorganizar a fita de memória de uma matriz de símbolos unidimensional symbol[n]
para uma matriz de símbolos bidimensional symbol[x,y]
usando a fórmula n = (x+y)*(x+y) + y
. Agora podemos usar cada linha da memória para expressar um número inteiro não assinado em binário com um símbolo de terminação, para obter uma memória unidimensional, de acesso aleatório e com valor inteiro memory[x]
. Podemos implementar a leitura da memória em um registro inteiro e a gravação de um registro na memória. Muitos recursos agora podem ser implementados com funções: aritmética de ponto flutuante e assinado, sequências de símbolos, etc.
Apenas mais uma instalação básica exige estritamente um pré-compilador, ou seja, funções recursivas. Isso pode ser feito com uma técnica amplamente usada para implementar linguagens interpretadas. Atribuímos a cada função recursiva de alto nível uma string de nome e organizamos o código de baixo nível em um while
loop grande que mantém uma pilha de chamadas com os parâmetros usuais: o ponto de chamada, a função chamada e uma lista de argumentos.
Neste ponto, a construção possui recursos suficientes de uma linguagem de programação de alto nível que funcionalidade adicional é mais o tópico de linguagens de programação e compiladores do que a teoria de CS. Também já é fácil escrever um simulador de máquina de Turing nessa linguagem desenvolvida. Não é exatamente fácil, mas certamente padrão, escrever um auto-compilador para o idioma. Obviamente, você precisa de um compilador externo para criar a TM externa a partir de um código nessa linguagem C ou Python, mas isso pode ser feito em qualquer linguagem de computador.
Observe que essa implementação esboçada não apenas suporta a tese de Church-Turing dos lógicos para a classe de função recursiva, mas também a tese de Church-Turing estendida (isto é, polinomial), conforme se aplica à computação determinística. Em outras palavras, possui sobrecarga polinomial. De fato, se recebermos uma máquina de RAM ou (meu favorito) uma TM de fita em árvore, isso pode ser reduzido a sobrecarga polilogarítmica para computação serial com memória RAM.