Сейчас я думаю о том, как убедить себя, что машины Тьюринга - это общая модель вычислений. Я согласен с тем, что стандартная трактовка тезиса Черча-Тьюринга в некоторых стандартных учебниках, например, «Сипсер», не очень полная. Вот набросок того, как я могу перейти от машин Тьюринга к более узнаваемому языку программирования.
Рассмотрит блок-структурированный язык программирования с if
и while
заявление, с не-рекурсивными определенными функциями и подпрограммами, с именованными булевыми случайными величинами и общими логическими выражениями, и с одной неограниченным булевым массивом tape[n]
с указателем массива целого , n
которое может быть увеличено или уменьшено, n++
или n--
, Первоначально указатель n
равен нулю, а массив tape
изначально равен нулю. Таким образом, этот компьютерный язык может быть C-подобным или Python-подобным, но он очень ограничен в своих типах данных. На самом деле они настолько ограничены, что у нас даже нет способа использовать указатель n
в логическом выражении. При условии, чтоtape
только бесконечно вправо, мы можем объявить указатель на понижение "системной ошибкой", если n
когда-либо будет отрицательным. Кроме того, наш язык имеет exit
оператор с одним аргументом для вывода логического ответа.
Тогда первое, что этот язык программирования является хорошим языком спецификаций для машины Тьюринга. Вы можете легко увидеть, что, кроме ленточного массива, код имеет только конечное число возможных состояний: состояние всех объявленных им переменных, текущая строка выполнения и его стек подпрограмм. Последний имеет только конечное количество состояний, потому что рекурсивные функции не допускаются. Вы можете представить себе «компилятор», который создает «реальную» машину Тьюринга из кода этого типа, но детали этого не важны. Дело в том, что у нас есть язык программирования с довольно хорошим синтаксисом, но очень примитивными типами данных.
Остальная часть конструкции состоит в том, чтобы преобразовать это в более удобный язык программирования с конечным списком библиотечных функций и этапов предварительной компиляции. Мы можем действовать следующим образом:
С помощью прекомпилятора мы можем расширить логический тип данных до большего, но конечного символьного алфавита, такого как ASCII. Можно предположить, что tape
принимает значения в этом большем алфавите. Мы можем оставить маркер в начале ленты, чтобы предотвратить переполнение указателя, и подвижный маркер в конце ленты, чтобы предотвратить случайное перемещение ТМ на бесконечность на ленте. Мы можем реализовать произвольные двоичные операции между символами и преобразования в логические выражения for if
и while
. (На самом деле if
может быть реализован с помощью while
, если он не был доступен.)
ККяяК
Мы обозначаем одну ленту как символьную «память», а другие - как беззнаковые, целочисленные «регистры» или «переменные». Мы храним целые числа в двоичном порядке с прямым порядком байтов с маркерами завершения. Сначала мы реализуем копию регистра и двоичный декремент регистра. Комбинируя это с увеличением и уменьшением указателя памяти, мы можем реализовать поиск произвольного доступа к памяти символов. Мы также можем написать функции для вычисления двоичного сложения и умножения целых чисел. Нетрудно написать двоичную функцию сложения с побитовыми операциями и функцию умножения на 2 с левым сдвигом. (Или действительно правое смещение, поскольку оно имеет младший порядок.) С этими примитивами мы можем написать функцию для умножения двух регистров, используя алгоритм длинного умножения.
Мы можем реорганизовать ленту памяти из одномерного массива символов symbol[n]
в двумерный массив символов, symbol[x,y]
используя формулу n = (x+y)*(x+y) + y
. Теперь мы можем использовать каждую строку памяти, чтобы выразить целое число без знака в двоичном виде с символом завершения, чтобы получить одномерную, целочисленную память с произвольным доступом memory[x]
. Мы можем реализовать чтение из памяти в целочисленный регистр и запись из регистра в память. Многие функции теперь могут быть реализованы с помощью функций: арифметика со знаком и с плавающей запятой, символьные строки и т. Д.
Только еще одно базовое средство строго требует прекомпилятора, а именно рекурсивные функции. Это можно сделать с помощью техники, которая широко используется для реализации интерпретируемых языков. Мы присваиваем каждой высокоуровневой рекурсивной функции строку имени и организуем низкоуровневый код в один большой while
цикл, который поддерживает стек вызовов с обычными параметрами: вызывающей точкой, вызываемой функцией и списком аргументов.
На данный момент в конструкции есть достаточно возможностей языка программирования высокого уровня, поэтому дальнейшая функциональность - это скорее тема языков программирования и компиляторов, а не теория CS. Также уже легко написать симулятор машины Тьюринга на этом развитом языке. Это не совсем легко, но, безусловно, стандартно, написать самокомпилятор для языка. Конечно, вам нужен внешний компилятор для создания внешней TM из кода на этом C-подобном или Python-подобном языке, но это можно сделать на любом компьютерном языке.
Обратите внимание, что эта схематичная реализация не только поддерживает тезис Черча-Тьюринга для рекурсивного класса функций, но и расширенный (т. Е. Полиномиальный) тезис Черч-Тьюринга применительно к детерминированным вычислениям. Другими словами, он имеет полиномиальные накладные расходы. Фактически, если нам дается машина с ОЗУ или (мой личный фаворит) ТМ с древовидной структурой, это может быть уменьшено до полилогарифмических издержек для последовательных вычислений с ОЗУ.