Je réfléchis en ce moment à la façon de me convaincre que les machines de Turing sont un modèle général de calcul. Je suis d'accord que le traitement standard de la thèse de Church-Turing dans certains manuels standard, par exemple Sipser, n'est pas très complet. Voici un aperçu de la façon dont je pourrais passer des machines Turing à un langage de programmation plus reconnaissable.
Considérons un langage de programmation structuré en blocs avec if
et des while
instructions, avec des fonctions et des sous-routines définies non récursives , avec des variables booléennes aléatoires nommées et des expressions booléennes générales, et avec un tableau booléen non borné unique tape[n]
avec un pointeur de tableau entier n
qui peut être incrémenté ou décrémenté, n++
ou n--
. Le pointeur n
est initialement nul et le tableau tape
est initialement entièrement nul. Ainsi, ce langage informatique peut être de type C ou de type Python, mais il est très limité dans ses types de données. En fait, ils sont si limités que nous n'avons même pas le moyen d'utiliser le pointeur n
dans une expression booléenne. En admettant quetape
n'est infini que vers la droite, nous pouvons déclarer un dépassement de pointeur "erreur système" s'il n
est jamais négatif. De plus, notre langage a une exit
instruction avec un argument, pour sortir une réponse booléenne.
Ensuite, le premier point est que ce langage de programmation est un bon langage de spécification pour une machine de Turing. Vous pouvez facilement voir que, à l'exception du tableau de bandes, le code n'a qu'un nombre fini d'états possibles: l'état de toutes ses variables déclarées, la ligne d'exécution en cours et sa pile de sous-programmes. Ce dernier n'a qu'un état fini car les fonctions récursives ne sont pas autorisées. Vous pourriez imaginer un "compilateur" qui crée une machine de Turing "réelle" à partir d'un code de ce type, mais les détails de cela ne sont pas importants. Le fait est que nous avons un langage de programmation avec une syntaxe assez bonne, mais des types de données très primitifs.
Le reste de la construction consiste à le convertir en un langage de programmation plus vivable avec une liste finie de fonctions de bibliothèque et d'étapes de précompilation. On peut procéder comme suit:
Avec un précompilateur, nous pouvons étendre le type de données booléen à un alphabet de symboles plus grand mais fini tel que ASCII. Nous pouvons supposer que tape
prend des valeurs dans cet alphabet plus grand. Nous pouvons laisser un marqueur au début de la bande pour éviter le débordement du pointeur, et un marqueur mobile à la fin de la bande pour empêcher le TM de patiner à l'infini sur la bande accidentellement. Nous pouvons implémenter des opérations binaires arbitraires entre les symboles et des conversions en booléens pour if
et while
instructions. (En fait, if
peut également être implémenté avec while
, s'il n'était pas disponible.)
kkjejek
Nous désignons une bande comme "mémoire" à valeur de symbole et les autres comme "registres" ou "variables" à valeur entière non signés. Nous stockons les entiers en binaire petit-boutien avec des marqueurs de terminaison. Nous implémentons d'abord la copie d'un registre et la décrémentation binaire d'un registre. En combinant cela avec l'incrémentation et la décrémentation du pointeur de mémoire, nous pouvons implémenter la recherche d'accès aléatoire de la mémoire de symboles. Nous pouvons également écrire des fonctions pour calculer l'addition binaire et la multiplication d'entiers. Il n'est pas difficile d'écrire une fonction d'addition binaire avec des opérations au niveau du bit, et une fonction à multiplier par 2 avec décalage à gauche. (Ou vraiment décalage à droite, car il est peu endian.) Avec ces primitives, nous pouvons écrire une fonction pour multiplier deux registres en utilisant l'algorithme de multiplication longue.
Nous pouvons réorganiser la bande mémoire d'un tableau de symboles unidimensionnel symbol[n]
à un tableau de symboles bidimensionnels en symbol[x,y]
utilisant la formule n = (x+y)*(x+y) + y
. Nous pouvons maintenant utiliser chaque ligne de la mémoire pour exprimer un entier non signé en binaire avec un symbole de terminaison, pour obtenir une mémoire unidimensionnelle à accès aléatoire à valeur entière memory[x]
. Nous pouvons implémenter la lecture de la mémoire dans un registre entier et l'écriture d'un registre dans la mémoire. De nombreuses fonctionnalités peuvent désormais être implémentées avec des fonctions: arithmétique à virgule flottante et signée, chaînes de symboles, etc.
Une seule installation de base supplémentaire nécessite strictement un précompilateur, à savoir des fonctions récursives. Cela peut être fait avec une technique largement utilisée pour implémenter des langages interprétés. Nous attribuons à chaque fonction récursive de haut niveau une chaîne de nom et nous organisons le code de bas niveau en une grande while
boucle qui conserve une pile d'appels avec les paramètres habituels: le point d'appel, la fonction appelée et une liste d'arguments.
À ce stade, la construction a suffisamment de fonctionnalités d'un langage de programmation de haut niveau pour que la fonctionnalité supplémentaire soit davantage le thème des langages de programmation et des compilateurs plutôt que la théorie CS. Il est également déjà facile d'écrire un simulateur de machine de Turing dans ce langage développé. Il n'est pas exactement facile, mais certainement standard, d'écrire un auto-compilateur pour la langue. Bien sûr, vous avez besoin d'un compilateur externe pour créer la MT externe à partir d'un code dans ce langage de type C ou Python, mais cela peut être fait dans n'importe quel langage informatique.
Notez que cette mise en œuvre esquissée prend en charge non seulement la thèse Church-Turing des logiciens pour la classe de fonctions récursives, mais aussi la thèse Church-Turing étendue (c'est-à-dire polynomiale), en ce qui concerne le calcul déterministe. En d'autres termes, il a une surcharge polynomiale. En fait, si on nous donne une machine RAM ou (mon préféré) un tree-tape TM, cela peut être réduit à une surcharge polylogarithmique pour le calcul en série avec la mémoire RAM.