33

谓词if_/3似乎在 Stack Overflow 的 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,_))
   ).

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

我发现的大多数链接都直接使用这个谓词,并且几乎没有解释为什么要使用它,Prolog 的非专家可以很容易地理解。

4

2 回答 2

23

在老式的 Prolog 代码中,以下模式经常出现:

谓词([],...)。
谓词([L|Ls], ...) :-
        条件(L),
        然后(Ls,...)。
谓词([L|Ls], ...) :-
        \+ 条件(L) ,
        否则(Ls,...)。

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

悲剧如下:

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

总之,现有的结构和语言特征在某种程度上都不足以表达实践中经常出现的模式。因此,几十年来,似乎有必要 妥协。您可以很好地猜测 Prolog 社区中“妥协”通常会朝哪个方向发展:几乎总是为了效率而牺牲正确性,以防有疑问。毕竟,只要你的程序很快,谁会关心正确的结果,对吧?因此,在 发明之前if_/3,这经常被错误地写成:

谓词([],...)。
谓词([L|Ls], ...) :-
        (    条件(L) ->
             然后(Ls,...)。
        ; 否则(Ls,...)。
        )

这样做的错误当然是当元素没有充分实例化时,即使两种选择在逻辑上都是可能的,这也可能会错误地提交到一个分支。出于这个原因,使用 if-then-else 几乎总是声明性错误,并且由于它违反了我们对纯 Prolog 程序所期望的最基本属性,因此极大地阻碍了声明性调试方法。


使用if_/3,您可以这样写:

谓词([],...)。
谓词([L|Ls], ...) :-
        if_(条件(L) ,
            那么(Ls, ...),
            否则(Ls,...))。

保留所有可取的方面。这是:

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

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

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

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

于 2016-10-03T14:27:33.197 回答
12

if_/3让我们尝试使用;来解决一个简单的问题。例如,我将尝试将一个列表(按 predicate 排序)p/2分成两个列表:一个前缀,对于每个元素X,我们都有.p(X, true)p/2p(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]
                )
        ).

传递给此元谓词的谓词将采用两个参数:第一个是当前列表元素,第二个是trueor 或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总是会是trueor 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:请仔细阅读代码和顶层交互。

于 2016-10-03T14:35:59.557 回答