Егор Урванов

Теорема Райса простыми словамиПочему нельзя проверить любую программу и что с этим делать

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

Теорема Райса простыми словами: нет программы, которая для любой программы точно скажет, верно ли что-то о её поведении, например «никогда не списывает деньги без заказа». О тексте кода ответить можно, о поведении любой программы — нет. Поэтому проверку сужают рамкой, внутри которой ответ точный. Ниже: предикат, доказательство, откуда приём, три рамки и связь с теоремой CAP.

Суть: что запрещает теорема Райса

Теорема Райса говорит, что все нетривиальные смысловые свойства программ неразрешимы. Доказал её Генри Гордон Райс в диссертации 1951 года. В трёх словах этой формулировки вся теорема.

правило о поведении: «никогда не списывает деньги без заказа»проверкалюбая, самая умная✓×✓×✓?программывсегда найдётся программа,где ответ неверено тексте кода ответить можно, о поведении любой программы — нет; выход — сузить рамку● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, границаДля любой проверки поведения найдётся программа, на которой она ошибётся

Какой бы умной ни была проверка, найдётся программа, на которой она ответит неверно.

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

Как это понимать на бытовом уровне. Представьте проверяющего, которому приносят любую программу и спрашивают: «Спишет ли она когда-нибудь деньги без заказа?» Проверяющий может быть сколько угодно умным, работать сколько угодно долго и знать все языки программирования. Теорема говорит, что всё равно найдётся программа, на которой он ответит неверно или не ответит вовсе. Это свойство самой задачи, и улучшение проверяющего его не снимает, как улучшение конструкции не даёт вечного двигателя.

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

Что меняемЧто получаемПример
одну конкретную программу вместо любойеё свойство можно доказать вручную или машинойдоказательство корректности алгоритма сортировки
разрешаем ответ «не знаю»проверка честно отказывается на трудных случаяханализатор кода, который помечает сомнительное место
вопрос о тексте вместо поведенияответ получают чтением кода«нет ли в коде прямых запросов к чужой таблице»
малый мир вместо бесконечногоперебор всех состояний до заданных чиселмодель на Quint для 3 комнат и 5 броней
Вывод
В этом примереАгент принёс 2000 строк кода оплаты. Ни линтер, ни ревью, ни другая модель не могут гарантировать для произвольного кода, что он никогда не спишет деньги без заказа.
В общем случаеЭто ограничение математическое. Улучшение проверки его не снимает, пока она должна работать для любой программы.
Что сделатьДля каждого правила, которое хотите проверять, сначала спросите: оно о тексте кода или о поведении?

Предикат: вопрос о записи или вопрос о поведении

Предикат — вопрос о программе с ответом «да» или «нет». Таких вопросов два вида, и теорема Райса касается только второго.

кодвопросчто нужно просмотретьответкак написано40 строкесть ли в коде записьв таблицу платежей?строки: 1, 2 … 40 — конецконечно: прочитать вседа или нетчто получитсятот же кодспишет ли деньги без заказахоть в одном запуске?вход 1вход 2вход 3вход 4… ∞запусков бесконечно, часть не кончается?не решаетсяесли у одних программ «да»,а у других «нет»● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, границаКак написано: прочитать конечный список строк. Что получится: перебрать все запуски, а их бесконечно

Код один и тот же. Разница в том, на что нужно посмотреть, чтобы ответить: на конечный текст или на бесконечное число запусков.

Как написано
Что получится
Вопрос
есть ли в коде запись в таблицу платежей?
спишет ли программа деньги без заказа хоть в одном запуске?
Что просмотреть
строки кода; их конечно, можно прочитать все
все запуски со всеми входами; их бесконечно, часть не кончается
Ответ
всегда есть
в общем случае нет

Почему слово «смысловое» здесь ключевое. Смысловое свойство зависит только от того, что программа делает, и не зависит от того, как она написана. Если две программы на всех входах ведут себя одинаково, у них одинаковый ответ на смысловой вопрос. Пример: одна программа считает сумму покупок циклом, другая — готовой функцией. Тексты разные, поведение одно, и на вопрос «бывает ли сумма отрицательной» у них общий ответ. А на вопрос «есть ли в коде цикл» ответы разные, потому что это вопрос о записи.

ВопросО чёмРешается?
есть ли в функции больше 50 строко текстеда, подсчётом
вызывает ли код функцию deleteо текстеда, поиском
пишет ли в таблицу заказов только один модульо текстеда, поиском по коду
закончится ли программа на любом входео поведениинет, в общем случае
вернёт ли функция когда-нибудь отрицательную суммуо поведениинет, в общем случае
спишет ли обработчик деньги без заказао поведениинет, в общем случае

Близкая аналогия — императивный и декларативный подходы. Код записывает шаги, то есть «как», а правило о результате — утверждение, то есть «что». Теорема Райса говорит, что из шагов в общем случае нельзя механически вывести, верно ли утверждение о результате. Оговорка: на мощном декларативном языке поведение тоже не проверить по тексту. Точный ответ дают бедные записи вроде ограничения в базе: в них поведение видно из самой записи.

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

Доказательство на одном примере

почему: такая проверка решила бы проблему остановкиP(w)любая программаP(w)B: нарушитьQ: сначала P(w), потом Bпроверка правила«Q нарушит»= «P(w) остановится»а это не решается● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, границаПроверка правила для Q ответила бы, остановится ли P(w), а это не решается

Проверка правила для QQ ответила бы, остановится ли P(w)P(w), а на этот вопрос не отвечает ни одна программа.

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

Возьмём любую программу PP со входом ww и соберём из неё новую программу QQ:

Q(x)  =  запусти P(w); когда она закончится, выполни B(x)Q(x) \;=\; \text{запусти } P(w)\text{; когда она закончится, выполни } B(x)

Шаг B(x)B(x) правило нарушает, а программа, которая работает вечно, его не нарушает. Значит, QQ доходит до нарушения, только если P(w)P(w) когда-нибудь закончится:

Q нарушает правило  ⟺  P(w) останавливаетсяQ \text{ нарушает правило} \iff P(w) \text{ останавливается}

Если бы существовала проверка правила для любой программы, она, получив QQ, ответила бы и на вопрос, остановится ли P(w)P(w). Но Алан Тьюринг доказал, что на этот вопрос, проблему остановки, не отвечает ни одна программа. Значит, и универсальной проверки правила нет. Правило здесь любое: вместо «деньги без заказа» можно подставить любое нетривиальное свойство поведения.

Те же шаги по порядку:

  1. Берём любое нетривиальное правило о поведении. Раз оно нетривиальное, есть программа, которая его нарушает. Её часть, которая нарушает правило, и есть шаг BB.
  2. Берём любую программу PP и её вход ww, про которые хотим узнать, остановится ли P(w)P(w).
  3. Склеиваем QQ: сначала P(w)P(w), потом BB. Склейку делает простая механическая процедура, её можно написать за минуту.
  4. Отдаём QQ воображаемой универсальной проверке. Её ответ «нарушает» значит «P(w)P(w) остановилась», ответ «не нарушает» значит «не остановилась».
  5. Получилось решение проблемы остановки, которого не бывает. Значит, не бывает и универсальной проверки.

Одна оговорка для точности. В построении считается, что программа, которая работает вечно и ничего не делает, правило не нарушает. Если для какого-то правила это не так, берут обратное правило: его проверка ничем не легче, и рассуждение повторяется слово в слово.

Вывод
В этом примереАгент написал обработчик оплаты: сначала он вызывает расчёт скидки из чужой библиотеки, а потом списывает деньги, не проверив, что заказ создан. Нарушит ли обработчик правило, зависит только от того, закончится ли расчёт скидки: если он зависнет навсегда, до списания дело не дойдёт.
В общем случаеЧтобы ответить точно по коду, проверке пришлось бы решить, остановится ли чужой расчёт. Это та же схема: P(w)P(w) — расчёт скидки, BB — списание без заказа.
Что сделатьЗакрывайте такое правило рамкой: ограничение в базе не даёт записать списание без заказа, что бы ни делал код до него.

Откуда приём: проблема остановки и парадокс брадобрея

Теорема Райса сводит любую проверку поведения к проблеме остановки и этим наследует её неразрешимость. А проблема остановки доказывается приёмом, которому больше ста лет: вопросом о самом себе.

спорщик«судья, а я остановлюсь?»судья«остановится»«не остановится»спорщик крутится вечносудья ошибсяспорщик сразу останавливаетсясудья ошибсякак в парадоксе брадобрея: вопрос о самом себе● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, границаСпорщик спрашивает судью о себе и поступает наоборот: судья ошибается при любом ответе

Спорщик спрашивает судью о себе и поступает наоборот, поэтому судья ошибается при любом ответе.

Допустим, есть судья, который про любую программу говорит, остановится ли она. Напишем программу-спорщика: она спрашивает судью о самой себе и поступает наоборот. Судья сказал «остановится» — спорщик крутится вечно; сказал «не остановится» — спорщик сразу останавливается. Судья ошибается в любом случае, значит, его не бывает.

AA = бреют себя самибрадобрей их не бреетBB = их бреет брадобрейправило: B — все, кто не в Aбрадобрей — тоже житель?в A → бреет себя сам,то есть его бреет брадобрей → он в Bв B → его бреет брадобрей,то есть он сам → он в Aкаждый житель — ровно в одном множестве; брадобрею места нет● модель, люди, системы, данные  ● риск, стоп, границаКаждый житель ровно в одном множестве: A или B. Брадобрей — ни в одном: встанет в A, окажется в B, и наоборот

Брадобрей бреет всех, кто не бреет себя сам. Сам он не попадает ни в одно из двух множеств.

Тот же приём в парадоксе брадобрея, наглядном варианте парадокса Рассела 1901 года. Жители делятся на множество AA — те, кто бреет себя сам, и множество BB — те, кого бреет брадобрей, и каждый попадает ровно в одно. Брадобрею места нет: в AA он оказывается в BB, в BB — в AA. У Рассела то же про множества: R=x:x∉xR = {x : x \notin x}, и R∈R  ⟺  R∉RR \in R \iff R \notin R.

ГодКтоЧтоПриём
1901Бертран Расселмножество всех множеств, которые не содержат себявопрос о себе
1936Алан Тьюрингнет судьи, который скажет об остановке любой программыспорщик спрашивает о себе
1951Генри Гордон Райснет проверки любого нетривиального свойства поведениясведение к остановке

Почему вопрос о самом себе так силён. Программа — это тоже текст, а значит, её можно подать на вход другой программе и даже самой себе. Как только проверяющий умеет говорить о любой программе, он обязан уметь говорить и о программе, которая спрашивает его о себе и поступает наоборот. Тогда любой его ответ оказывается неверным. Тот же ход есть в парадоксе брадобрея, в парадоксе Рассела и в теореме Гёделя о неполноте.

Что делать: три рамки, внутри которых ответ точный

выход: сузить проверку рамкоймалый мир3 комнаты, 5 броней: перебрать всёузкий языквопрос о тексте вместо поведения?«не знаю»решает человек или тест● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, границаТри способа сузить проверку так, чтобы ответ был точным

Теорема запрещает проверку для любой программы. Для программ внутри рамки проверка возможна.

Теорема запрещает одно сочетание из трёх условий сразу: проверка должна работать для любой программы, на любой вопрос о поведении отвечать точно «да» или «нет». Доказательство опирается на все три: оно строит программу QQ, которую проверка обязана принять, задаёт про неё вопрос о поведении и требует однозначный ответ. Каждый способ убирает одно условие, и доказательство перестаёт работать.

СпособЧто делают на практикеПочему ответ получается точнымЧто теряют
Малый мирпишут модель бронирования на Quint: 3 комнаты, до 5 броней, действия «забронировать» и «отменить». Программа перебирает все последовательности этих действий и после каждого шага проверяет правило «брони одной комнаты не пересекаются»вариантов конечное число, поэтому перебор доходит до конца и отвечает «правило держится» или показывает цепочку шагов, где оно сломалосьошибку, которая появляется только при 50 одновременных бронях, перебор с 5 бронями не увидит
Узкий языкправило записывают ограничением в базе PostgreSQL, а писать в таблицу броней разрешают только файлу booking/repository.ts. При каждой правке агента поиск проверяет, нет ли записи в таблицу броней в других файлахограничение — одна строка в схеме базы, база сама проверяет его при каждой записи; поиск по файлам всегда заканчивается и даёт точный ответагент не может писать бронь откуда удобно: даже массовый импорт броней придётся провести через тот же разрешённый файл
Ответ «не знаю»проверка правки агента выдаёт один из трёх ответов: «соблюдено», «нарушено», «не могу определить». Третий ответ ставит на правку метку «нужен человек»проверка не обязана решать трудные случаи, например запрос к базе, собранный из строк на лету: на них она честно говорит «не могу определить», а там, где ответила «соблюдено» или «нарушено», ответ точныйразработчик тратит время на правки с меткой «нужен человек»

Каждый из трёх ответов точен только в своих пределах. Перебор доказывает, что правило держится в модели с 3 комнатами и 5 бронями, но не в боевой базе с тысячами броней. Поиск доказывает, что в коде нет записи в обход, но не то, что само ограничение в базе написано верно. Ответ «соблюдено» точен, но часть правок всё равно уходит человеку. Поэтому способы используют вместе: модель проверяет замысел до кода, ограничение и поиск проверяют сам код, а человек смотрит то, что осталось с меткой.

У любой проверки, которая работает внутри рамки, бывают ошибки двух видов. Полезно заранее решить, какую из них можно терпеть.

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

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

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

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

Связь с теоремой CAP: общее ядро, разные причины

Теорема CAP — известный запрет из распределённых систем. Когда данные лежат на нескольких серверах и связь между ними оборвалась, система не может одновременно отвечать на каждый запрос и показывать всем одни и те же данные. Приходится выбирать: ждать восстановления связи или отвечать по своей копии, которая могла устареть. Эрик Брюер высказал это в 2000 году, Сет Гилберт и Нэнси Линч доказали в 2002-м.

клиент«сколько броней?»сервер 1сервер 2здесь свежая бронь×связь оборваласьC: ждать связиданные верны, ответа пока нетA: ответить сейчасответ есть, но без свежей брониждать или угадать —как проверка программы● модель, люди, системы, данные  ● агент и харнесс  ● риск, стоп, границаСвязь между серверами оборвалась: либо ждать и не отвечать, либо отвечать по своей копии, которая может устареть

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

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

Верх схемы — развилка из двух взаимоисключающих выходов. Низ — третий путь: он меняет условие, из-за которого развилка возникла.

Как выглядят две ветки развилки в сети и в проверке программы:

ВыходВ сети (CAP)В проверке программы
ждатьсервер ждёт, пока связь восстановится; данные верны, но ответа пока нет, и он может не прийти долгопроверка ждёт, пока программа закончится, или отвечает «не могу определить»; ошибки нет, но ответа может не быть
ответить сейчассервер отвечает по своей копии; ответ есть, но в нём может не быть свежей брони с другого серверапроверка отвечает по тестам или по прогону в 1000 шагов; ответ есть, но программа могла бы нарушить правило на шаге 1001

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

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

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

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

Частые вопросы

Что такое теорема Райса простыми словами?
Нет программы, которая для любой программы точно скажет, верно ли что-то о её поведении, если это свойство есть у одних программ и нет у других. О тексте кода ответить можно, о поведении любой программы — нет.
Как теорема Райса связана с проблемой остановки?
Любую проверку поведения можно превратить в вопрос, остановится ли программа: собрать программу, которая нарушает правило, только если другая программа закончится. На вопрос об остановке не отвечает ни одна программа, поэтому и проверки поведения нет.
Значит, проверять программы бесполезно?
Невозможна только проверка для любой программы. Внутри рамки — малый мир, узкий язык или ответ «не знаю» — проверка точная, и так работают Quint, ограничения в базе и линтеры.
При чём тут парадокс брадобрея?
Проблема остановки доказывается тем же приёмом, что парадокс брадобрея и парадокс Рассела: вопросом о самом себе. Теорема Райса наследует этот приём через сведение к остановке.
© 2026 Егор Урванов