Какое использование имеет значение if_/3?
Предикат if_/3
кажется довольно популярным среди несколько основных участников в части Prolog.
Этот предикат реализуется как таковой, любезно предоставленный @false:
if_(If_1, Then_0, Else_0) :-
call(If_1, T),
( T == true -> call(Then_0)
; T == false -> call(Else_0)
; nonvar(T) -> throw(error(type_error(boolean,T),_))
; /* var(T) */ throw(error(instantiation_error,_))
).
Однако я не смог найти объяснение четкого, простого и сжатого того, что делает этот предикат, и то, что его использует, по сравнению с, например, классическая конструкция If-then-else Prolog if -> then ; else
.
Большинство ссылок, которые я нашел, напрямую используют этот предикат и не дают объяснений относительно того, почему он используется, что легко понять непрофессионал в Прологе.
Ответы
Ответ 1
В старомодном коде Пролога следующий шаблон возникает довольно часто:
predicate([], ...).
predicate([L|Ls], ...) :-
condition(L),
then(Ls, ...).
predicate([L|Ls], ...) :-
\+ condition(L),
else(Ls, ...).
Я использую списки здесь в качестве примера, где это происходит (см., например, include/3
, exclude/3
и т.д.), хотя шаблон, конечно, также встречается в другом месте.
Трагическое следующее:
- Для созданного экземпляра список совпадений шаблонов может отличить первое предложение от остальных two, но он не может отличить второй от последнего, поскольку оба они имеют
'.'(_, _)
в качестве основного функтор и арность их первого аргумента.
- Условия, в которых применяются последние два предложения, явно взаимоисключающие.
- Таким образом, когда все известно, мы хотим получить эффективный, детерминированный предикат, который не оставляет точек выбора, и в идеале даже не создает точки выбора.
- Однако, пока не все может быть безопасно определено, мы хотим извлечь выгоду из backtracking, чтобы увидеть все решения, поэтому мы не можем позволить себе совершить какое-либо из статьи.
Таким образом, существующие конструкции и языковые функции все никак не позволяют выразить шаблон, который часто встречается на практике. Поэтому на протяжении десятилетий казалось необходимым пойти на компромисс. И вы можете сделать довольно хорошее предположение, в каком направлении "компромиссы" обычно идут в сообществе Prolog: почти всегда правильность приносится в жертву за эффективность в случае сомнений. В конце концов, кто заботится о правильных результатах, пока ваши программы бывают быстрыми, верно? Поэтому до изобретения if_/3
это часто ошибочно записывалось как:
predicate([], ...).
predicate([L|Ls], ...) :-
( condition(L) ->
then(Ls, ...).
; else(Ls, ...).
)
ошибка в этом, конечно, состоит в том, что, когда элементы недостаточно созданы, это может неправильно зафиксировать одну ветвь, даже если обе альтернативы логически возможны. По этой причине использование if-then-else почти всегда декларативно неверно и значительно расширяет возможности декларативных подходов к отладке из-за нарушения самых элементарных свойств, которые мы ожидаем от чистых программ Prolog.
Используя if_/3
, вы можете записать это как:
predicate([], ...).
predicate([L|Ls], ...) :-
if_(condition(L),
then(Ls, ...),
else(Ls, ...)).
и сохранить все желаемые аспекты. Это:
- детерминированный, если все можно безопасно решить.
- эффективный, поскольку он даже не создает точки выбора
- complete, в котором вы никогда не ошибочно выполняете одну конкретную ветвь.
цена этого довольно доступная: как отметил Борис в комментариях, вам нужно реализовать reification. У меня теперь есть некоторый опыт в этом, и я нашел это довольно легко с некоторой практикой.
Хорошие новости. Во многих случаях condition
имеет форму (=)/2
или (#=)/2
, а первая даже поставляется с library(reif)
для бесплатного.
Для получения дополнительной информации см. Индексирование dif/2 Ульриха Ноймеркеля и Стефана & Krab!
Ответ 2
Попробуйте решить простую задачу, используя if_/3
; например, я попытаюсь разбить список (отсортированный по предикату p/2
) в двух списках: префикс, в котором для каждого элемента X
мы имеем p(X, true)
и остаток (в котором, если список был отсортирован по p/2
, мы имели бы p(X, false)
.
Я буду использовать библиотеку reif
как здесь. Итак, вот полный код моей программы:
:- use_module(reif).
pred_prefix(Pred_1, List, L_true, L_false) :-
pred_prefix_aux(List, Pred_1, L_true, L_false).
pred_prefix_aux([], _, [], []).
pred_prefix_aux([X|Xs], Pred_1, True, False) :-
if_( call(Pred_1, X),
( True = [X|True0],
pred_prefix_aux(Xs, Pred_1, True0, False)
),
( True = [],
False = [X|Xs]
)
).
Предикат, переданный этому мета-предикату, примет два аргумента: первый - текущий элемент списка, а второй будет либо true
, либо false
. В идеале этот предикат всегда будет успешным и не оставит за собой точек выбора.
В первом аргументе if_/2
предикат оценивается с помощью текущего элемента списка; второй аргумент - это то, что происходит, когда true
; третий аргумент - это то, что происходит, когда false
.
С этим я могу разбить список в ведущем a
и остальном:
?- pred_prefix([X, B]>>(=(a, X, B)), [a,a,b], T, F).
T = [a, a],
F = [b].
?- pred_prefix([X, B]>>(=(a, X, B)), [b,c,d], T, F).
T = [],
F = [b, c, d].
?- pred_prefix([X, B]>>(=(a, X, B)), [b,a], T, F).
T = [],
F = [b, a].
?- pred_prefix([X, B]>>(=(a, X, B)), List, T, F).
List = T, T = F, F = [] ;
List = T, T = [a],
F = [] ;
List = T, T = [a, a],
F = [] ;
List = T, T = [a, a, a],
F = [] .
Как вы можете избавиться от ведущего 0, например:
?- pred_prefix([X, B]>>(=(0, X, B)), [0,0,1,2,0,3], _, F).
F = [1, 2, 0, 3].
Конечно, это могло быть написано гораздо проще:
drop_leading_zeros([], []).
drop_leading_zeros([X|Xs], Rest) :-
if_(=(0, X), drop_leading_zeros(Xs, Rest), [X|Xs] = Rest).
Здесь я только что удалил все ненужные аргументы.
Если вам нужно будет сделать это без if_/3
, вам пришлось бы написать:
drop_leading_zeros_a([], []).
drop_leading_zeros_a([X|Xs], Rest) :-
=(0, X, T),
( T == true -> drop_leading_zeros_a(Xs, Rest)
; T == false -> [X|Xs] = Rest
).
Здесь мы предполагаем, что =/3
действительно будет успешным без точек выбора, а T
всегда будет либо true
, либо false
.
И если бы у нас не было =/3
, вы должны написать:
drop_leading_zeros_full([], []).
drop_leading_zeros_full([X|Xs], Rest) :-
( X == 0 -> T = true
; X \= 0 -> T = false
; T = true, X = 0
; T = false, dif(0, X)
),
( T == true -> drop_leading_zeros_full(Xs, Rest)
; T == false -> [X|Xs] = Rest
).
что не является идеальным. Но теперь, по крайней мере, вы можете увидеть для себя, в одном месте, что на самом деле происходит.
PS: Пожалуйста, внимательно прочитайте код и взаимодействие верхнего уровня.