Динамика базы знаний: 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— все.- Применяют для накопления состояния и мемоизации.
- Это побочные эффекты: они не откатываются бэктрекингом и нарушают декларативность — отсюда «грязно».