Ich denke gerade darüber nach, wie ich mich davon überzeugen kann, dass Turing-Maschinen ein allgemeines Rechenmodell sind. Ich stimme zu, dass die Standardbehandlung der Church-Turing-These in einigen Standardlehrbüchern, z. B. Sipser, nicht sehr vollständig ist. Hier ist eine Skizze, wie ich von Turing-Maschinen zu einer erkennbareren Programmiersprache gelangen könnte.
Betrachten Sie eine blockstrukturierte Programmiersprache mit if
und while
-Anweisungen mit nicht rekursiv definierten Funktionen und Unterprogrammen, mit benannten booleschen Zufallsvariablen und allgemeinen booleschen Ausdrücken sowie mit einem einzelnen unbegrenzten booleschen Array tape[n]
mit einem ganzzahligen Arrayzeiger n
, der inkrementiert oder dekrementiert werden kann, n++
oder n--
. Der Zeiger n
ist anfänglich Null und das Array tape
ist anfänglich alle Null. Diese Computersprache kann also C-ähnlich oder Python-ähnlich sein, ist jedoch in ihren Datentypen sehr begrenzt. Tatsächlich sind sie so begrenzt, dass wir nicht einmal die Möglichkeit haben, den Zeiger n
in einem booleschen Ausdruck zu verwenden. Vorausgesetzt, dasstape
Ist nur unendlich rechts, können wir einen Zeiger-Unterlauf als "Systemfehler" deklarieren, falls dieser n
jemals negativ ist. Unsere Sprache hat auch eine exit
Anweisung mit einem Argument, um eine boolesche Antwort auszugeben.
Dann ist der erste Punkt, dass diese Programmiersprache eine gute Spezifikationssprache für eine Turing-Maschine ist. Sie können leicht erkennen, dass der Code mit Ausnahme des Bandarrays nur endlich viele mögliche Zustände aufweist: den Zustand aller deklarierten Variablen, die aktuelle Ausführungszeile und seinen Unterprogrammstapel. Letzteres hat nur eine begrenzte Menge an Zustand, da rekursive Funktionen nicht zulässig sind. Sie können sich einen "Compiler" vorstellen, der aus einem Code dieses Typs eine "tatsächliche" Turing-Maschine erstellt, aber die Details dazu sind nicht wichtig. Der Punkt ist, dass wir eine Programmiersprache mit ziemlich guter Syntax, aber sehr primitiven Datentypen haben.
Der Rest der Konstruktion besteht darin, dies in eine lebenswertere Programmiersprache mit einer endlichen Liste von Bibliotheksfunktionen und Vorkompilierungsstufen umzuwandeln. Wir können wie folgt vorgehen:
Mit einem Precompiler können wir den booleschen Datentyp auf ein größeres, aber endliches Symbolalphabet wie ASCII erweitern. Wir können davon ausgehen, dass dies tape
Werte in diesem größeren Alphabet annimmt. Wir können eine Markierung am Anfang des Bandes hinterlassen, um ein Unterlaufen des Zeigers zu verhindern, und eine bewegliche Markierung am Ende des Bandes, um zu verhindern, dass das TM versehentlich auf dem Band bis ins Unendliche läuft. Wir können beliebige binäre Operationen zwischen Symbolen und Konvertierungen in boolesche for- if
und while
Anweisungen implementieren . ( if
Kann tatsächlich auch mit implementiert werden while
, wenn es nicht verfügbar wäre.)
kkiik
Wir bezeichnen ein Band als symbolwertigen "Speicher" und die anderen als vorzeichenlose, ganzzahlige "Register" oder "Variablen". Wir speichern die ganzen Zahlen in Little-Endian-Binärdateien mit Terminierungsmarkern. Wir implementieren zuerst eine Kopie eines Registers und eine binäre Dekrementierung eines Registers. In Kombination mit dem Inkrementieren und Dekrementieren des Speicherzeigers können wir eine Direktzugriffssuche des Symbolspeichers implementieren. Wir können auch Funktionen schreiben, um die binäre Addition und Multiplikation von ganzen Zahlen zu berechnen. Es ist nicht schwer, eine binäre Additionsfunktion mit bitweisen Operationen und eine Funktion zum Multiplizieren mit 2 mit Linksverschiebung zu schreiben. (Oder wirklich Rechtsverschiebung, da es Little-Endian ist.) Mit diesen Grundelementen können wir eine Funktion schreiben, um zwei Register unter Verwendung des langen Multiplikationsalgorithmus zu multiplizieren.
Mit der Formel können wir das Speicherband von einem eindimensionalen Symbolarray symbol[n]
in ein zweidimensionales Symbolarray umorganisieren . Wir können jetzt jede Zeile des Speichers verwenden, um eine vorzeichenlose Ganzzahl in Binärform mit einem Abschlusssymbol auszudrücken und einen eindimensionalen Speicher mit wahlfreiem Zugriff und ganzzahligem Wert zu erhalten . Wir können das Lesen aus dem Speicher in ein ganzzahliges Register und das Schreiben aus einem Register in den Speicher implementieren. Viele Funktionen können jetzt mit Funktionen implementiert werden: Vorzeichen- und Gleitkomma-Arithmetik, Symbolzeichenfolgen usw.symbol[x,y]
n = (x+y)*(x+y) + y
memory[x]
Nur eine weitere grundlegende Funktion erfordert unbedingt einen Precompiler, nämlich rekursive Funktionen. Dies kann mit einer Technik erfolgen, die häufig zur Implementierung interpretierter Sprachen verwendet wird. Wir weisen jeder übergeordneten rekursiven Funktion eine Namenszeichenfolge zu und organisieren den Code auf niedriger Ebene in einer großen while
Schleife, die einen Aufrufstapel mit den üblichen Parametern verwaltet: dem Aufrufpunkt, der aufgerufenen Funktion und einer Liste von Argumenten.
Zu diesem Zeitpunkt verfügt die Konstruktion über genügend Funktionen einer höheren Programmiersprache, sodass weitere Funktionen eher das Thema von Programmiersprachen und Compilern als die CS-Theorie sind. Es ist auch schon einfach, einen Turing-Maschinensimulator in dieser entwickelten Sprache zu schreiben. Es ist nicht gerade einfach, aber sicherlich Standard, einen Self-Compiler für die Sprache zu schreiben. Natürlich benötigen Sie einen äußeren Compiler, um das äußere TM aus einem Code in dieser C- oder Python-ähnlichen Sprache zu erstellen, aber das kann in jeder Computersprache erfolgen.
Beachten Sie, dass diese skizzierte Implementierung nicht nur die Church-Turing-These der Logiker für die rekursive Funktionsklasse unterstützt, sondern auch die erweiterte (dh polynomielle) Church-Turing-These, wie sie für deterministische Berechnungen gilt. Mit anderen Worten, es hat einen Polynom-Overhead. Wenn wir eine RAM-Maschine oder (mein persönlicher Favorit) ein Tree-Tape TM erhalten, kann dies für die serielle Berechnung mit RAM-Speicher auf polylogarithmischen Overhead reduziert werden.