Отрицание как неудача (\+)

Урок про оператор \+ — способ сказать «это недоказуемо» и про подводные камни такого «отрицания».

Отрицание как неудача (\+ Goal) — цель, которая успешна тогда и только тогда, когда Goal не удаётся доказать из текущей базы знаний.

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

Как пишется и работает

Оператор \+ ставится перед целью. Если Goal доказывается — \+ Goal проваливается. Если Goal доказать не удалось — \+ Goal успешно.

likes(mary, wine).
likes(mary, books).

?- \+ likes(mary, beer).
true.

?- \+ likes(mary, wine).
false.

Вывод:

true.

Про beer в базе ничего нет, доказать likes(mary, beer) нельзя — значит \+ успешно. А likes(mary, wine) — факт, он доказывается, поэтому \+ likes(mary, wine) проваливается.

Замкнутость мира

За этим стоит предположение о замкнутости мира (closed-world assumption): всё, что не выводится из базы, считается ложным. Prolog исходит из того, что база знаний полна — если факта нет, его как бы не существует.

В реальности это не всегда так: отсутствие записи может означать «неизвестно», а не «ложно». Из-за этого \+ уместен там, где база действительно полна (например, список зарегистрированных пользователей), и опасен там, где данные неполны.

Отличие от логического отрицания

Логическое «не P» утверждает, что P объективно ложно. \+ P утверждает лишь, что P не доказуемо здесь и сейчас. Разница хорошо видна на динамике: добавьте факт — и тот же запрос \+ поменяет ответ, хотя «истина» во внешнем мире не менялась.

СвойствоЛогическое ¬P\+ P в Prolog
СмыслP ложноP не доказуемо из базы
Зависит от базынетда
Связывает переменныеникогда не оставляет связок

Ловушка: \+ с переменными

Самая коварная ошибка — отрицание цели с несвязанными переменными. \+ никогда не возвращает связок переменных: даже когда внутренняя цель находит решение, после отката все связки теряются. Поэтому смысл «не существует такого X» работает корректно, а вот «найди X, которого нет» — нет.

student(anna).
student(boris).

?- \+ student(X).
false.

?- \+ student(carol).
true.

Вывод:

false.

Запрос \+ student(X) читается как «не существует студента» — а студенты есть, поэтому false. Если вы хотели «X не является студентом», нужно сперва связать X конкретным значением, и только потом отрицать: X = carol, \+ student(X). Правило: к моменту \+ переменные внутри должны быть означены, иначе результат почти наверняка не тот, что вы ждёте.

Как работает под капотом

Классически \+ Goal определяется через связку cut-fail:

neg(Goal) :- call(Goal), !, fail.
neg(_).

Логика такая: пробуем доказать Goal через call. Если получилось — срабатывает ! (отсекает второе предложение) и затем fail заставляет весь neg провалиться. Если же Goal не доказался, call провалился до cut, и тогда сработает второе предложение neg(_), давая успех.

Goal доказан?
   ├─ да  -> ! , fail  ->  neg ПРОВАЛ
   └─ нет -> второе правило -> neg УСПЕХ

Именно поэтому \+ не оставляет связок: любые подстановки, найденные внутри call(Goal), откатываются вместе с fail. Понимание этой схемы объясняет и ловушку с переменными, и связь отрицания с отсечением.

Частые ошибки

  • Отрицание со свободными переменными. \+ p(X) при несвязанном X почти всегда даёт не тот смысл. Сначала свяжите переменные.
  • Ожидать связок от \+. Отрицание не возвращает значений — оно только успех или неудача.
  • Применять \+ к неполной базе. «Не доказано» не равно «ложно»; на неполных данных это даёт ложноположительные ответы.
  • Путать с \=. \= — это «термы не унифицируются», узкий частный случай; \+ — общее отрицание любой цели.

Итог

  • \+ Goal успешно тогда, когда Goal недоказуемо из базы.
  • В основе — предположение о замкнутости мира: чего нет в базе, то считается ложным.
  • Это «не доказуемо», а не логическое «ложно» — ответ зависит от содержимого базы.
  • \+ требует означенных переменных и никогда не возвращает связок.
  • Классически реализуется через cut-fail: call(Goal), !, fail. плюс запасное правило.
Проверьте себя
1. Когда цель \+ Goal завершается успешно?
AКогда Goal логически ложно во внешнем мире
BКогда Goal не удаётся доказать из текущей базы знаний
CКогда Goal содержит ошибку синтаксиса
DКогда Goal возвращает связки переменных
2. Почему запрос ?- \+ student(X). при фактах student(anna), student(boris) даёт false?
AПотому что X не объявлен как dynamic
BПотому что он читается как «не существует студента», а студенты есть
CПотому что \+ нельзя применять к фактам
DПотому что X связывается с anna
3. Через какую конструкцию классически выражают \+ Goal?
Aassert(Goal), retract(Goal)
Bcall(Goal), !, fail. и запасное правило
CGoal is true
DGoal =:= fail