Пару месяцев назад прочитал пост, в котором уважаемая ksusha написала эмулятор машины Тьюринга используя MySQL и хранимые процедуры. Статья дала толчок к идее сделать машину Тьюринга на чистом SQL, без использования хранимых процедур. Для реализации был использован знакомый и любимый Firebird версии 2.1.
Существует две принципиальные проблемы при создании машины Тьюринга на голом SQL:
Тем не менее, мне удалось обойти эти ограничения
Для начала я написал машину Тьюринга на обычной хранимой процедуре. Не знаю, зачем ksuha использовала рекурсию, я сделал обычным циклом и на бесконечной в обе стороны ленте. Ничего примечательного в том коде нет, поэтому сразу приступил к реализации на «голом» SQL.
Для решения первой проблемы в Firebird 2.1 имеется аж две конструкции: MERGE и UPDATE OR INSERT.
Сначала планировал воспользоваться второй, однако с ее помощью можно модифицировать или вставить только 1 запись. MERGE позволяет «склеить» таблицу с произвольной выборкой.
Для хранения состояний после ряда экспериментов было решено использовать рекурсивные запросы, которые тоже появились в версии 2.1.
Для хранения программы и ленты использовалась структура таблиц, аналогичная исходному посту.
Вот что в итоге у меня получилось:
в вычисляемом поле state храним состояние, pos — позиция на ленте.
конструкция «FROM RDB$DATABASE» — это такая «особенность» Firebird, используется когда нужно сформировать выборку с одной строчкой без какой-либо таблицы вообще.
Таким образом, можно считать, что DML SQL в реализации Firebird 2.1 можно считать языком программирования, полным по Тьюрингу. Теоретически, подобный запрос можно выполнить на любой СУБД, поддерживающей рекурсивные запросы и конструкции вроде MERGE, с учетом отличий синтаксиса.
Первоначально, планировалось хранить состояние в генераторе, он же SEQUENCE. Однако написать такой запрос — источник новых данных для ленты, чтобы он мог бегать по программе и ленте «туда-сюда» мне не удалось. Тем не менее, этот эксперимент позволил найти несколько интересных решений при работе с генераторами:
Надо сказать, что вложенность рекурсивных запросов Firebird огранична 1024 уровнями, поэтому если мне все-таки удастся сделать это на генераторах — ограничение будет снято.
Firebird 2.1 Release Notes
Существует две принципиальные проблемы при создании машины Тьюринга на голом SQL:
- 1) лента машины может быть и модифицирована и дописана, что требует операторов INSERT и UPDATE в одной конструкции;
- 2) машина Тьюринга требует как минимум одной переменной для состояния. Обычные SQL(DML)-запросы не могут хранить промежуточных переменных, по крайней мере в Firebird.
Тем не менее, мне удалось обойти эти ограничения
Для начала я написал машину Тьюринга на обычной хранимой процедуре. Не знаю, зачем ksuha использовала рекурсию, я сделал обычным циклом и на бесконечной в обе стороны ленте. Ничего примечательного в том коде нет, поэтому сразу приступил к реализации на «голом» SQL.
Для решения первой проблемы в Firebird 2.1 имеется аж две конструкции: MERGE и UPDATE OR INSERT.
Сначала планировал воспользоваться второй, однако с ее помощью можно модифицировать или вставить только 1 запись. MERGE позволяет «склеить» таблицу с произвольной выборкой.
Для хранения состояний после ряда экспериментов было решено использовать рекурсивные запросы, которые тоже появились в версии 2.1.
Для хранения программы и ленты использовалась структура таблиц, аналогичная исходному посту.
Вот что в итоге у меня получилось:
MERGE INTO ribbon r USING ( WITH RECURSIVE data AS ( SELECT 0 AS state, 1 AS pos, (SELECT sinput FROM ribbon WHERE id = 1) AS val FROM RDB$DATABASE UNION ALL SELECT p.out_state AS STATE, CASE WHEN p.actions = '<' THEN data.pos - 1 WHEN p.actions = '>' THEN data.pos + 1 ELSE data.pos END AS pos, CASE WHEN p.actions = '<' THEN COALESCE((SELECT sinput FROM ribbon WHERE id = (data.pos - 1)), ' ') WHEN p.actions = '>' THEN COALESCE((SELECT sinput FROM ribbon WHERE id = (data.pos + 1)), ' ') ELSE p.actions END AS val FROM program p JOIN data ON data.state = p.in_state WHERE p.sread = COALESCE((SELECT sinput FROM ribbon WHERE id = data.pos), ' ') AND p.actions != '#' ) SELECT * FROM data ) data ON data.pos = r.id WHEN MATCHED THEN UPDATE SET sinput = data.val WHEN NOT MATCHED THEN INSERT (id, sinput) VALUES (data.pos, data.val)
в вычисляемом поле state храним состояние, pos — позиция на ленте.
конструкция «FROM RDB$DATABASE» — это такая «особенность» Firebird, используется когда нужно сформировать выборку с одной строчкой без какой-либо таблицы вообще.
Таким образом, можно считать, что DML SQL в реализации Firebird 2.1 можно считать языком программирования, полным по Тьюрингу. Теоретически, подобный запрос можно выполнить на любой СУБД, поддерживающей рекурсивные запросы и конструкции вроде MERGE, с учетом отличий синтаксиса.
вместо послесловия
Первоначально, планировалось хранить состояние в генераторе, он же SEQUENCE. Однако написать такой запрос — источник новых данных для ленты, чтобы он мог бегать по программе и ленте «туда-сюда» мне не удалось. Тем не менее, этот эксперимент позволил найти несколько интересных решений при работе с генераторами:
- 1) чтобы подвинуть генератор в секции WHERE можно воспользоваться конструкцией
При любом значении increment она возвращает истину;GEN_ID(POS, <increment>)=GEN_ID(POS, 0) - 2) обнулить генератор можно конструкцией
Используя совместно с 1) можно обнулить генератор в условии WHERE.GEN_ID(POS, -GEN_ID(POS, 0))
Надо сказать, что вложенность рекурсивных запросов Firebird огранична 1024 уровнями, поэтому если мне все-таки удастся сделать это на генераторах — ограничение будет снято.
структура таблиц
CREATE TABLE PROGRAM ( IN_STATE INTEGER NOT NULL, SREAD CHAR(1) NOT NULL, ACTIONS CHAR(1) NOT NULL, OUT_STATE INTEGER NOT NULL ); CREATE TABLE RIBBON ( SINPUT CHAR(1) NOT NULL, ID INTEGER NOT NULL );
источники
Firebird 2.1 Release Notes
