Eu estava tentando explicar a alguém que C é Turing completo e percebi que na verdade não sei se é tecnicamente Turing completo. (C como na semântica abstrata, não como em uma implementação real.)
A resposta "óbvia" (grosso modo: ela pode endereçar uma quantidade arbitrária de memória, para emular uma máquina de RAM e, portanto, é completa em Turing) não é realmente correta, tanto quanto posso dizer, como se o padrão C permitisse para size_t ser arbitrariamente grande, ele deve ser fixado em algum comprimento e, independentemente do tamanho que for fixado, ele ainda é finito. (Em outras palavras, embora você possa, considerando uma máquina de Turing de parada arbitrária, escolher um tamanho_t de modo que ele funcione "corretamente", não há como escolher um tamanho_t de modo que todas as máquinas de Turing que parem funcionem corretamente)
Então: o C99 Turing está completo?
Respostas:
Não tenho certeza, mas acho que a resposta é não, por razões bastante sutis. Eu perguntei em Ciência da Computação Teórica há alguns anos e não obter uma resposta que vai além do que eu vou apresentar aqui.
Na maioria das linguagens de programação, você pode simular uma máquina de Turing:
Uma implementação concreta em execução em um computador ficaria sem memória se a fita ficasse muito longa, mas uma implementação ideal poderia executar o programa da máquina de Turing fielmente. Isso pode ser feito com caneta e papel ou comprando um computador com mais memória e um compilador visando uma arquitetura com mais bits por palavra e assim por diante, se o programa ficar sem memória.
Isso não funciona em C porque é impossível ter uma lista vinculada que pode crescer para sempre: sempre há algum limite no número de nós.
Para explicar o porquê, primeiro preciso explicar o que é uma implementação em C. C é realmente uma família de linguagens de programação. O padrão ISO C (mais precisamente, uma versão específica deste padrão) define (com o nível de formalidade que o inglês permite) a sintaxe e a semântica de uma família de linguagens de programação. C tem muitos comportamentos indefinidos e comportamentos definidos pela implementação. Uma "implementação" de C codifica todo o comportamento definido pela implementação (a lista de itens a serem codificados está no apêndice J da C99). Cada implementação de C é uma linguagem de programação separada. Observe que o significado da palavra “implementação” é um pouco peculiar: o que realmente significa é uma variante de linguagem, pode haver vários programas de compilação diferentes que implementam a mesma variante de linguagem.
Em uma determinada implementação de C, um byte possui valores possíveis de CHAR_BIT . Todos os dados podem ser representados como uma matriz de bytes: um tipo tem no máximo 2 CHAR_BIT × sizeof (t) valores possíveis. Esse número varia em diferentes implementações de C, mas para uma determinada implementação de C, é uma constante.2CHAR_BIT 2CHAR_BIT×sizeof(t)
t
Em particular, os ponteiros podem ter no máximo . Isso significa que existe um número máximo finito de objetos endereçáveis.2CHAR_BIT×sizeof(void*)
Os valores de
CHAR_BIT
esizeof(void*)
são observáveis; portanto, se você ficar sem memória, não poderá simplesmente continuar executando o programa com valores maiores para esses parâmetros. Você estaria executando o programa em uma linguagem de programação diferente - uma implementação em C diferente.Se os programas em uma linguagem só podem ter um número limitado de estados, a linguagem de programação não é mais expressiva que os autômatos finitos. O fragmento de C restrito ao armazenamento endereçável permite apenas no máximo onde n é o tamanho da árvore de sintaxe abstrata do programa (representando o estado do fluxo de controle); programa pode ser simulado por um autômato finito com tantos estados. Se C é mais expressivo, deve ser através do uso de outros recursos.n×2CHAR_BIT×sizeof(void*) n
C não impõe diretamente uma profundidade máxima de recursão. É permitido que uma implementação tenha um máximo, mas também não é permitido. Mas como nos comunicamos entre uma chamada de função e seu pai? Os argumentos não são bons se forem endereçáveis, porque isso indiretamente limitaria a profundidade da recursão: se você tiver uma função
int f(int x) { … f(…) …}
, todas as ocorrências dex
quadros ativosf
terão seu próprio endereço e, portanto, o número de chamadas aninhadas será limitado pelo número de endereços possíveis parax
.O programa CA pode usar armazenamento não endereçável na forma de
register
variáveis. As implementações “normais” podem ter apenas um número pequeno e finito de variáveis que não têm um endereço, mas, em teoria, uma implementação pode permitir uma quantidade ilimitada deregister
armazenamento. Em tal implementação, você pode fazer uma quantidade ilimitada de chamadas recursivas para uma função, desde que seu argumento sejaregister
. Mas, como os argumentos sãoregister
, você não pode fazer um ponteiro para eles e, portanto, precisa copiar explicitamente os dados deles: só é possível transmitir uma quantidade finita de dados, não uma estrutura de dados de tamanho arbitrário feita de ponteiros.Com profundidade de recursão ilimitada e a restrição de que uma função pode obter apenas dados do chamador direto (
register
argumentos) e retornar dados ao chamador direto (o valor de retorno da função), você obtém o poder dos autômatos de empilhamento determinístico .Não consigo encontrar um caminho a percorrer.
(É claro que você pode fazer com que o programa armazene o conteúdo da fita externamente, através das funções de entrada / saída de arquivo. Mas você não perguntaria se C é Turing-completo, mas se C mais um sistema de armazenamento infinito é Turing-completo, para qual a resposta é um chato "sim". Você também pode definir o armazenamento como um oráculo de Turing - chamada
fopen("oracle", "r+")
,fwrite
o conteúdo inicial da fita efread
o conteúdo final da fita.)fonte
A adição de C99
va_copy
ao API de argumento variado pode nos dar uma porta dos fundos para a integridade de Turing. Como é possível iterar por meio de uma lista de argumentos variados mais de uma vez em uma função diferente daquela que originalmente recebeu os argumentos,va_args
pode ser usado para implementar um ponteiro sem ponteiro.Obviamente, uma implementação real da API de argumento variável provavelmente terá um ponteiro em algum lugar, mas em nossa máquina abstrata ela pode ser implementada usando magia.
Aqui está uma demonstração implementando um autômato de empilhamento de 2 pilhas com regras de transição arbitrárias:
Nota: Se
va_list
for um tipo de matriz, na verdade existem parâmetros de ponteiro ocultos para as funções. Portanto, provavelmente seria melhor alterar os tipos de todos osva_list
argumentos parawrapped_stack
.fonte
va_list
variáveis automáticasstack
. Essas variáveis devem ter um endereço&stack
e só podemos ter um número limitado delas. Esse requisito pode ser contornado declarando todas as variáveis locaisregister
, talvez?register
.int
ser necessário ter um limite, a menos que alguém o use ousizeof(int)
?int
tem um valor entre alguns limites finitosINT_MIN
eINT_MAX
. E se o valor de umint
exceder o limite, ocorrerá um comportamento indefinido. Por outro lado, o padrão intencionalmente não exige que todos os objetos estejam fisicamente presentes na memória em um endereço específico, pois isso permite otimizações, como armazenar o objeto em um registro, armazenar apenas parte do objeto, representando-o de forma diferente do padrão. layout ou omiti-lo completamente, se não for necessário.Aritmética fora do padrão, talvez?
Então, parece que o problema é do tamanho finito de
sizeof(t)
. No entanto, acho que conheço uma solução alternativa.Até onde eu sei, C não requer uma implementação para usar os números inteiros padrão para seu tipo inteiro. Portanto, poderíamos usar um modelo aritmético não-padrão . Em seguida,
sizeof(t)
definiríamos um número fora do padrão e agora nunca o alcançaremos em um número finito de etapas. Portanto, o comprimento da fita das máquinas de Turing sempre será menor que o "máximo", pois o máximo é literalmente impossível de alcançar.sizeof(t)
simplesmente não é um número no sentido regular da palavra.Este é um tecnicismo, é claro: o teorema de Tennenbaum . Ele afirma que o único modelo de aritmética Peano é o padrão, o que obviamente não daria. No entanto, até onde eu sei, C não exige implementações para usar tipos de dados que satisfaçam os axiomas do Peano, nem exige que a implementação seja computável, portanto isso não deve ser um problema.
O que deve acontecer se você tentar gerar um número inteiro fora do padrão? Bem, você pode representar qualquer número inteiro fora do padrão usando uma sequência não padrão, portanto, basta transmitir os dígitos da frente dessa sequência.
fonte
sizeof(t)
em si é um valor do tiposize_t
, portanto, é um número inteiro natural entre 0 eSIZE_MAX
.Na IMO, uma forte limitação é que o espaço endereçável (através do tamanho do ponteiro) é finito e isso é irrecuperável.
Alguém poderia advogar que a memória pode ser "trocada para o disco", mas em algum momento as informações do endereço excederão o tamanho endereçável.
fonte
Na prática, essas restrições são irrelevantes para a integridade de Turing. O requisito real é permitir que a fita seja arbitrária por muito tempo, não infinita. Isso criaria um problema de parada de um tipo diferente (como o universo "calcula" a fita?)
É tão falso quanto dizer "Python não é Turing completo porque você não pode fazer uma lista infinitamente grande".
[Edit: obrigado ao Sr. Whitledge por esclarecer como editar.]
fonte
size_t
é finito. O problema é que você não pode estabelecer um limitesize_t
válido para todo o cálculo: para qualquer limite, um programa pode estourá-lo. Mas a linguagem C afirma que existe um limite parasize_t
: em uma determinada implementação, ela só pode crescer atésizeof(size_t)
bytes. Além disso, seja legal . Dizer que as pessoas que o criticam "não conseguem pensar por conta própria" é rude.A mídia removível nos permite contornar o problema de memória ilimitada. Talvez as pessoas pensem que isso é um abuso, mas acho que está tudo bem e é inevitável, de qualquer maneira.
Corrija qualquer implementação de uma máquina universal de Turing. Para a fita, usamos mídia removível. Quando a cabeça sai do final ou do início do disco atual, a máquina solicita que o usuário insira o próximo ou o anterior. Podemos usar um marcador especial para indicar a extremidade esquerda da fita simulada ou ter uma fita sem limites nas duas direções.
O ponto principal aqui é que tudo o que o programa C deve fazer é finito. O computador precisa apenas de memória suficiente para simular o autômato e
size_t
precisa ser grande o suficiente para permitir endereçar essa quantidade (realmente pequena) de memória e dos discos, que podem ter qualquer tamanho finito fixo. Como o usuário é solicitado apenas a inserir o disco seguinte ou anterior, não precisamos de números inteiros ilimitados para dizer "Insira o número do disco 123456 ..."Suponho que a principal objeção provavelmente seja o envolvimento do usuário, mas isso parece inevitável em qualquer implementação, porque parece não haver outra maneira de implementar a memória ilimitada.
fonte
Escolha
size_t
ser infinitamente grandeVocê pode optar
size_t
por ser infinitamente grande. Naturalmente, é impossível realizar essa implementação. Mas isso não é surpresa, dada a natureza finita do mundo em que vivemos.Implicações práticas
Mas, mesmo que fosse possível realizar essa implementação, haveria questões práticas. Considere a seguinte instrução C:
SIZE_MAX
SIZE_MAX
size_t
SIZE_MAX
printf
Felizmente, para nossos propósitos teóricos, não pude encontrar nenhum requisito na especificação que garanta
printf
que terminará para todas as entradas. Portanto, até onde sei, não violamos a especificação C aqui.Sobre Completude Computacional
Ainda resta provar que nossa implementação teórica é Turing Complete . Podemos mostrar isso implementando "qualquer máquina de Turing com uma única fita".
A maioria de nós provavelmente implementou uma Máquina de Turing como um projeto escolar. Não vou dar os detalhes de uma implementação específica, mas aqui está uma estratégia comumente usada:
Agora vamos ver o que é necessário para realizar essa implementação:
MAX_INT
ser infinitos. (Como alternativa, poderíamos usar outros objetos para representar estados e símbolos.)malloc
, mas há um pouco mais a considerar:malloc
falhar se, por exemplo, a memória disponível tiver sido esgotada. Portanto, nossa implementação é verdadeiramente universal, semalloc
nunca falhar.malloc
falha. Sem violar o padrão C, nossa implementação garantirá quemalloc
nunca falhará.Portanto, a lista acima é o que é necessário para implementar uma Máquina de Turing em nossa hipotética implementação em C. Esses recursos devem terminar. Qualquer outra coisa, no entanto, pode ter permissão para não terminar (a menos que exigido pela norma). Isso inclui aritmética, E / S, etc.
fonte
printf("%zu\n",SIZE_MAX);
impresso em tal implementação?sizeof(size_t)
(ouCHAR_BITS
). Você não pode retomar a partir do novo estado, é preciso começar novamente, mas a execução do programa pode ser diferente agora que essas constantes são diferentesO argumento principal aqui foi que o tamanho do size_t é finito, embora possa ser infinitamente grande.
Existe uma solução alternativa para isso, embora não tenha certeza se isso coincide com a ISO C.
Suponha que você tenha uma máquina com memória infinita. Portanto, você não está limitado ao tamanho do ponteiro. Você ainda tem seu tamanho size_t. Se você me perguntar o que é sizeof (size_t), a resposta será apenas sizeof (size_t). Se você perguntar se é maior que 100, por exemplo, a resposta é sim. Se você perguntar o que é sizeof (size_t) / 2, como você pode imaginar, a resposta ainda é sizeof (size_t). Se você quiser imprimi-lo, podemos concordar com alguns resultados. A diferença desses dois pode ser NaN e assim por diante.
O resumo é que relaxar a condição para size_t ter tamanho finito não interromperá nenhum programa já existente.
PS Ainda é possível alocar tamanho de memória sizeof (size_t), você só precisa de tamanho contável, então digamos que você faça todos os ajustes (ou truque semelhante).
fonte
sizeof
tem que retornar asize_t
. Então você tem que escolher algum valor específico.Sim.
1. Resposta citada
Como reação à grande quantidade de votos negativos nas minhas (e outras) respostas corretas - em comparação com a alarmante aprovação de respostas falsas -, procurei uma explicação alternativa menos teoricamente profunda. Eu encontrei este . Espero que abranja algumas das falácias comuns aqui, para que um pouco mais de insight seja mostrado. Parte essencial do argumento:
Em resumo, porque para cada função computável existe uma solução na linguagem C (devido aos limites superiores ilimitados), todo problema computável possui um programa C, portanto C é Turing-completo.
2. Minha resposta original
Existem confusões generalizadas entre conceitos matemáticos na ciência da computação teórica (como a integralidade de Turing) e sua aplicação no mundo real, ou seja, técnicas em ciência da computação prática. A perfeição de Turing não é uma propriedade de máquinas fisicamente existentes ou de qualquer modelo limitado no espaço-tempo. É apenas um objeto abstrato que descreve propriedades de teorias matemáticas.
O C99 é Turing-complete, independentemente das restrições baseadas na implementação, como praticamente qualquer outra linguagem de programação comum, pois é capaz de expressar um conjunto funcionalmente completo de conectivos lógicos e, em princípio, tem acesso a uma quantidade ilimitada de memória. As pessoas apontaram que C restringe explicitamente o acesso à memória aleatória para ser limitado, mas isso não é algo que não possa ser contornado, uma vez que essas são restrições adicionais declaradas no padrão C, enquanto a integridade de Turing é necessária sem elas :
Aqui está uma coisa muito básica sobre sistemas lógicos que deve ser suficiente para uma prova não construtiva . Considere um cálculo com alguns esquemas e regras de axioma, de modo que o conjunto de conseqüências lógicas seja X. Agora, se você adicionar algumas regras ou axiomas, o conjunto de consequências lógicas aumentará, ou seja, deve ser um superconjunto de X. É por isso que, por exemplo, , a lógica modal S4 está adequadamente contida em S5. Da mesma forma, quando você tem uma subespecificação completa de Turing, mas adiciona algumas restrições na parte superior, elas não impedem nenhuma das conseqüências em X, ou seja, deve haver uma maneira de contornar todas as restrições. Se você deseja um idioma não completo de Turing, o cálculo deve ser reduzido, não estendido. Extensões que afirmam que algo não seria possível, mas na verdade são, apenas adicionam inconsistência. Essas inconsistências no padrão C podem não ter conseqüências práticas, assim como a integridade de Turing não está relacionada à aplicação prática.
Simulando números arbitrários baseado em profundidade de recursão (ou seja, este , com a possibilidade de apoiar vários números através de agendamento / pseudo-threads; não há limite teórico para a profundidade de recursão em C ), ou usando o armazenamento de arquivos para simular memória programa ilimitado ( ideia ) são provavelmente apenas duas dentre infinitas possibilidades para provar construtivamente a completude de Turing de C99. É preciso lembrar que, para computabilidade, a complexidade do tempo e do espaço é irrelevante. Em particular, assumir um ambiente limitado para falsificar a completude de Turing é apenas um raciocínio circular, uma vez que essa limitação exclui todos os problemas que excedem o limite de complexidade pressuposto.
( NOTA : Eu apenas escrevi esta resposta para impedir que as pessoas sejam impedidas de obter intuição matemática devido a algum tipo de pensamento limitado orientado a aplicativos. É uma pena que a maioria dos alunos leia a resposta falsa aceita por ser votada com base em falhas fundamentais de raciocínio, para que mais pessoas espalhem essas falsas crenças. Se você rejeitar esta resposta, você será apenas parte do problema.)
fonte
CHAR_BITS
.