Есть ли в Prolog условие и система перезапуска, как в Common Lisp?

Common Lisp позволяет обрабатывать исключения через условия и перезапуски. Грубо говоря, когда функция выдает исключение, «ловец» может решить, как/должен ли действовать «выбрасыватель». Предлагает ли Prolog подобную систему? Если нет, можно ли построить его поверх существующих предикатов для обхода и изучения стека вызовов?


person mndrix    schedule 27.09.2012    source источник


Ответы (4)


Стандарт ISO/IEC Пролога предоставляет лишь очень рудиментарный механизм обработки исключений и ошибок, который - более или менее - сопоставимо с тем, что предлагает Java, и далеко от богатого механизма Common Lisp, но все же есть некоторые моменты, на которые стоит обратить внимание. В частности, помимо фактического механизма сигнализации и обработки многие системы предоставляют механизм, аналогичный unwind-protect. То есть способ гарантировать, что цель будет выполнена, даже при наличии необработанных сигналов.

ISO бросок/1, ловля/3

Исключение вызывается/генерируется с помощью throw(Term). Сначала создается копия Term с помощью copy_term/2, назовем ее Termcopy, а затем эта новая копия используется для поиска соответствующего catch(Goal, Pattern, Handler), второй аргумент которого унифицируется с Termcopy. При выполнении Handler все объединения, вызванные Goal, отменяются. Таким образом, Handler не может получить доступ к заменам, присутствующим при выполнении throw/1. И нет возможности продолжить на том месте, где было казнено throw/1.

Об ошибках встроенных предикатов сигнализирует выполнение throw(error(Error_term, Imp_def)), где Error_term соответствует одному из классы ошибок ISO и Imp_def могут предоставлять дополнительную информацию, определяемую реализацией (например, исходный файл, номер строки и т. д.).

Есть много случаев, когда локальная обработка ошибки была бы очень полезной, но многие разработчики считают ее слишком сложной для реализации.

Дополнительные усилия, направленные на то, чтобы заставить процессор Prolog обрабатывать каждую ошибку локально, довольно значительны и намного больше, чем в Common Lisp или других языках программирования. Это связано с самой природой унификации в Прологе. Локальная обработка ошибки потребует отмены унификации, выполненной во время выполнения встроенной функции: Таким образом, у разработчика есть две возможности реализовать это:

  • создать «точку выбора» во время вызова встроенного предиката, это повлечет за собой много дополнительных накладных расходов как для создания этой точки выбора, так и для «отслеживания» последующих привязок.
  • пройти через каждый встроенный предикат вручную и решить в каждом конкретном случае, как обрабатывать ошибки, хотя это наиболее эффективно с точки зрения накладных расходов во время выполнения, это также самый дорогостоящий и подверженный ошибкам подход

Аналогичные сложности возникают при использовании регистров WAM во встроенных модулях. Опять же, у вас есть выбор между медленной системой или системой со значительными затратами на реализацию.

исключение_обработчик/3

Многие системы, однако, обеспечивают более совершенные внутренние механизмы, но немногие предлагают их постоянно программисту. IF/Prolog предоставляет exception_handler/3, который имеет те же аргументы, что и catch/3, но обрабатывает ошибку или исключение локально:

[user] ?- catch((arg(a,f(1),_); Z=ok), error(type_error(_,_),_), fail).

no

[user] ?- exception_handler((arg(a,f(1),_); Z=ok), error(type_error(_,_),_), fail).

Z       = ok

yes

setup_call_cleanup/3

Это встроенное, предлагаемое довольно многими системами. Он очень похож на unwind-protect, но требует дополнительной сложности из-за механизма возврата в Прологе. См. его текущее определение.


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

person false    schedule 28.09.2012
comment
exception_handler/3 звучит очень полезно. Я не понимаю, как setup_call_cleanup/3 может реализовать условия и перезапустить. Можете ли вы разъяснить это? - person mndrix; 29.09.2012
comment
setup_call_cleanup/3 играет (более или менее) ту же роль, что и unwind-protect в Common Lisp: это часть системы для обработки ошибок, поэтому ее необходимо упомянуть в этом контексте. - person false; 01.10.2012

Пролог ISO определяет эти предикаты:

  • throw/1, который выдает исключение. Аргумент - это исключение, которое должно быть выброшено (любой термин)
  • catch/3, который выполняет цель и перехватывает определенные исключения, и в этом случае он выполняет обработчик исключений. Первый аргумент — это вызываемая цель, второй аргумент — шаблон исключения (если исключение, созданное throw/1, объединяется с этим шаблоном, выполняется цель обработчика), а третий аргумент — выполняется цель обработчика.

Пример использования:

test:-
  catch(my_goal, my_exception(Args), (write(exception(Args)), nl)).

my_goal:-
  throw(my_exception(test)).

Относительно вашего примечания "Если нет, то можно ли построить его поверх существующих предикатов для обхода и изучения стека вызовов?" я не думаю, что существует общий способ сделать это. Возможно, посмотрите документацию используемой вами системы пролога, чтобы узнать, есть ли какой-нибудь способ пройтись по стеку.

person gusbro    schedule 27.09.2012

Как ложно упомянуто в его ответе, ISO Prolog не позволяет этого. Однако некоторые эксперименты показывают, что SWI-Prolog предоставляет механизм, на основе которого можно создавать условия и перезапуски. Далее следует очень грубое доказательство концепции.

«Ловец» вызывает restart/2 для вызова цели и предоставляет предикат для выбора среди доступных перезапусков в случае возникновения условия. «Метатель» вызывает signal_condition/2. Первый аргумент — это условие повышения. Второй аргумент будет привязан к выбранному перезапуску. Если перезапуск не выбран, условие становится исключением.

restart(Goal, _) :-  % signal condition finds this predicate in the call stack
    call(Goal).

signal_condition(Condition, Restart) :-
    prolog_current_frame(Frame),
    prolog_frame_attribute(Frame, parent, Parent),
    signal_handler(Parent, Condition, Restart).

signal_handler(Frame, Condition, Restart) :-
    ( prolog_frame_attribute(Frame, goal, restart(_, Handler)),
        call(Handler, Condition, Restart)
    -> true
    ; prolog_frame_attribute(Frame, parent, Parent)
    -> signal_handler(Parent, Condition, Restart)
    ; throw(Condition)  % reached top of call stack
    ).
person mndrix    schedule 28.09.2012
comment
Я предполагаю, что call(Handler, Condition, Restart) вызовет исключение создания экземпляра, если вы действительно выберете reboot/2, как показано выше. Итак, для подчеркивания нужно указать 2-арное замыкание, верно? - person Mostowski Collapse; 29.09.2012
comment
Приведенный выше код работает, не вызывая исключения при создании экземпляра. По крайней мере, так было во всех моих тестах. Несмотря на то, что в restart/2 второй аргумент не используется, он создается в стеке вызовов. - person mndrix; 29.09.2012
comment
Хорошо понял. Вы вызываете перезапуск(‹какая-то цель›,‹какой-то хендлер›) в приложении, и определение перезагрузки/2 остается фиксированным. (То же самое в решении (-:)/2). - person Mostowski Collapse; 29.09.2012

Вы можете использовать гипотетические рассуждения, чтобы реализовать то, что вы хотите. Допустим, система Пролога, допускающая гипотетические рассуждения, поддерживает следующее правило вывода:

G, A |- B
----------- (Right ->)
G |- A -> B

Некоторые системы Prolog поддерживают это, например лямбда-пролог. Теперь вы можете использовать гипотетические рассуждения для реализации, например, restart/2 и signal_condition/3. Предположим, что гипотетические рассуждения выполняются через (-:)/2, тогда мы могли бы иметь:

restart(Goal,Handler) :- 
   (handler(Handler) -: Goal).

signal_condition(Condition, Restart) :- 
   handler(Handler), call(Handler,Condition,Restart), !.
signal_condition(Condition, _) :- 
   throw(Condition).

Решение не будет напрасно проходить всю трассировку стека, а будет напрямую запрашивать обработчик. Но возникает вопрос, нужен ли мне специальный Пролог или я могу сам проводить гипотетические рассуждения. В первом приближении (-:)/2 можно реализовать следующим образом:

(Clause -: Goal) :- assume(Clause), Goal, retire(Clause).

assume(Clause) :- asserta(Clause).
assume(Clause) :- once(retact(Clause)).

retire(Clause) :- once(retract(Clause)).
retire(Clause) :- asserta(Clause).

Но вышеописанное не будет работать корректно, если Goal выдаст вырез или исключение. Поэтому лучшее решение доступно, например, в Jekejeke Minlog 0,6 будет:

(Clause -: Goal) :- compile(Clause, Ref), assume_ref(Ref), Goal, retire_ref(Ref).

assume_ref(Ref) :- sys_atomic((recorda(Ref), sys_unbind(erase(Ref)))).

retire_ref(Ref) :- sys_atomic((erase(Ref), sys_unbind(recorda(Ref)))).

Предикат sys_unbind/1 назначает цель отмены в списке привязок. Это соответствует undo/1 от SICStus. Список привязки устойчив к сокращениям. sys_atomic/1 гарантирует, что цель отмены всегда запланирована, даже если во время выполнения происходит внешний сигнал, такой как, например, прерывание, выданное конечным пользователем. Это соответствует тому, как, например, обрабатывается первый аргумент setup_call_cleanup/3.

Преимущество использования здесь ссылок на предложения состоит в том, что предложение компилируется только один раз, даже если возврат происходит между целью и продолжением после (-:)/2. Но в противном случае решение, скорее всего, будет медленнее, чем поставить цель на трассировку стека, вызвав ее. Но можно представить себе дальнейшие усовершенствования системы Пролога, например (-:)/2 в качестве примитивных и подходящих методов компиляции.

до свидания

person Mostowski Collapse    schedule 29.09.2012