Зараз я думаю про те, як переконати себе, що машини Тьюрінга є загальною моделлю обчислень. Я погоджуюсь, що стандартне трактування тези Церкви Тьюрінга в деяких стандартних підручниках, наприклад, Сіпсера, не дуже повно. Ось ескіз того, як я можу перейти від машин Тьюрінга до більш впізнаваної мови програмування.
Розгляне блок-структурована мова програмування з if
і while
заяву, з НЕ-рекурсивними певними функціями і підпрограмами, з іменованими булеві випадковими величинами і загальними логічними виразами, і з однієї необмеженим булевим масивом tape[n]
з покажчиком масиву цілого , n
яке може бути збільшена або зменшена, n++
або n--
. Вказівник n
спочатку дорівнює нулю, а масив tape
спочатку є рівним нулю. Так, ця комп’ютерна мова може бути схожою на C або Python, але вона дуже обмежена у своїх типах даних. Насправді вони настільки обмежені, що у нас навіть немає способу використовувати вказівник n
у булевому виразі. Якщо припустити, щоtape
є нескінченним праворуч, ми можемо оголосити підводний підтік "системною помилкою", якщо n
він коли-небудь негативний. Крім того, наша мова має exit
висловлювання з одним аргументом, щоб виводити булеву відповідь.
Тоді першим моментом є те, що ця мова програмування є хорошою мовою специфікації для машини Тьюрінга. Ви легко бачите, що, за винятком стрічкового масиву, код має лише кінцево багато можливих станів: стан усіх заявлених змінних, поточний рядок виконання та стек підпрограми. Останній має лише обмежений стан, оскільки рекурсивні функції заборонені. Ви можете уявити собі "компілятор", який створює "фактичну" машину Тьюрінга з коду такого типу, але деталі цього не важливі. Справа в тому, що у нас мова програмування з досить хорошим синтаксисом, але дуже примітивними типами даних.
Решта конструкції полягає в перетворенні цього на більш зручну мову програмування з кінцевим списком функцій бібліотеки та етапів попередньої компіляції. Ми можемо діяти так:
За допомогою прекомпілятора ми можемо розширити булеві типи даних до більшого, але кінцевого алфавіту символів, такого як ASCII. Можна припустити, що tape
приймає значення в цьому більшому алфавіті. Ми можемо залишити маркер на початку стрічки, щоб запобігти перетіканню вказівника, і рухомий маркер в кінці стрічки, щоб запобігти випадковому ковзанню ТМ до нескінченності на стрічці. Ми можемо реалізовувати довільні бінарні операції між символами та перетвореннями на булі для if
та while
операторів. (Насправді це if
може бути реалізовано while
також, якщо воно не було доступне.)
Ми хочемо необмежений цілий тип даних для того, щоб реалізувати як випадковий доступ до стрічки, так і (позитивну) цілу арифметику. З цією метою ми моделюємо стрічку TM для деякого фіксованого однією стрічкою, яку ми маємо. Ця побудова дана як теорема Шипсера. Ідея полягає в тому, щоб переплетені емульовані стрічки на низькорівневій стрічці у нас із маркерними символами, що представляють положення голови. Якщо покажчик стрічки низького рівня дорівнює нулю, він обслуговує й піддіапазон, переміщуючись у положення а потім стрибаючи кроків за один раз; після кожного читання чи запису низький рівень покажчика стрічки зменшується до нуля. Як і на попередньому етапі, це легше реалізувати як попередній компілятор.k i i kkkiik
Одну стрічку ми позначаємо як "пам'ять", а іншу - "регістри" або "змінні", які не мають підпису, цілими значеннями. Ми зберігаємо цілі числа в двійковій системі малої ендіану з маркерами закінчення. Спочатку реалізуємо копію реєстру та бінарний декрет регістра. Поєднуючи це з збільшенням і зменшенням покажчика пам'яті, ми можемо здійснити пошук випадкового доступу до пам'яті символів. Ми також можемо записати функції для обчислення двійкового додавання та множення цілих чисел. Не важко записати двійкову функцію додавання з побітними операціями, а функцію помножити на 2 при зсуві вліво. (Або дійсно правильний зсув, оскільки це малоеквієнт.) За допомогою цих примітивів ми можемо записати функцію множення двох регістрів, використовуючи довгий алгоритм множення.
Ми можемо реорганізувати стрічку пам'яті з одновимірного масиву символів symbol[n]
у двовимірний масив символів symbol[x,y]
за допомогою формули n = (x+y)*(x+y) + y
. Тепер ми можемо використовувати кожен рядок пам'яті для вираження безпідписаного цілого числа у двійковій формі із символом закінчення, для отримання одновимірної пам'яті з цілим значенням у випадковому доступі memory[x]
. Ми можемо реалізувати читання з пам'яті в цілий регістр і запис з регістра в пам'ять. Зараз багато функцій можна реалізувати за допомогою функцій: арифметика з підписами та плаваючою точкою, рядки символів тощо.
Тільки ще один базовий інструмент суворо вимагає докомпілятора, а саме рекурсивних функцій. Це можна зробити за допомогою методики, яка широко використовується для реалізації інтерпретованих мов. Кожній рекурсивній функції високого рівня ми призначаємо рядок імен і організовуємо код низького рівня в один великий while
цикл, який підтримує стек виклику із звичайними параметрами: точка виклику, викликана функція та список аргументів.
На даний момент конструкція має достатньо функцій мови програмування високого рівня, що подальша функціональність - це більше тема мов програмування та компіляторів, а не теорія CS. Так само вже легко написати тренажер машини Тюрінга на цій розробленій мові. Написати самокомпілятор для мови не просто, але, звичайно, стандартно. Звичайно, вам потрібен зовнішній компілятор, щоб створити зовнішній ТМ з коду на цій мові, подібній С або Python, але це можна зробити на будь-якій мові комп'ютера.
Зауважимо, що ця замальована реалізація підтримує не лише тезу Церкви-Тьюрінга логіків для рекурсивного функціонального класу, але й розширену (тобто поліноміальну) тезу Церкви-Тьюрінга, як це стосується детермінованих обчислень. Іншими словами, він має многочлен. Насправді, якщо нам дають машину оперативної пам’яті або (мій особистий фаворит) ТМ-дерево, це може бути зведено до полілогоарифмічних накладних витрат для послідовних обчислень з оперативною пам’яттю.