if_/3有什么用?

What use does if_/3 have?

谓词 if_/3 seems to be fairly popular 是 Stack Overflow 序言部分的少数主要贡献者之一。

这个谓词是这样实现的,由@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,_))
   ).

但是,我一直无法找到清楚、简单和简明的解释这个谓词的作用,以及它与 e.g. 相比有何用途。 Prolog if -> then ; else 的经典 if-then-else 结构。

我发现的大多数链接都直接使用这个谓词,并且很少解释为什么使用它,非 Prolog 专家也能轻松理解。

在老式的 Prolog 代码中,以下模式出现得相当频繁:

predicate([], ...).
predicate([L|Ls], ...) :-
        condition(L),
        then(Ls, ...).
predicate([L|Ls], ...) :-
        \+ condition(L),
        else(Ls, ...).

我在这里使用列表作为发生这种情况的示例(参见 include/3exclude/3 等),尽管这种模式当然也出现在其他地方。

悲剧的是:

  • 对于实例化列表,模式匹配可以区分第一个子句和其余两个子句,但不能区分第二个和最后一个子句,因为它们 都将 '.'(_, _) 作为第一个参数的主要函子和元数。
  • 最后两个子句适用的条件显然互斥
  • 因此,当一切已知时,我们希望获得一个高效的、确定性的谓词不留下选择点,理想情况下甚至 创建 个选择点。
  • 但是,只要不是所有的事情都可以安全确定,我们想受益于回溯看看所有解决方案,因此我们无法承担任何条款。

总而言之,现有的结构和语言功能在表达实践中经常出现的模式方面都存在不足。因此,几十年来,似乎有必要妥协。您可以很好地猜测 "compromises" 通常在 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, ...)).

保留所有理想的方面。这是:

  • 确定性 如果一切都可以安全地决定
  • 高效因为它甚至不创建选择点
  • 完整 因为你永远不会错误地 提交到一个特定的分支。

价格 相当实惠:正如鲍里斯在评论中提到的,您需要实施 具体化。我现在对此有一些经验,并且发现通过一些练习它相当容易。

大家好消息:在很多情况下,condition的形式是(=)/2,或者(#=)/2,甚至第一个发货library(reif) 免费

有关详细信息,请参阅 Ulrich Neumerkel 和 Stefan Kral 的 Indexing dif/2

让我们尝试使用 if_/3 来解决一个简单的问题;例如,我将尝试将一个列表(根据谓词 p/2 排序)分成两个列表:一个前缀,其中对于每个元素 X,我们有 p(X, true),以及一个前缀(其中,如果列表按 p/2 排序,我们将有 p(X, false).

我将使用库 reif 作为 here。所以,这是我的程序的完整代码:

:- 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]
                )
        ).

传递给这个元谓词的谓词将有两个参数:第一个是当前列表元素,第二个是 truefalse。理想情况下,此谓词将始终成功并且不会留下选择点。

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 总是 truefalse

而且,如果我们也没有 =/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:请仔细阅读代码和顶层交互