Как избежать использования assert и retractall в Prolog для реализации глобальных (или состояний) переменных
Я часто заканчиваю тем, что пишу код на Прологе, который включает в себя некоторое арифметическое вычисление (или информацию о состоянии, важную для всей программы), сначала получая значение, сохраненное в предикате, затем пересчитывая значение и, наконец, сохраняя значение, используя retractall
а также assert
потому что в Прологе мы не можем присвоить значения переменной дважды, используя is
(таким образом, делая почти каждую переменную, которая нуждается в модификации, глобальной). Я узнал, что это не очень хорошая практика в Прологе. В связи с этим я хотел бы спросить:
Почему это плохая практика в Прологе (хотя мне самому не нравится проходить вышеупомянутые шаги, чтобы иметь какую-то гибкую (модифицируемую) переменную)?
Каковы общие способы избежать этой практики? Маленькие примеры будут с благодарностью.
PS Я только начал изучать пролог. У меня есть опыт программирования на таких языках, как C.
Отредактировано для дальнейшего уточнения
Плохой пример (в win-прологе) того, что я хочу сказать, приведен ниже:
:- dynamic(value/1).
:- assert(value(0)).
adds :-
value(X),
NewX is X + 4,
retractall(value(_)),
assert(value(NewX)).
mults :-
value(Y),
NewY is Y * 2,
retractall(value(_)),
assert(value(NewY)).
start :-
retractall(value(_)),
assert(value(3)),
adds,
mults,
value(Q),
write(Q).
Тогда мы можем запросить как:
?- start.
Здесь это очень тривиально, но в реальной программе и приложении показанный выше метод глобальной переменной становится неизбежным. Иногда приведенный выше список вроде assert(value(0))
... растет очень долго с большим количеством предикатов assert для определения большего количества переменных. Это сделано для того, чтобы сделать возможной передачу значений между различными функциями и сохранить состояния переменных во время выполнения программы.
Наконец, я хотел бы знать еще одну вещь: когда упомянутая выше практика становится неизбежной, несмотря на различные решения, предложенные вами, чтобы ее избежать?
2 ответа
Общий способ избежать этого - думать с точки зрения отношений между состояниями ваших вычислений: вы используете один аргумент, чтобы сохранить состояние, относящееся к вашей программе, перед вычислением, и второй аргумент, который описывает состояние после некоторого вычисления. Например, чтобы описать последовательность арифметических операций над значением V0
, ты можешь использовать:
state0_state(V0, V) :-
operation1_result(V0, V1),
operation2_result(V1, V2),
operation3_result(V2, V).
Обратите внимание, как состояние (в вашем случае: арифметическое значение) пронизывается через предикаты. Соглашение об именах V0
-> V1
->... -> V
легко масштабируется на любое количество операций и помогает помнить, что V0
является начальным значением, и V
это значение после применения различных операций. Каждый предикат, которому необходимо получить доступ или изменить состояние, будет иметь аргумент, позволяющий передать ему состояние.
Огромное преимущество потоковой обработки состояния таким образом заключается в том, что вы можете легко рассуждать о каждой операции изолированно: вы можете протестировать ее, отладить ее, проанализировать с помощью других инструментов и т. Д. Без необходимости устанавливать какое-либо неявное глобальное состояние. В качестве еще одного огромного преимущества вы можете использовать свои программы в нескольких направлениях, если вы используете достаточно общие предикаты. Например, вы можете спросить: какие начальные значения приводят к данному результату?
?- state0_state(V0, given_outcome).
Это, конечно, не всегда возможно при использовании императивного стиля. Поэтому вы должны использовать ограничения вместо is/2
, так как is/2
работает только в одном направлении. Ограничения намного проще в использовании и являются более общей современной альтернативой низкоуровневой арифметике.
Динамическая база данных также медленнее, чем состояние потоков в переменных, потому что она выполняет индексацию и т. Д. Для каждой assertz/1
,
1 - это плохая практика, потому что разрушает декларативную модель, которую демонстрируют (чистые) программы Prolog.
Тогда программист должен думать в процедурном плане, а процедурная модель Пролога довольно сложна и трудна для подражания.
В частности, мы должны быть в состоянии принять решение о достоверности утвержденных знаний, в то время как программы возвращаются назад, то есть следуют альтернативным путям к тем, которые уже были опробованы, что (возможно) вызвало утверждения.
2 - нам нужны дополнительные переменные, чтобы сохранить состояние. Практический, может быть, не очень интуитивный способ - использовать грамматические правила (DCG) вместо простых предикатов. Правила грамматики переводятся с добавлением двух аргументов списка, обычно скрытых, и мы можем использовать эти аргументы для неявной передачи состояния и ссылаться на него / изменять его только при необходимости.
Здесь действительно интересное введение: DCG в Прологе Маркуса Триски. Ищу Implicitly passing states around
Вы найдете этот небольшой пример:
num_leaves(nil), [N1] --> [N0], { N1 is N0 + 1 }.
num_leaves(node(_,Left,Right)) -->
num_leaves(Left),
num_leaves(Right).
В более общем плане и для дальнейших практических примеров см. Мышление в состояниях того же автора.
edit: как правило, assert/retract требуются только в том случае, если вам нужно изменить базу данных или отслеживать результаты вычислений при обратном отслеживании. Простой пример от моего (очень) старого переводчика Пролога:
findall_p(X,G,_):-
asserta(found('$mark')),
call(G),
asserta(found(X)),
fail.
findall_p(_,_,N) :-
collect_found([],N),
!.
collect_found(S,L) :-
getnext(X),
!,
collect_found([X|S],L).
collect_found(L,L).
getnext(X) :-
retract(found(X)),
!,
X \= '$mark'.
findall/3 можно рассматривать как основной предикат всех решений. Этот код должен быть тем же самым из Clockins-Mellish учебника - Программирование на Прологе. Я использовал его при тестировании "настоящего" findall/3, который я реализовал. Вы можете видеть, что это не "реентерабельный" из-за псевдонима "$mark".