在Prolog中重构纠结的循环规则

时间:2014-10-15 01:11:06

标签: prolog refactoring

在前面:这不是一个家庭作业。我正在努力学习Prolog,这只是一个需要解决的问题,而Prolog是一个完美的选择。

我有一堆家庭关系,包括我的事实:

male/1
female/1
husband/2
wife/2
father/2
mother/2
grandfather/2
grandmother/2
son/2
daughter/2
brother/2
sister/2
uncle/2
aunt/2
nephew/2
niece/2
cousin/2

我拥有的数据不完整,缺少家庭网格的许多链接。我的事实来自外部来源,我只能提供规则。对于某个人,我可能会malebrother和表兄,而是另一个motherwife。在最糟糕的情况下,我几乎不知道cousin,但有足够的其他事实能够推断出谁是,比如说,叔叔,因此这个人可能是其他地方提到的那个兄弟,因此是男性。等等。

我无法影响将会有哪些事实。这就是问题的全部要点:如果事实完整,我就不需要这样做了。我可以手工做猜测,但这就是计算机的用途,如果我能找到表达方式的话。因此,我们的目标是尽可能地填补缺失的环节,特别是关于叔叔,阿姨,侄子,侄女,特别是堂兄的“间接”关系,这些关系众所周知是不完整的。

我可以像这样天真地写下我的规则:

male(Who) :-
   brother(Who, _); father(Who, _); uncle(Who, _); …
brother(Who, Whose) :-
   sibling(Who, Ofwhom), male(Who).
sibling(Who, Whose) :-
   brother(Who, Whose) ; brother(Whose, Who).
motherly_cousin(Cousin, Whose) :-
   cousin(Cousin, Whose),
   sibling(Mother, Uncle_or_Aunt),
   parent(Uncle_or_Aunt, Cousin).

我很确定我试图以一种根本错误的方式解决问题,因为我认为无法打破循环推理。在没有打破圈子的情况下,我为此设计的任何Prolog程序都将退化为无休止的递归。

那么我该怎样做才能将这种纠结分解成可以解决的问题呢?

3 个答案:

答案 0 :(得分:6)

一般来说,这是一个棘手的问题。检查这种递归是否是可能的(类似于统一中的发生检查),但是大多数实现都省略它们,因为(a)通常不清楚要排除哪些递归路径; (b)计算成本太高;或者(c)程序员通常有办法规避代码中的问题。

有很多方法可以解决这个问题,有些方法比其他方法更糟糕。我将介绍一种方式:

  • 允许你合理地定义你的谓词天真;
  • 处理不完整的事实;
  • 非常低效;
  • 不会无限递归。

我将描述的方式是使用元解释器。 Prolog中的标准解释器不会检查您的代码是否一遍又一遍地执行相同的条款。例如,brother/2sibling/2的定义之间存在一个令人讨厌的相互递归的情况。虽然您为它们提供的定义似乎没有问题,但请考虑当所有参数未绑定时调用它们时会发生什么:

brother(X, Y)sibling(X, Y)brother(X, Y)↝...(ad infinitum / nauseum)

相反,我们可以做的是定义如何完全理解这些谓词的执行情况,通过将它们的执行指向单独的谓词(我称之为meta/1),它们可以无限递归。这个谓词是元解释器,它将指导Prolog如何以防止无限递归的方式执行您提供的规则和事实。这是一个可能的定义(带注释内联):

meta(Goal) :-
    % defer to meta/2 with a clause reference accumulator 
    meta(Goal, []).

meta(true, _ClauseRefs) :- 
    % the body to execute is true (i.e., a fact); just succeed.
    !,
    true.

meta(meta(X), ClauseRefs) :-
    % the body to execute is a call to the meta interpreter. 
    % interpret the interior goal X, and NOT the interpreter itself.
    !,
    meta(X, ClauseRefs).

meta((G0, G1), ClauseRefs) :- 
    % interpret a conjunct: ,/2. G0 then G1:
    !,
    % interpret the first sub-goal G0
    meta(G0, ClauseRefs),
    % then interpret the second sub-goal G1
    meta(G1, ClauseRefs).

meta((G0 ; G1), ClauseRefs) :- 
    % interpret a disjunct: ;/2. One or the other:
    ( meta(G0, ClauseRefs) 
    ; meta(G1, ClauseRefs) 
    ),
    !.

meta(G0, ClauseRefs) :-
    % G0 is an executable goal: look up a clause to execute 
    clause(G0, Body, Ref),
    % check to see if this clause reference has already been tried 
    \+ memberchk(Ref, ClauseRefs),
    % continue executing the body of this previously unexecuted clause 
    meta(Body, [Ref|ClauseRefs]).
设计

meta/1meta/2,以便它们以确保在目标执行分支中使用的每个子句明确不重复的方式执行提供给它们的目标。为了在您的情况下使用它,请考虑以下事项:

brother_of(a, b).
brother_of(b, c).
brother_of(d, e).
brother_of(X, Y) :- meta((sibling_of(X, Y), male(X))).

male(a).
male(d).
male(b).
male(X) :- meta(brother_of(X, _)).

female(c).
female(e).
female(X) :- meta(sister_of(X, _)).

sister_of(X, Y) :- meta((sibling_of(X, Y), female(X))).

sibling_of(X, Y) :- meta(brother_of(X, Y)).
sibling_of(X, Y) :- meta(brother_of(Y, X)).
sibling_of(X, Y) :- meta(sister_of(X, Y)).
sibling_of(X, Y) :- meta(sister_of(Y, X)).

注意任何递归子句的主体如何包含在meta/1的调用中,指导Prolog使用元解释器执行它们的定义,这将确保它们的执行(通过解释)不会递归。例如,目标:

?- sister_of(X,Y).
X = c,
Y = b ;
X = c,
Y = b ;
X = c,
Y = b ;
... 
X = e,
Y = d ;
false.

请注意,它在通过所有可能的非递归执行路径找到所有绑定后终止,这意味着可能会有很多重复(因此,效率低下的原因)。要查找唯一绑定,可以使用setof/3,如下所示:

?- setof(sister_of(X,Y), sister_of(X,Y), Set).
Set = [sister_of(c, b), sister_of(e, d)].

这只是您可能会觉得有用的一种方法,并且通常是Prolog程序员需要注意的一个很好的(尽管是高级的)工具。您不需要坚持固有的执行策略。

对于在实践中只考虑使用meta/1meta/2的人来说,您还应该考虑其他一些事项:

  • 也许您可能想要或需要允许在执行(子)目标时多次执行相同的子句(例如,如果您需要执行相同的子句但具有不同的头部绑定)。作为一个例子,考虑如何使用元解释器递归地实现ancestor/2,这可能需要使用不同的头部绑定(即路径扩展)多次执行相同的子句(本身)。在这种情况下,您可以跟踪子句引用及其特定的头部绑定作为Ref-Head项,而不是简单地跟踪子句引用,并检查之前是否已执行这些。这可能是一大堆额外的信息,可能很昂贵!
  • 以上meta/1meta/2的定义仅处理事实等谓词(隐含true作为其主体);或使用任何组合(,/2)和析取(;/2)定义的主体的谓词。您只需向meta/2添加更多子句即可处理其他语言结构,例如蕴涵(->/2),否定(\+/1),剪切(!/0)等。如果你需要。
  • 并非所有这类问题都需要使用元解释器。例如,你可以在执行之前简单地构造你的子句并检查模式(即谓词绑定是地面/非地面),但是这可能会变得棘手,程序越复杂。< / LI>
  • 如果你认真思考这个问题,或许有一种方法可以简单地避免使用递归:即,不要使用递归定义,而是使用不同名称的谓词,这些谓词不是相互递归的。

答案 1 :(得分:6)

+1通常&#34;家庭示例&#34;。

除了其他人已经说过的内容之外,请考虑使用约束处理规则(CHR)。它们似乎非常适合这个问题,需要根据一组事实和规则来计算修复点。

编辑:根据要求,举个小例子。我专注于围绕brother_of/2的插图。首先,请注意brother_of/2显然比male/1更具体,因为我们知道兄弟总是男性,反之则不然。因此,第一个CHR规则非正式地说:当brother_of(X,_)成立且male(X)成立时,请删除male(X)约束,因为它总是可以在以后推断出来。第二条规则显示了brother(X, Y)持有的演绎的示例。第三条规则删除了多余的brother_of/2约束。

使用SWI-Prolog测试的完整代码:

:- use_module(library(chr)).

:- chr_constraint male/1, brother_of/2, child_parent/2.

brother_of(X, Y) \ male(X) <=> brother_of(X, Y).

male(X), child_parent(X, P), child_parent(Y, P) ==> X \== Y | brother_of(X, Y).

brother_of(X, Y) \ brother_of(X, Y) <=> true.

示例查询及其结果:

?- male(john), child_parent(john, mary), child_parent(susan, mary).
brother_of(john,susan)
child_parent(susan,mary)
child_parent(john,mary)
true ;
false.

答案 2 :(得分:3)

在@ sharky的优秀帖子之后,我有点羞于试图说些什么。但是,如果我接近这个问题,我会做一些较小的改动,并接受Prolog不完全符合逻辑。这就是说我会选择@ sharky建议完全避免相互递归。

首先,性别在现实中是一种内在的而非一种衍生的事实。通过这个我只是意味着我不会从属于父亲/叔叔/兄弟与其他人的关系中获得我的男性。

如果你把male(X)放在sibling(X,Y)之前而不是之后,它对程序的逻辑含义没有影响,但它会改变Prolog执行程序的方式,实际上是重要的。例如,如果X未绑定,male(X)可能会生成(假设您进行了我建议的更改),而无需通过brother/2的远程相互递归重新输入sibling/2

我鼓励你将事实与谓词分开,除非事实确实是基本情况。

不幸的是,Prolog不会让你不必设计一个连贯的数据模型。您仍然需要担心是否以正确的形状存储正确的数据。你可以用任何一种方式提供丰富的API,只是现在你的数据类型被涂抹了。你可以用任何东西装箱,只是在Prolog中,即使发生这种情况,你也会得到部分结果。

我觉得tabling可能对你有所帮助,但由于它只存在于相当模糊的实现中,因此效益可能太有限。我自己从未使用它,所以我不知道它是否确实解决了这些问题,或者只是减轻了轻度问题的症状。我怀疑后者只是因为如果它真的有用并且解决一个重要的内在问题我会期望它被移植到GNU和SWI(但也许我过于乐观)。