Sto pensando proprio ora a come convincermi che le macchine di Turing sono un modello generale di calcolo. Concordo sul fatto che il trattamento standard della tesi di Church-Turing in alcuni libri di testo standard, ad esempio Sipser, non sia molto completo. Ecco uno schizzo di come potrei passare dalle macchine di Turing a un linguaggio di programmazione più riconoscibile.
Prendi in considerazione un linguaggio di programmazione strutturato a blocchi con if
e while
istruzioni, con funzioni e subroutine definite non ricorsive , con variabili casuali booleane denominate ed espressioni booleane generali e con un singolo array booleano illimitato tape[n]
con un puntatore di array integer n
che può essere incrementato o decrementato, n++
oppure n--
. Il puntatore n
è inizialmente zero e l'arraytape
è inizialmente tutto zero. Quindi, questo linguaggio del computer può essere simile a C o Python, ma è molto limitato nei suoi tipi di dati. In realtà, sono così limitati che non abbiamo nemmeno un modo per usare il puntatore n
in un'espressione booleana. Supponendo chetape
è solo infinito a destra, possiamo dichiarare un underflow puntatore "errore di sistema" se n
mai negativo. Inoltre, la nostra lingua ha una exit
dichiarazione con un argomento, per dare una risposta booleana.
Quindi il primo punto è che questo linguaggio di programmazione è un buon linguaggio di specifica per una macchina Turing. Si può facilmente vedere che, ad eccezione dell'array di nastri, il codice ha solo molti stati possibili: lo stato di tutte le sue variabili dichiarate, l'attuale linea di esecuzione e il suo stack di subroutine. Quest'ultimo ha solo una quantità limitata di stato perché le funzioni ricorsive non sono consentite. Si potrebbe immaginare un "compilatore" che crea una macchina Turing "reale" da un codice di questo tipo, ma i dettagli non sono importanti. Il punto è che abbiamo un linguaggio di programmazione con una sintassi abbastanza buona, ma tipi di dati molto primitivi.
Il resto della costruzione è di convertirlo in un linguaggio di programmazione più vivibile con un elenco finito di funzioni di libreria e fasi di precompilazione. Possiamo procedere come segue:
Con un precompilatore, possiamo espandere il tipo di dati booleani in un alfabeto di simboli più grande ma finito come ASCII. Possiamo assumere che tape
assume valori in questo alfabeto più grande. Possiamo lasciare un marcatore all'inizio del nastro per impedire il underflow del puntatore, e un marcatore mobile alla fine del nastro per impedire che il TM pattini all'infinito sul nastro accidentalmente. Siamo in grado di implementare operazioni binarie arbitrarie tra simboli e conversioni in booleane if
e while
dichiarazioni. (In realtà if
può essere implementato anche con while
, se non fosse disponibile.)
KKioioK
Designiamo un nastro come "memoria" con valori simbolici e gli altri come "registri" o "variabili" senza segno, con valori interi. Conserviamo gli interi in binario little-endian con marcatori di terminazione. Per prima cosa implementiamo la copia di un registro e il decremento binario di un registro. Combinando questo con l'incremento e il decremento del puntatore di memoria, possiamo implementare la ricerca ad accesso casuale della memoria dei simboli. Possiamo anche scrivere funzioni per calcolare l'addizione binaria e la moltiplicazione di numeri interi. Non è difficile scrivere una funzione di aggiunta binaria con operazioni bit a bit e una funzione da moltiplicare per 2 con spostamento a sinistra. (O proprio il turno giusto, poiché è little-endian.) Con queste primitive, possiamo scrivere una funzione per moltiplicare due registri usando l'algoritmo di moltiplicazione lungo.
Possiamo riorganizzare il nastro di memoria da una matrice di simboli unidimensionale symbol[n]
a una matrice di simboli bidimensionali symbol[x,y]
utilizzando la formula n = (x+y)*(x+y) + y
. Ora possiamo usare ogni riga della memoria per esprimere un numero intero senza segno in binario con un simbolo di terminazione, per ottenere una memoria unidimensionale, ad accesso casuale, con valori interi memory[x]
. Possiamo implementare la lettura dalla memoria a un registro intero e la scrittura da un registro alla memoria. Molte funzioni possono ora essere implementate con funzioni: aritmetica con segno e virgola mobile, stringhe di simboli, ecc.
Solo un'altra struttura di base richiede rigorosamente un precompilatore, ovvero funzioni ricorsive. Questo può essere fatto con una tecnica ampiamente utilizzata per implementare linguaggi interpretati. Assegniamo a ogni funzione di alto livello e ricorsiva una stringa di nomi e organizziamo il codice di basso livello in un unico grande while
ciclo che mantiene uno stack di chiamate con i soliti parametri: il punto di chiamata, la funzione chiamata e un elenco di argomenti.
A questo punto, la costruzione ha abbastanza caratteristiche di un linguaggio di programmazione di alto livello che ulteriori funzionalità sono più l'argomento dei linguaggi di programmazione e dei compilatori piuttosto che la teoria CS. È anche già facile scrivere un simulatore di Turing-machine in questo linguaggio sviluppato. Scrivere un autocompilatore per la lingua non è esattamente semplice, ma sicuramente standard. Naturalmente è necessario un compilatore esterno per creare la TM esterna da un codice in questo linguaggio simile a C o Python, ma ciò può essere fatto in qualsiasi linguaggio informatico.
Si noti che questa implementazione schematica supporta non solo la tesi Church-Turing dei logici per la classe di funzioni ricorsive, ma anche la tesi Church-Turing estesa (cioè polinomiale) in quanto si applica al calcolo deterministico. In altre parole, ha un sovraccarico polinomiale. In effetti, se ci viene fornita una macchina RAM o (il mio preferito) una TM a nastro d'albero, questo può essere ridotto al sovraccarico pollogaritmico per il calcolo seriale con memoria RAM.