TM·IX Пределы вычислений Глава 56 из 65

Разговор с Оракулом

Глава-диалог. Ваш собеседник — Оракул, который берётся по тексту любой программы сказать, остановится ли она. Вы спорите с ним, пока он не сдастся: строите программу, которая делает наперекор любому его ответу, и выясняете, что от этого не спасают ни хитрость, ни маленькие программы, ни скромные вопросы. По дороге — Гильберт в Кёнигсберге, куайн из главы 0 в роли оружия, усердный бобёр, побеждённый в 2024 году, и задача в духе Коллатца, спрятанная в машине из шести состояний.

Дальше 70 минут Теория вычислений Математика История Открытые проблемы
TM·IX

Пределы вычислений

  1. 54 Автоматы
  2. 55 Машина Тьюринга
  3. 56 Неразрешимое вы здесь
  4. 57 P и NP
  5. 58 Трудные задачи

Опирается на: 55 · Машина Тьюринга 00 · Что умеет программа

Что вы унесёте из главы

  • доказывать, что проблема остановки неразрешима, и объяснять доказательство через диагональ и через самоссылку
  • сводить одну задачу к другой и по теореме Райса узнавать вопросы о программах, на которые не ответит ни один анализатор
  • понимать, что умеют настоящие анализаторы кода, антивирусы и проверки типов вместо невозможного: приближения, лимиты и урезанные языки

9Есть ли задачи, которые не решит никакой компьютер?

Прошлая глава кончилась вопросом: любой ли точный вопрос имеет ответ, который найдёт машина? Один из таких вопросов встречался нам уже трижды. В главе 0 — три строки про Коллатца, о которых никто не знает, остановятся ли они на каждом числе. В главе 53 — желание написать проверку посильнее проверки типов: программу, которая читает любую программу и говорит, не зависнет ли та. В прошлой главе — симулятор, который умеет только ждать. На вопрос прошлой главы ответ — «нет», но верить на слово не нужно: мы получим этот ответ в споре с тем, кто уверен в обратном.

Глава написана как диалог, по образцу Дугласа Хофштадтера и его книги «Гёдель, Эшер, Бах» (1979): между главами у него беседуют Ахилл и Черепаха, позаимствованные у Льюиса Кэрролла, а через их споры проходят самые трудные идеи книги. Ваш собеседник — Оракул. Это слово — тоже заимствование: в 1938 году Тьюринг в диссертации, написанной в Принстоне под руководством Чёрча, рассматривал машины, которые на трудный вопрос могут обратиться к «оракулу»: как он работает, Тьюринг не объясняет, но ответы его всегда верны. Наш Оракул утверждает, что он и есть такое устройство.

Кёнигсберг, сентябрь 1930

Гёдель накануне подорвал одну половину этой уверенности: не всякое истинное утверждение можно доказать. Об этом — в «Царице наук», в главе о Гёделе. Вторую половину — что для любого точного вопроса найдётся способ отвечать механически — через шесть лет опровергли Чёрч и Тьюринг. К их ответу мы и придём в разговоре.

Прелюдия. Лавка Оракула

Оракул. Входите. Дайте мне текст любой программы, и я скажу, остановится она или будет работать вечно. Всегда. Без ошибок.

Вы. Хорошо. while True: pass.

Оракул. Вечно. Это и ребёнок скажет.

Вы. А путь числа 27 по правилу Коллатца?

Оракул. Остановится через 111 шагов. Я просто проследил.

Вы. Значит, вы её запускаете. А если программа не остановилась — сколько вы ждёте, прежде чем сказать «вечно»?

Оракул. Сто тысяч строк. Кто не управился за сто тысяч, тот не управится никогда.

Это первая и самая естественная попытка: запустить программу и подождать. Модуль курса cs.oracle содержит такого Оракула: функция halts(source, steps) выполняет программу, считая строки, и если программа не закончилась за steps строк, отвечает «не остановится». Поймать его нетрудно.

Терпеливая программа выполняет две строки на круг — проверку условия и прибавление, — всего 120 002 строки, и в бюджет в сто тысяч не помещается. Оракул объявил её вечной, хотя она закончится через доли секунды. Любой бюджет ломается так же: для лимита в миллион найдётся программа, которой нужен миллион и одна строка.

Сформулируем вопрос, о котором спорим. Проблема остановки — по тексту программы и её входу определить, закончит ли программа работу. Задачу с ответом «да» или «нет» называют разрешимой, если есть программа, которая на любом входе останавливается и даёт верный ответ. Вся тяжесть определения лежит на двух словах. «На любом» — Оракул обязан справляться не только с теми программами, которые ему приятны. «Останавливается» — ответ «подождите ещё» не считается.

Оракул. Я пошутил про сто тысяч строк. Никаких программ я не запускаю. Я их понимаю. Как — секрет фирмы.

Вы. Пусть так. Тогда сделаем вас функцией: halts(program) возвращает True или False, всегда, и всегда верно. Как устроена функция внутри, мне неважно. Я хочу понять, может ли она вообще существовать.

Оракул. Сколько угодно. Спрашивайте.

Действие первое. Упрямец

Вы. Вот программа. Назову её «упрямец». Первым делом она спрашивает вас о себе самой: остановится ли упрямец? Если вы скажете «остановится», она уходит в вечный цикл. Если «не остановится» — сразу заканчивает работу.

Оракул. Покажите.

Вы. Так остановится ли упрямец?

Оракул. Сейчас посмотрю… Если я скажу «да», он зациклится. Значит, правильный ответ «нет». Но если я скажу «нет», он остановится. Значит, правильный ответ… Подождите.

Вы. Я подожду. Только помните уговор: вы обязаны ответить.

Попробуйте сыграть за Оракула. Отвечайте что угодно и сколько угодно раз, меняйте стратегию от раза к разу.

Машина-парадокс. Вы — Оракул: выберите ответ, и упрямец выполнится с этим ответом. Подсвечена строка, на которой программа сейчас. Счёт внизу — сколько раз Оракул ошибся.

Оракул проигрывает всегда, и дело не в неудачном выборе. Упрямец устроен так, что делает противоположное любому ответу. Значит, верного ответа на вопрос «остановится ли упрямец» Оракул дать не может — а ведь ответ существует: упрямец либо останавливается, либо нет. Остаётся одно: такого Оракула не бывает. Запишем это строго.

Не существует программы halts(P), которая для любой программы P останавливается и верно отвечает, остановится ли P.

Предположим, такая программа есть. Напишем упрямца — программу D, которая вызывает halts(D), получает ответ за конечное время (так обещано) и делает наоборот: на True зацикливается, на False останавливается. Как программе передать в halts саму себя, разберём в следующем действии, — это возможно.

Посмотрим, что ответит halts(D). Если True, то D по своему устройству зацикливается, и ответ неверен. Если False, то D останавливается, и ответ снова неверен. Третьего halts не дано: она обязана остановиться и ответить. Противоречие, значит, предположение ложно.

Доказательство короче этого абзаца, и в этом его сила: оно не зависит от того, как halts устроена внутри. Простая она или сложная, перебирает ли она варианты, обучена ли на миллионе программ — упрямец строится по ней одной и тот же. Стоит запомнить и то, чего теорема не говорит: она не утверждает, что про остановку конкретной программы ничего нельзя узнать, — про while True: pass можно. Не бывает лишь одного способа на все программы.

Действие второе. Зеркало

Оракул. Я нашёл у вас дыру. Ваш упрямец спрашивает меня о себе, значит, передаёт мне свой текст. Но если программа содержит свой текст, то этот текст содержит программу, которая содержит текст… Она бесконечна. Такой программы нет, и спорить не о чем.

Вы. Вы рассуждаете, как я в главе 0. Там была программа в две строки, которая печатает сама себя.

В главе 7 мы разобрали, как устроен куайн: шаблон программы с одной дыркой, в которую подставляется запись самого шаблона. Тем же приёмом программа может получить свой текст для любого дела, печать — лишь одно из них. Вот упрямец, которого можно запустить. Он спрашивает Оракула, что сам напечатает, — «да» или «нет», — и печатает обратное. Это та же ловушка, только вместо «остановлюсь ли я» — «что я скажу», так её проще проверить. Сначала мы отдадим его честному Оракулу из cs.oracle, который отвечает, запуская программу, а потом двум Оракулам, которые отвечают не думая.

Первые четыре строки вывода — сам упрямец, программа конечная и вполне обычная. Честный Оракул, чтобы узнать, что она напечатает, запускает её — и она снова спрашивает Оракула, и тот снова запускает её. Он никогда не ответит, и мы останавливаем его на четвёртом круге. Два «нечестных» Оракула отвечают сразу и оба ошибаются: упрямец говорит наоборот. Последняя строка подтверждает, что Оракулу упрямец передал свой текст буква в букву.

То, что сработало здесь, работает всегда. В 1938 году Стивен Клини доказал теорему о рекурсии: какое бы преобразование текстов программ мы ни взяли, найдётся программа, которая получает собственный текст и применяет к нему это преобразование. Напечатать себя — частный случай, преобразование «ничего не менять»; это куайн из главы 0. Спросить о себе Оракула и сделать наоборот — другой.

Для любой программы $T$, которая принимает текст программы и вход, существует программа $R$, которая на любом входе $x$ делает то же, что $T$ на паре (текст $R$, $x$). Иначе говоря, всякая программа может считать, что ей дан её собственный текст.

Это более глубокий ответ на первый большой вопрос курса. Напечатать себя программа может, но это меньшее из её умений: любая программа может получить свой текст и делать с ним что угодно — печатать, считать хеш, отправлять Оракулу. Самоссылка в вычислениях не парадокс, а обычный инструмент. Парадокс возникает только у того, кто обещал отвечать на все вопросы.

Действие третье. Диагональ

Оракул. Хорошо, упрямца я не одолею. Но это один уродец, сделанный нарочно против меня. Обещаю отвечать на все нормальные программы, а на упрямцев — пусть ошибаюсь. Их ведь немного.

Вы. Упрямец вырастает из любого Оракула сам, по диагонали. Нарисую таблицу, и вы увидите как.

Все программы можно выписать в один бесконечный список: программы — конечные тексты, а конечные тексты можно упорядочить сначала по длине, а при равной длине — по алфавиту. Пусть $P_0, P_1, P_2, \ldots$ — этот список. Входы тоже пронумеруем: $0, 1, 2, \ldots$ Составим бесконечную таблицу: в клетке $(i, j)$ — останавливается ли программа $P_i$ на входе $j$. Если бы Оракул существовал, он умел бы заполнить любую клетку.

Теперь пройдём по диагонали — по клеткам $(0, 0), (1, 1), (2, 2), \ldots$ — и построим программу $D$, которая на входе $n$ делает наоборот по сравнению с клеткой $(n, n)$: если $P_n$ на входе $n$ останавливается, $D$ зацикливается, и наоборот. $D$ отличается от $P_0$ на входе 0, от $P_1$ — на входе 1, от каждой $P_n$ — на входе $n$. Значит, $D$ нет в списке. Но в списке все программы, а $D$ — программа: если Оракул есть, её можно написать, вызывая его на каждой диагональной клетке. Противоречие то же, что с упрямцем, — упрямец и есть $D$, спрошенная о себе.

Диагональная таблица. Строки — программы, столбцы — входы; ● — остановится, ∞ — нет. Нажимайте на клетки, чтобы менять таблицу, — диагональная программа $D$ внизу перестраивается сама. Нажмите на строку слева, чтобы сравнить её с $D$: они расходятся на диагонали. «Добавить $D$ в таблицу» сделает её новой строкой — и у таблицы появится новая диагональ.

Тем же приёмом Георг Кантор в 1891 году доказал, что действительных чисел больше, чем натуральных, — день третий в «Бесконечностях» «Царицы наук». И у него есть следствие покрепче самой теоремы. Программ — счётное множество: их можно перенумеровать. А задач с ответом «да» или «нет» о натуральных числах столько же, сколько подмножеств натурального ряда: каждая задача — это множество чисел, на которых ответ «да». Таких множеств, по той же диагонали Кантора, несчётно много. Программ на всех не хватит, и неразрешимые задачи составляют подавляющее большинство. Странно скорее другое: задачи, которые нам попадаются, так часто оказываются разрешимыми.

Действие четвёртое. Скромные вопросы

Оракул. Сдаюсь насчёт остановки. Но этот вопрос и не нужен никому. Я стану скромнее. Например, буду говорить, напечатает ли программа слово «привет». Тут нечему зацикливаться: напечатала или нет.

Вы. Тогда я сделаю из вас прежнего Оракула. Дайте мне любую программу P. Я напишу программу Q: она молча выполняет P, а когда та закончит, печатает «привет». Q печатает «привет» ровно тогда, когда P останавливается. Спрошу вас про Q — и узнаю про остановку P.

Оракул. А я только что согласился, что про остановку знать нельзя.

Вы. Вот именно.

Это рассуждение называют сведением. Задача A сводится к задаче B, если любой вход A можно механически переделать во вход B с тем же ответом. Тогда решатель B решал бы и A. И обратно: если A неразрешима, то неразрешима и B — иначе решатель B решил бы A. Сведение — главный инструмент этой части курса: в следующей главе им же будут доказывать, что задача трудна.

Схема сведения к вопросу о «привете» — три строки:

Слово «молча» здесь не украшение. Если P сама печатает «привет» и потом зависает, Q не должна выдать её ответ за свой. И если P падает с ошибкой — это тоже остановка, и Q должна дожить до своей последней строки. Довести схему до работающего кода — задача «Сведение» в конце главы. Та же схема годится и для любого другого «скромного» вопроса.

Конструктор сведений. Выберите вопрос, на который якобы отвечает анализатор, и программу $P$. Внизу — программа $Q$, построенная по $P$: её ответ на выбранный вопрос совпадает с ответом на вопрос «остановится ли $P$». Анализатор, отвечающий на этот вопрос всегда и верно, был бы Оракулом остановки.

Ни один из вопросов конструктора не говорит об остановке. «Будет ли деление на ноль», «обратится ли программа к сети», «вернёт ли 42», «удалит ли файл» — это вопросы о том, что программа делает. И для каждого сработала одна и та же схема: выполнить P молча, а потом сделать то, о чём спрашивают. В 1951 году Генри Гордон Райс заметил, что так можно поступить с любым подобным вопросом, и доказал это в диссертации; напечатана теорема в 1953 году.

Пусть свойство программ зависит только от того, что программа делает (какие входы она переводит в какие выходы и на каких зацикливается), а не от того, как она записана. Если у одних программ свойство есть, а у других нет, то не существует программы, которая по тексту любой программы определяет, есть ли у неё это свойство.

Пусть программа, которая никогда не останавливается, свойством не обладает (иначе рассмотрим отрицание свойства — оно тоже нетривиально). Возьмём программу $W$, у которой свойство есть. По любой программе $P$ построим $Q$: на входе $x$ она сначала молча выполняет $P$, а потом делает то же, что $W$ на $x$. Если $P$ останавливается, $Q$ ведёт себя в точности как $W$ и свойством обладает. Если не останавливается, $Q$ ничего не делает вечно, как программа без свойства. Детектор свойства на $Q$ отвечал бы, остановится ли $P$, а это невозможно.

Теорема Райса — это короткий ответ на множество практических вопросов. Идеального антивируса нет: «программа заражает другие файлы» — свойство поведения. Фред Коэн, один из первых исследователей компьютерных вирусов, ставил опыты уже в 1983 году, когда учился у Леонарда Адлемана (это его «A» в названии RSA из главы 60). В статье 1984 года (журнальная версия вышла в 1987-м) Коэн доказал, что никакой алгоритм не обнаруживает все возможные вирусы. Идеальной проверки типов нет: «программа никогда не сложит строку с числом» — свойство поведения, и поэтому mypy в главе 53 отвергал невиновные программы. По той же причине ни один линтер не найдёт весь мёртвый код, ни один оптимизатор не построит лучшую программу, а тесты не докажут, что ошибок нет.

И всё же анализаторы существуют и приносят пользу: каждый нарушает одно из трёх обещаний Оракула — отвечать всегда, отвечать верно, отвечать про любую программу, — а какое, выбирает сам. Одни отвечают не всегда: «не знаю» — тоже ответ, так работают проверки, которые просят подсказать им типы. Другие ошибаются, но в безопасную сторону: проверка типов отвергает сомнительное (лучше ложная тревога, чем пропущенная ошибка), а антивирус по сигнатурам, наоборот, пропускает незнакомое, зато почти не поднимает ложных тревог. Частый случай — лимит времени, стратегия Оракула из прелюдии, только признанная открыто: когда время вышло, честный инструмент говорит «не знаю», а торопливый рискует ошибиться. А третьи меняют язык: если программы пишутся на языке, неполном по Тьюрингу, как конфигурации Starlark из прошлой главы, теорема Райса на них не распространяется, и многое становится разрешимым.

Вопрос «что сделает эта программа» в общем случае не решает никакой анализатор. Любой работающий инструмент — компилятор, линтер, антивирус, проверка типов — либо иногда молчит, либо иногда ошибается, либо работает с урезанным языком. Когда инструмент обещает всё сразу, он что-то недоговаривает.

Действие пятое. Бобры

Оракул. Последнее предложение. Я отвечаю только про маленькие программы. Машин Тьюринга с двумя состояниями конечное число — я разберу их все и выучу ответы наизусть. Потом с тремя, с четырьмя…

Вы. С двумя — пожалуйста. С пятью тоже можно, но на это у людей ушло больше шестидесяти лет. А с шестью никто не знает как. Сейчас покажу вам бобров.

Мы встречали их на шестом уровне игры в прошлой главе. В 1962 году Тибор Радо задал вопрос: сколько шагов может сделать машина Тьюринга с $n$ состояниями и символами 0 и 1, запущенная на пустой ленте, — при условии, что она в конце концов остановится? Максимум называют числом усердного бобра $BB(n)$, а машину, которая его достигает, — чемпионом.

Машины ниже записаны так, как их пишут участники проекта bbchallenge: строка на состояние, в строке по правилу на каждый символ — что записать, куда сдвинуться и куда перейти; Z — остановка. Модуль cs.turing из прошлой главы читает эту запись функцией standard.

Шесть шагов, двадцать один, сто семь. Чемпион с пятью состояниями за миллион шагов не остановился и не собирается: ему нужно 47 176 870 шагов. В песочнице курса это заняло бы секунды, а на загруженном сервере могло бы не уложиться в лимит ячейки. Браузеру хватает доли секунды: в гонке ниже чемпиона можно досчитать до конца.

Гонка бобров. Каждая дорожка — чемпион своего числа состояний: счётчик справа — сделанные шаги, зелёная шкала под именем показывает их в логарифмическом масштабе, а полоса под ней — его ленту в эту минуту (тёмное — единицы, цветная черта — головка). «Случайная машина» тянет из лотереи машину с четырьмя состояниями: одни останавливаются сразу, другие явно зациклились, а про некоторые за миллион шагов так ничего и не скажешь.

С пятью состояниями пришлось так трудно, а с шестью, возможно, не выйдет никогда, потому что бобры — та же проблема остановки в другой одежде.

Функция $BB(n)$ невычислима: нет программы, которая по любому $n$ выдаёт $BB(n)$.

Пусть такая программа есть. Тогда решалась бы остановка любой машины $M$ на пустой ленте: если у $M$ $n$ состояний, вычислим $BB(n)$ и запустим $M$ на $BB(n) + 1$ шагов. Не остановилась — значит, не остановится никогда, иначе она побила бы рекорд, который по определению максимален. А остановка на пустой ленте неразрешима: к ней сводится общая проблема остановки, ведь вход можно «зашить» в саму машину несколькими состояниями, которые пишут его на ленту.

То же рассуждение показывает, что $BB(n)$ растёт быстрее любой вычислимой функции: если бы для какой-то вычислимой $f$ было $BB(n) \le f(n)$ при всех $n$, то $f$ заменила бы $BB$ в доказательстве. Цифры это подтверждают. Для шести состояний известно, что $BB(6)$ не меньше числа $2 \uparrow\uparrow\uparrow 5$ — это башня из двоек, высота которой равна башне из двоек высотой 65 536. И есть машина с 745 состояниями, которая останавливается, только если противоречива ZFC — система аксиом, на которой стоит почти вся математика. Если ZFC непротиворечива, то в ней невозможно доказать, что эта машина не останавливается, а значит, и узнать в ней значение $BB(745)$. Здесь проблема остановки встречается с теоремой Гёделя из Кёнигсберга.

Действие шестое. Коллатц

Оракул. Шесть состояний — это же совсем крошечная машина. Что в ней может быть такого, чего нельзя разобрать?

Вы. Гипотеза, которую вы видели в главе 0. Вернее, её близкая родственница.

В конце июня 2024 года, когда доказательство для пяти состояний было уже готово, участник bbchallenge mxdys сообщил о машине с шестью состояниями, поведение которой выглядело случайным, а вскоре участник под ником Racheline понял, что она делает. Машину назвали «Антигидрой». Если отбросить подробности, она выполняет вот такой процесс:

Число раз за разом умножается на полтора с отбрасыванием половинки, а счётчик получает два очка за каждое чётное значение и теряет одно за нечётное. Машина остановится, если нечётных значений когда-нибудь окажется больше, чем удвоенное число чётных, — тогда счётчик уйдёт в минус. Если чётность чисел ведёт себя как монетка, нечётных в среднем столько же, сколько чётных, счётчик растёт, и уйти в минус ему всё труднее. Скорее всего, Антигидра не остановится никогда. Но доказать это — значит решить задачу того же рода, что гипотеза Коллатца: про поведение простого арифметического правила на всех шагах сразу. Такие задачи математика решать пока не умеет, и поэтому $BB(6)$ упирается в нерешённую проблему ещё до того, как дело дойдёт до неразрешимости.

Связь Коллатца с этой главой глубже совпадения. В 1972 году Джон Конвей, автор «Жизни» из прошлой главы, рассмотрел обобщённые правила Коллатца вида «если число $n$ даёт остаток $r$ при делении на $m$, замени его на $a_r n + b_r$»; дробные коэффициенты в них подобраны так, чтобы результат оставался целым. Конвей доказал, что вопрос «дойдёт ли число до единицы» для таких правил неразрешим: на них можно запрограммировать любую машину Тьюринга. Позже он построил на этой основе язык FRACTRAN, где вся программа — список дробей. Сама гипотеза Коллатца может оказаться верной, неверной или недоказуемой; но вопрос, к которому она принадлежит, в общем виде неразрешим.

Финал. Лавка закрывается

Оракул. Подведём итог моего разорения. Про остановку я отвечать не могу — упрямец. Сделать вид, что упрямцев мало, нельзя — диагональ. Отвечать на скромные вопросы о поведении — тоже нельзя, к ним сводится остановка. Даже маленькие машины не спасают: уже на шести состояниях нужна гипотеза, которую никто не умеет доказывать.

Вы. Зато вы прекрасно отвечаете про большинство программ, которые вам приносят. Оставьте вывеску, только поменяйте одно слово: не «всегда», а «часто». И разрешите себе говорить «не знаю».

Оракул. Так поступают все, кто проверяет программы?

Вы. Все, кому можно верить.

Да, и это не временная слабость техники, а теорема. Есть точные вопросы с ответом «да» или «нет», на которые не ответит никакая программа, сколько ей ни дай времени и памяти. Главный из них — проблема остановки: по тексту программы узнать, закончит ли она работу. Доказательство помещается в абзац. Если бы программа-Оракул существовала, можно было бы написать упрямца, который спрашивает её о себе и делает наоборот; свой текст он получает, как куайн из главы 0. На вопрос об упрямце у Оракула нет верного ответа — значит, Оракула нет.

«Никакая программа» здесь значит «никакой компьютер». Глава 54 показала машину, про которую можно узнать всё, — конечный автомат, — и её пределы. Глава 55 — что машина Тьюринга вычисляет всё, что вычисляет любой компьютер, и по тезису Чёрча — Тьюринга всё, что вообще вычислимо механически. Эта глава — что даже она не отвечает на вопросы о себе подобных. Из одной неразрешимой задачи сведениями получаются другие: по теореме Райса неразрешим любой нетривиальный вопрос о поведении программ, поэтому не бывает идеального антивируса, проверки типов или оптимизатора. Функция «усердного бобра» невычислима, а неразрешимых задач в целом больше, чем разрешимых: программ счётное число, задач — несчётное.

Задачи

Три задачи. В первой вы сами становитесь упрямцем: программа должна получить свой текст без чтения файла и обмануть любого Оракула. Во второй — доводите до рабочего кода сведение из четвёртого действия, и тесты проверяют те ловушки, на которых оно ломается. В третьей — пишете симулятор бобров, достаточно быстрый, чтобы гонять миллионы шагов.

Напишите программу-упрямца. Она должна получить свой собственный текст символ в символ, передать его функции predict из модуля cs.oracle (импорт: from cs.oracle import predict) и напечатать одно слово: «нет», если Оракул ответил «да», и «да» в любом другом случае. Оракула нужно спросить ровно один раз. Тесты подменяют predict разными Оракулами — простыми и хитрыми — и проверяют, что программа спросила о своём точном тексте и сделала наоборот. Читать собственный файл и пользоваться модулями inspect, sys, os и им подобными нельзя.

Это куайн из главы 7, только текст вместо печати кладётся в переменную. Заведите шаблон s — текст всей программы, где на месте записи самого шаблона стоит %r, — и получите программу как s % s.

В шаблоне все знаки %, кроме %r, удваиваются: строка me = s % s в шаблоне пишется как me = s %% s. Переводы строк внутри шаблона — \n.

Проверить себя можно без Оракула: временно замените последнюю строку на print(me, end="") — получится куайн, и его вывод должен совпасть с программой. Если не совпадает, тесты тоже покажут первое расхождение. Потом верните последнюю строку на место, а в шаблоне она должна быть такой же, как в программе.

Четыре строки, и в них вся теорема Клини: программа получила свой текст, ничего не читая, и распорядилась им как хотела. Тест подставлял Оракулов, которые отвечают всегда «да», всегда «нет», по чётности длины текста, по наличию в нём слова «нет», — и ни один не угадал. Не угадает и любой другой: что бы Оракул ни ответил, упрямец печатает обратное. А Оракул из cs.oracle, который запускает программу, чтобы узнать ответ, не отвечает вовсе: попробуйте запустить решение без тестов.

Напишите make_q(p): по тексту программы p верните текст программы Q, которая печатает «привет» тогда и только тогда, когда p останавливается, — и больше ничего не печатает. «Останавливается» значит заканчивает работу любым способом: дойдя до конца, упав с ошибкой или вызвав sys.exit(). Если p не останавливается, Q тоже работает вечно и не печатает ничего. Тесты запускают Q отдельным процессом Python и для вечных программ ждут секунду. Программы p в тестах ничего не читают с клавиатуры.

Заготовка ломается трижды. Если p сама печатает, её вывод попадает в вывод Q. Если p падает или вызывает sys.exit(), до последней строки дело не доходит. А если в p есть кавычки или отступы, склейка может даже не скомпилироваться.

Не вставляйте текст p в код Q как код — вставьте его как строку, через repr, как в куайне, и выполните внутри Q функцией exec. Тогда ни кавычки, ни отступы не помешают.

Чужой вывод можно заглушить: contextlib.redirect_stdout(io.StringIO()) отправляет всё напечатанное в строку, которую никто не читает. Ошибки и sys.exit() ловит except BaseException: SystemExit — не Exception, обычный except Exception его пропустит.

Три ловушки — три строки защиты: repr превращает любой текст в безопасную строковую запись, redirect_stdout глушит чужой вывод, except BaseException ловит и ошибки, и выход через sys.exit. Эти подробности и прячет слово «молча» в схеме главы, и на них же сведения обычно ломаются при первой попытке. Теперь, если бы у вас был анализатор, который всегда верно говорит, напечатает ли программа «привет», analyzer(make_q(p)) отвечал бы на вопрос об остановке p. Такого анализатора, значит, нет.

Напишите bb_run(code, limit) — симулятор машин Тьюринга в записи bbchallenge. Запись — строки состояний A, B, C… через _; в строке по три знака на символ ленты 0 и 1: что записать, куда сдвинуться (L или R), в какое состояние перейти. Состояние Z (или H) — остановка: такое правило выполняется, как обычное, и считается шагом. Тройка --- значит «правила нет»: машина останавливается, не делая шага. Машина начинает в состоянии A на ленте из нулей. Верните кортеж (шагов, единиц на ленте), если машина остановилась не более чем за limit шагов, иначе None. Тесты гоняют чемпионов и машины-обманки, а последний тест даёт пять секунд на два миллиона шагов чемпиона с пятью состояниями.

Заготовка почти верна, но считает шаги на единицу меньше: остановка по правилу с Z — тоже шаг. Чемпион с двумя состояниями должен дать (6, 4).

Ещё два случая: остановка по H вместо Z и тройка ---, на которой машина останавливается без шага. А для скорости разберите запись один раз, до цикла: превратите её в список правил по номеру состояния и символу, где состояние — число, а сдвиг — +1 или −1.

Лента-словарь работает, но для двух миллионов шагов список быстрее: заведите список нулей с запасом и головку посередине, а если она подошла к краю — расширьте список. Единицы можно считать на ходу, а можно пересчитать в конце одной строкой — как удобнее.

Разбор записи вынесен из цикла, состояния стали числами, лента — списком, единицы считаются на ходу: ones += write - tape[head] прибавляет единицу, когда 0 меняется на 1, и отнимает, когда наоборот. Два миллиона шагов чемпиона так проходят в песочнице курса за доли секунды, все 47 миллионов — за несколько секунд; гонка в главе считает то же в браузере. И ещё одно: в отличие от Оракула, bb_run имеет право ответить «не знаю» (None), и только поэтому она существует. Функция, которая вместо None говорила бы «никогда не остановится», вычисляла бы $BB$.

Куда дальше

Граница проведена: есть задачи, которые не решит никакая машина. Но с другой стороны границы не всё так безоблачно. Возьмите формулу из ста логических переменных, соединённых «и», «или», «не», и спросите, можно ли подобрать значения так, чтобы она стала истинной. Эта задача разрешима — проще некуда: переберите все наборы значений, их конечное число. Только этих наборов $2^{100}$, около $1{,}3 \cdot 10^{30}$, и компьютер, проверяющий миллиард наборов в секунду, переберёт их примерно за три тысячи возрастов Вселенной.

Разрешимо — ещё не значит решаемо: есть задачи, которые ждут дольше жизни Вселенной. Можно ли решить такую задачу быстро, не перебирая всё? Проверить готовый ответ легко — подставить значения в формулу. А найти? В 1956 году Курт Гёдель написал об этом письмо Джону фон Нейману, который когда-то в Кёнигсберге отвёл его в сторону после доклада. Фон Нейман тогда уже умирал, а вопрос из письма открыт до сих пор. О нём — следующая глава.