Как избежать использования assert и retractall в Prolog для реализации глобальных (или состояний) переменных

Я часто заканчиваю тем, что пишу код на Прологе, который включает в себя некоторое арифметическое вычисление (или информацию о состоянии, важную для всей программы), сначала получая значение, сохраненное в предикате, затем пересчитывая значение и, наконец, сохраняя значение, используя retractall а также assert потому что в Прологе мы не можем присвоить значения переменной дважды, используя is (таким образом, делая почти каждую переменную, которая нуждается в модификации, глобальной). Я узнал, что это не очень хорошая практика в Прологе. В связи с этим я хотел бы спросить:

  1. Почему это плохая практика в Прологе (хотя мне самому не нравится проходить вышеупомянутые шаги, чтобы иметь какую-то гибкую (модифицируемую) переменную)?

  2. Каковы общие способы избежать этой практики? Маленькие примеры будут с благодарностью.

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".

Другие вопросы по тегам