8.4 Turing-Maschinen codieren

Wir haben mit der Turingmaschine ein einfaches aber doch sehr mächtiges Modell einer Rechenmaschine kennengelernt. Sie haben vielleicht mittlerweile - auch durch Ihre Programmiererfahrung - das Gefühl, das man im Prinzip alles, was man überhaupt programmieren kann, auch auf einer Turingmaschine hinkriegt. Versetzen Sie sich nun in die Lage der Menschen vor ungefähr 100 Jahren. Damals gab es durchaus Rechenmaschinen. Maschinen zum Addieren und sogar zum Multiplizieren gibt es schon seit dem 17. Jahrhunder (Wikipedia: Mechanical calculator). Leider musste man für jede Aufgabe eine neue Maschine erfinden und auch bauen. Zahlen addieren? Maschine bauen. Multiplizieren? Neue Maschine bauen. Verschlüsselung brechen? Neue Maschine bauen. Und so weiter.

Die Idee einer programmierbaren Maschine, die erstmals circa 1834 mit Charles Babbage aufkam, ist, dass man neben den Eingabedaten (z.B. die zu multiplizierenden Zahlen) auch die Rechenvorschrift (das Programm) als Eingabe übergibt. Hätte man so eine Maschine, dann müsste man nicht für jede neue Aufgabe eine neue Maschine entwerfen; man könnte eine Maschine bauen und sie für die jeweilige Aufgabe programmieren, indem ihr auf einem separaten Eingabeband die Rechenvorschrift überreicht. Von heute aus gesehen ist diese Idee nicht mehr allzu überraschend, weil diese Maschinen überall anzutreffen sind. Damals aber war es revolutionär. Um dies, zumindest auf dem Papier, in die Realität zu übersetzen, müssen wir zwei Fragen beantworten.

  1. Wie können wir eine Rechenvorschrift (d.h. ein Programm) so codifizieren, dass wir es im Prinzip als eine Zeichenkette aufschreiben und einer Maschine übergeben können?
  2. Welche Maschine könnte so eine Rechenvorschrift lesen und sie an gegebenen Eingabedaten dann auch ausführen?

Es stellt sich heraus, dass wir beide Antworten (beinahe) schon kennen. Eine beliebige Rechenvorschrift können wir, da sind wir uns mittlerweile recht sicher, als Turingmaschine M implementieren. Diese können wir über einem Alphabet codieren und erhalten ein Wort enc(M). Wie tun wir das? Nun ja, auf turingmachinesimulator.com haben wir das bereits getan: eine Turingmaschine mit Alphabet Σ können wir dort als String über dem Alphabet Σ{a,,z,A,Z,0,,9}{<,-,>,,,_,\n,} codieren. Codierung ist im Prinzip kein Problem, wir werden aber ein paar Subtilitäten ansprechen.

Punkt 2 ist schwieriger. Können wir eine Maschine bauen, die als eingabe (1) die Codierung enc(M) einer Turingmaschine und (2) ein Eingabewort wΣ entgegennimmt und dann die Berechnung M(x) simuliert bzw. zu dem Ergebnis gelangt, zu dem auch M(x) gelangen würde? Die Programmierer von turingmachinesimulator.com haben dies offensichtlich geschafft: sie haben eine Maschine "gebaut" (also wohl einen Server gemietet und eine Webseite mit viel Javascript programmiert), der eine Turingmaschine in einer spezifischen Codierung und ein Eingabewort einliest und dann diese simuliert.

In diesem Teilkapitel werden wir sehen, wie wir eine Turingmaschine M über einem fixen, nicht von M abhängigen Alphabet codieren können. Im nächsten Teilkapitel werden wir uns überlegen, wie man einen Turingmaschinen-Simulator selbst als Turingmaschine implementieren kann. Also eine Turingmaschine U, die als Input Wörter der Form c#x entgegennimmt und dann

  1. ablehnt, falls c keine gültige Codierung einer Turingmaschine M ist,
  2. ansonsten, falls also c=enc(M), dann
    1. akzeptiert falls M(x) akzeptiert;
    2. ablehnt, falls M(x) ablehnt
    3. nicht terminiert, falls M(x) nicht terminiert.
Falls wir bei M nicht nur an Akzeptieren / Ablehnen interessiert sind, sondern am Ergebnis der Berechnung, dann hätten wir gerne, dass U(enc(M)#x) am Ende den gleichen Bandinhalt hat wie M(x) am Ende; hierbei gibt es allerdings eine Schwierigkeit mit den Details der Codierung, die wir gleich ansprechen werden.

Die Codierung

Zuerst müssen wir uns auf ein Eingabealphabet Σ einigen. Im Ernstfall genügt immer Σ={0,1}, allerdings gibt es keinen Grund, für die Definitionen nicht allgemeine endliche Alphabete Σ zuzulassen. Wir wollen nun ein Codierungsalphabet Λ und eine Codierungsfunktion enc, so dass enc(M)Λ für jede Turingmaschine M mit Eingabealphabet Σ gilt.

Erster, zum scheitern verurteilter Versuch. Sei M eine Turingmaschine mit Eingabealphabet Σ, Arbeitsalphabet Γ, Zustandsmenge Q, Startzustand qstart, akzeptierendem Zustand qyes und Übergangsfunktion δ. Wir codieren M wie folgt: wenn δ(q,x)=(r,y,R)δ(q,y)=(s,z,L) dann schreiben wir in der Codierung qstart#qyes#qxryR#qyszL# Unser Codierungsalphabet ist also Λ:=QΓ{#,L,S,R} . Sehen Sie das Problem? Das Codierungsalphabet ist nicht uniform: wir brauchen, abhängig von der Zustandsmenge Q und dem Bandalphabet Γ jeweils neue Alphabete. Wir wollen aber ein Λ, da für alle Turingmaschinen mit Eingabealphabet Σ funktioniert.

Zweiter, erfolgreicher Versuch.

Wir müssen also die Zustandsmenge Q und die Zeichen ΓΣ erst einmal selbst codieren, beispielsweise über dem Alphabet {0,1}. Die δ-Tabelle der Turingmaschine für {anbncn}
würde dann zu folgender Tabelle:

Wenn wir dies nun als ein Wort in obigen Schema schreiben, können wir für eine Tabellenzelle δ(q,x)=(r,y,R) nicht einfach qxryR schreiben, auch nicht einfach die Codierungen zusammenschreiben: in diesem Falle würde nämlich δ(00,0)=(11,0,R) zu 000110R und wir würden nicht mehr erkennen, wo welches Zeichen beginnt und aufhört. Wir brauchen ein Separatorzeichen, beispielsweise ein Komma. Aus Gründen, die später klar werden werden, schließen wir die Codierung der Turingmaschine mit einem ; ab. Die Codierung der obigen Maschine ist dann also

00#100#00,a,01,1,S#00,b,L#00,c,L#00,1,L#00,0,11,0,R#01,a,R#01,b,10,1,S#01,1,R#10,b,R#10,c,00,1,S#10,1,R#11,1,R#11,0,100#;

In dieser Codierung behalten wir zwei Konventionen bei: wenn eine Regel "fehlt", also beispielsweise für δ(10,a) die Zelle leer ist, dann soll das in den Zustand qno führen; wenn in der Zelle nur ein Richtungszeichen, also beispielsweise 01,1,R steht, dann ist das eine Abkürzung für δ(01,1)=(01,1,R), also #01,1,01,1,R#

Wir können nun jede Turingmaschine über dem Alphabet Σ codieren als Wort über dem Alphabet Λ:=Σ{0,1,#,,,L,S,R,;}

Definition/Beobachtung. 8.4.1 Zu einem Eingabealphabet Σ definieren wir das Codierungsalphabet Λ:=Σ{0,1,#,,,L,S,R,;}, wobei wir annehmen, dass #,,,L,S,R,;Σ. Wir können nun jede Turingmaschine M mit Eingabealphabet Σ als String enc(M)Λ codieren.

Anmerkungen: das Wort Codierung suggeriert, dass wir, gegeben den String c=enc(M) die ursprüngliche Turingmaschine M rekonstruieren können. Das gilt natürlich nur beschränkt: eventuell decodieren wir c zu einer Maschine M, die sich von M in denen Namen der Zustände und der Bandalphabetsymbole unterscheidet. Allerdings stimmen die Funktionen fM:Σ{accept,reject,undefined} und fM:Σ{accept,reject,undefined} überein.