Динамика базы знаний: assert и retract

Урок про то, как Prolog-программа меняет саму себя во время работы: добавляет и убирает факты в базе знаний.

assert и retract — встроенные предикаты, которые во время исполнения добавляют и удаляют предложения (факты или правила) из базы знаний.

До сих пор база знаний была неизменной: вы загрузили факты и правила, и они оставались такими весь запрос. Но иногда программе нужно помнить что-то между вызовами — счётчик, накопленный результат, заметку «это уже считал». Для этого Prolog умеет править собственную базу на лету.

Объявление dynamic

Предикат, который будет меняться во время работы, нужно заранее пометить директивой :- dynamic. Иначе многие системы выдадут ошибку при попытке изменить «статический» предикат или при обращении к ещё не существующему.

:- dynamic(score/1).

?- assertz(score(0)).
?- score(X).
X = 0.

Вывод:

X = 0.

Запись score/1 означает «предикат score с одним аргументом». После объявления его можно безопасно наполнять и опустошать.

Добавление: assert, asserta, assertz

Семейство assert добавляет предложение в базу. Разница — в том, куда именно.

ПредикатКуда добавляет
asserta(C)в начало (станет первым предложением)
assertz(C)в конец (станет последним)
assert(C)обычно синоним assertz

Порядок важен, потому что он определяет, в каком порядке Prolog перебирает предложения. asserta пригодится, например, для кэша, где свежие записи должны находиться первыми.

:- dynamic(fact/1).

?- assertz(fact(a)), assertz(fact(b)), asserta(fact(z)).
?- fact(X).
X = z ;
X = a ;
X = b.

Вывод:

X = z ;
X = a ;
X = b.

Удаление: retract и retractall

retract(C) убирает первое предложение, унифицируемое с C. retractall(C) удаляет все совпадающие. Это позволяет «обновлять» факт: удалить старый и записать новый.

:- dynamic(counter/1).

increment :-
    retract(counter(N)),
    N1 is N + 1,
    assertz(counter(N1)).

?- assertz(counter(0)), increment, increment, counter(X).
X = 2.

Вывод:

X = 2.

Каждый вызов increment снимает текущее значение, увеличивает его и записывает обратно. Так получается изменяемое состояние — то, чего в «чистом» Prolog нет.

Типичные применения

Накопление состояния

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

Мемоизация

Дорогой расчёт можно посчитать один раз и записать результат как факт, чтобы при повторном запросе сразу его вернуть.

:- dynamic(known_fib/2).

fib(N, F) :- known_fib(N, F), !.
fib(N, F) :-
    N > 1,
    N1 is N - 1, N2 is N - 2,
    fib(N1, F1), fib(N2, F2),
    F is F1 + F2,
    assertz(known_fib(N, F)).
fib(0, 0).
fib(1, 1).

Здесь known_fib хранит уже вычисленные числа Фибоначчи; первое правило с ! сразу отдаёт готовый ответ, превращая экспоненциальный перебор в линейный.

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

В Prolog программа и данные — одно и то же: и факты, и правила хранятся в общей базе предложений. assert добавляет в эту базу новое дерево-терм, retract ищет первое унифицируемое и удаляет его. То есть программа модифицирует ту же структуру, по которой идёт доказательство.

База предложений:
[ counter(0) ]
   |  retract(counter(N))  -> N=0, факт удалён
   v
[ ]
   |  assertz(counter(1))
   v
[ counter(1) ]

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

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

  • Забыть :- dynamic. Обращение к необъявленному динамическому предикату или его правка часто даёт ошибку.
  • Рассчитывать на откат. assert и retract не отменяются бэктрекингом — база остаётся изменённой. Это частый источник «загадочного» состояния.
  • Дубли вместо обновления. Если хотите заменить факт, сначала retract/retractall, иначе накопятся несколько counter/1.
  • Злоупотребление. Динамика делает код императивным и трудным для отладки — её стоит применять точечно, а не как замену рекурсии.

Почему это «грязно»

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

Итог

  • Изменяемые предикаты объявляют через :- dynamic(имя/арность).
  • asserta добавляет в начало, assertz (и обычно assert) — в конец базы.
  • retract убирает первое совпадение, retractall — все.
  • Применяют для накопления состояния и мемоизации.
  • Это побочные эффекты: они не откатываются бэктрекингом и нарушают декларативность — отсюда «грязно».
Проверьте себя
1. Чем asserta отличается от assertz?
Aasserta добавляет предложение в начало базы, assertz — в конец
Basserta добавляет факт, assertz — правило
Casserta удаляет, assertz добавляет
DМежду ними нет разницы
2. Что происходит с фактом, добавленным через assertz, если ветка вычисления затем проваливается при бэктрекинге?
AФакт автоматически удаляется вместе с откатом
BФакт остаётся в базе — assert не откатывается бэктрекингом
CВозникает ошибка instantiation error
DФакт превращается в динамический
3. Почему использование assert/retract считается «грязным» в Prolog?
AОни работают медленнее обычных предикатов
BОни вносят побочные эффекты и нарушают декларативность: результат зависит от истории вызовов
CОни требуют оператора is
DОни не поддерживаются современными системами