【问题标题】:prolog - infinite ruleprolog - 无限规则
【发布时间】:2011-06-24 18:08:19
【问题描述】:

我有下一条规则

% Signature: natural_number(N)/1
% Purpose: N is a natural number.
natural_number(0).
natural_number(s(X)) :-
   natural_number(X).

ackermann(0, N, s(N)). % rule 1
ackermann(s(M),0,Result):-
   ackermann(M,s(0),Result). % rule 2
ackermann(s(M),s(N),Result):-
   ackermann(M,Result1,Result),
   ackermann(s(M),N,Result1). % rule 3

查询是:ackermann (M,N,s(s(0)))

现在,据我了解,在第三次计算中,我们得到了无限搜索(失败分支)。我检查了一下,我得到了一个有限搜索(失败分支)。

我将解释:首先,我们得到了 M=0, N=s(0) 的替换(规则 1 - 成功!)。在第二个中,我们得到了 M=s(0),N=0 的替换(规则 2 - 成功!)。但是现在呢?我尝试匹配 M=s(s(0)) N=0,但它有一个 finite 搜索 - 失败分支。为什么编译器不写我“失败”。

谢谢。

【问题讨论】:

    标签: prolog failure-slice successor-arithmetics


    【解决方案1】:

    很难理解汤姆在这里问的是什么。也许人们期望谓词 natural_number/1 会以某种方式影响 ackermann/3 的执行。它不会。后一个谓词是纯递归的,不会产生依赖于 natural_number/1 的子目标。

    当为 ackermann/3 定义显示的三个子句时,目标:

    ?- ackermann(M,N,s(s(0))).

    导致 SWI-Prolog 找到(回溯)Tom 报告的两个解决方案,然后进入无限递归(导致“Out of Stack”错误)。我们可以肯定,这种无限递归涉及为 ackermann/3 给出的第三个子句(代码中汤姆的 cmets 的规则 3),因为在没有它的情况下,我们只能得到两个公认的解决方案,然后是显式失败:

    M = 0,
    N = s(0) ;
    M = s(0),
    N = 0 ;
    false.
    

    在我看来,Tom 要求解释为什么 将提交的查询更改为设置 M = s(s(0))N = 0 的查询,从而产生有限搜索(找到一个解决方案然后失败回溯),与前面查询产生的无限递归一致。我的怀疑是对 Prolog 引擎在回溯中尝试的内容存在误解(对于原始查询),所以我将深入研究。希望它可以为汤姆解决问题,但让我们看看是否可以。诚然我的处理比较罗嗦,但是Prolog的执行机制(子目标的统一和解析)还是值得研究的。

    [补充:谓词与著名的Ackermann function 有明显的联系,它是完全可计算的,但不是原始递归的。这个函数以快速增长而闻名,所以我们在声明无限递归时需要小心,因为一个非常大但有限的递归也是可能的。然而,第三个子句将其两个递归调用的顺序与我所做的相反,并且这种反转似乎在我们在逐步执行下面的代码时发现的无限递归中发挥了关键作用。]

    当顶级目标ackermann(M,N,s(s(0))) 被提交时,SWI-Prolog 会尝试为 ackermann/3 定义的子句(事实或规则),直到找到其“头”与给定的一致询问。 Prolog 引擎没有远看作为第一个子句,这个事实:

    ackermann(0, N, s(N)).

    将统一,绑定M = 0N = s(0),正如已经描述的第一次成功。

    如果要求回溯,例如通过用户键入分号,Prolog 引擎检查是否有替代方法来满足第一个子句。那没有。然后,Prolog 引擎继续按给定顺序尝试 ackermann/3 的以下子句。

    再一次,搜索不必走太远,因为第二个子句的头部也与查询相结合。在这种情况下,我们有一个规则:

    ackermann(s(M),0,Result) :- ackermann(M,s(0),Result).

    根据查询中使用的变量,统一查询和此规则的头部会产生绑定M = s(0)N = 0。就上述规则中使用的变量而言,M = 0Result = s(s(0))。请注意,统一通过它们作为调用参数的外观来匹配术语,并且不将跨查询/规则边界重用的变量名视为表示身份。

    因为这个子句是一个规则(有主体也有头),统一只是尝试成功的第一步。 Prolog 引擎现在尝试出现在该规则正文中的一个子目标:

    ackermann(0,s(0),s(s(0))).

    请注意,此子目标来自将规则中使用的“本地”变量替换为统一值M = 0Result = s(s(0))。 Prolog 引擎现在递归调用谓词 ackermann/3,以查看是否可以满足此子目标。

    它可以,因为 ackermann/3 的第一个子句(事实)以明显的方式统一(实际上与之前在子句中使用的变量的方式基本相同)。因此(在此递归调用成功时),我们在外部调用(顶级查询)中获得了第二个成功的解决方案。

    如果用户要求 Prolog 引擎再次回溯,它会再次检查当前子句(ackermann/3 的第二个子句)是否可以以另一种方式得到满足。它不能,因此通过传递到谓词 ackermann/3 的第三个(也是最后一个)子句继续搜索:

    ackermann(s(M),s(N),Result) :-
        ackermann(M,Result1,Result),
        ackermann(s(M),N,Result1).
    

    我要解释一下,这种尝试确实会产生无限递归。当我们将顶级查询与该子句的头部统一起来时,我们会得到参数的绑定,通过将查询中的术语与头部中的术语对齐,我们或许可以清楚地理解这些参数:

       query     head
         M       s(M)
         N       s(N)
       s(s(0))  Result
    

    请记住,查询中与规则中的变量同名的变量不会限制统一,因此可以统一这三个术语。查询M 将是头部s(M),这是一个复合术语,涉及函子s,应用于头部出现的一些未知变量M。查询N 也是如此。到目前为止,唯一的“基础”术语是出现在规则头部(和主体)中的变量 Result,它已从查询中绑定到 s(s(0))

    现在第三个子句是一条规则,因此 Prolog 引擎必须继续尝试满足出现在该规则主体中的子目标。如果您将头部统一中的值替换为主体,则要满足的第一个子目标是:

    ackermann(M,Result1,s(s(0))).

    让我指出,我在这里使用了子句的“本地”变量,除了我用它在统一中绑定的值替换了Result。现在请注意,除了将原始顶级查询的 N 替换为变量名称 Result1 之外,我们只是在此子目标中询问与原始查询相同的事情。当然,这是我们可能即将进入无限递归的重要线索。

    但是,需要进行更多讨论才能了解为什么我们没有报告任何进一步的解决方案!这是因为第一个子目标的第一个成功(如前所述)将需要 M = 0Result1 = s(0),然后 Prolog 引擎必须继续尝试子句的第二个子目标:

    ackermann(s(0),N,s(0)).

    很遗憾,这个新的子目标与 ackermann/3 的第一个子句(事实)不一致。它确实与第二个子句的头部统一,如下:

       subgoal     head
         s(0)      s(M)
          N         0
         s(0)     Result
    

    但这会导致一个子目标(来自第二个子句的主体):

    ackermann(0,s(0),s(0)).

    这不与第一个或第二个子句的头部统一。它也不与第三个子句的头部统一(它要求第一个参数具有s(_) 的形式)。所以我们在搜索树中遇到了一个失败点。 Prolog 引擎现在回溯以查看是否可以以替代方式满足第三子句主体的第一个子目标。众所周知,可以(因为这个子目标与原始查询基本相同)。

    现在 M = s(0)Result1 = 0 的第二个解决方案导致第三个子句正文的第二个子目标:

    ackermann(s(s(0)),N,0).

    虽然这不与谓词的第一个子句(事实)统一,但它确实与第二个子句的头部统一:

       subgoal     head
       s(s(0))     s(M)
          N         0
          0       Result
    

    但是为了成功,Prolog 引擎还必须满足第二个子句的主体,现在是:

    ackermann(s(s(0)),s(0),0).

    我们可以很容易地看到这不能与 ackermann/3 的第一个或第二个子句的头部统一。可以和第三个子句的头部统一:

      sub-subgoal  head(3rd clause)
        s(s(0))       s(M)
          s(0)        s(N)
           0         Result
    

    正如现在应该熟悉的那样,Prolog 引擎检查是否可以满足第三个子句主体的第一个子目标,这相当于这个子子子目标:

    ackermann(s(0),Result1,0).

    这无法与第一个子句(事实)统一,但与绑定 M = 0Result1 = 0Result = 0 的第二个子句的头部统一,产生(通过熟悉的逻辑)子子子-子目标:

    ackermann(0,0,0).

    由于 this 不能与三个子句的任何一个头部统一,因此失败。此时,Prolog 引擎回溯到尝试使用第三个子句来满足上述子子目标。统一是这样的:

      sub-sub-subgoal  head(3rd clause)
           s(0)             s(M)
          Result1           s(N)
             0             Result
    

    然后,Prolog 引擎的任务是满足从第三个子句主体的第一部分派生的子子子子目标:

    ackermann(0,Result1,0).

    但这不会与三个子句中的任何一个的头部统一。对上述子子目标的解决方案的搜索以失败告终。 Prolog 引擎一直回溯到它第一次尝试满足原始顶级查询调用的第三个子句的第二个子目标的位置,因为现在已经失败了。换句话说,它试图用第三个子句的第一个子目标的前两个解决方案来满足它,你会记得它本质上是相同的,除了变量名称与原始查询的更改:

    ackermann(M,Result1,s(s(0))).

    我们在上面看到的是这个子目标的解决方案,从 ackermann/3 的第一个和第二个子句复制原始查询,不允许第三个子句主体的第二个子目标成功。因此,Prolog 引擎试图找到满足第三个子句的解决方案。但很明显,这现在进入了无限递归,因为第三个子句将在其头部统一,但第三个子句的主体将重复我们刚刚进行的相同搜索。因此,Prolog 引擎现在会无休止地进入第三个子句的主体。

    【讨论】:

      【解决方案2】:

      让我重新表述您的问题:查询ackermann(M,N,s(s(0))). 找到两个解决方案,然后循环。理想情况下,它会在这两个解决方案之后终止,因为没有其他 NM 的值为 s(s(0))

      那么为什么查询不会普遍终止呢?理解这一点可能相当复杂,最好的建议是不要尝试理解精确的执行机制。有一个非常简单的原因:Prolog 的执行机制非常复杂,如果您尝试通过单步执行代码来理解它,无论如何您很容易误解它。

      相反,您可以尝试以下操作:在程序中的任何位置插入目标 false。如果生成的程序没有终止,那么原始程序也不会终止。

      在你的情况下:

      ackermann(0, N, s(N)) :- falseackermann(s(M),0,Result):- false,
         阿克曼(M,s(0),结果)。
      阿克曼(s(M),s(N),结果):-
         ackermann(M,Result1,Result), ,
         阿克曼(s(M),N,Result1)

      我们现在可以删除第一个和第二个子句。而在第三个子句中,我们可以去掉 false 之后的目标。所以如果下面的片段没有终止,那么原程序也不会终止。

      ackermann(s(M),s(N),Result):-ackermann(M,Result1,Result), false.
      

      这个程序现在只有在第一个参数已知时才会终止。但在我们的例子中,它是免费的......

      也就是说:通过考虑程序的一小部分(称为),我们已经能够推断出整个程序的属性。详情请见this paper和网站上的其他人。

      不幸的是,这种推理仅适用于未终止的情况。对于终止,事情更复杂。最好的方法是尝试像cTI 这样的工具,它可以推断终止条件并尝试证明它们的最优性。我已经进入你的程序了,试试修改if看看效果吧!

      如果我们这样做:这个小片段还告诉我们第二个参数不会影响终止1。这意味着,像ackermann(s(s(0)),s(s(0)),R). 这样的查询将 也不终止。交换目标以查看差异...


      1 确切地说,与s(_) 不统一的术语会影响终止。想想0。但是任何s(0)s(s(0))、...都不会影响终止。

      【讨论】:

      • 感谢您的回答,但我必须了解系统的工作原理(用于我的理论研究)。你很好地解释了如何通过prolog检查这个问题,但我必须知道为什么会出现这个问题。
      • @false:我不同意 Prolog 的执行机制如此“复杂,如果你试图通过单步执行代码来理解它,无论如何你很容易误解它”。按照您的建议简化代码可能会有所帮助,但我们不应该至少能够将原始程序的行为与您的简化行为严格联系起来,并从该示例中受益吗?
      • @hardmath: 简化程序(故障片)与原程序之间存在严格的联系:如果故障片没有终止,原程序也不会终止终止。不幸的是,这是唯一的联系。
      • @hardmath:关于Prolog执行机制的复杂性:你看到上面的程序什么时候终止了吗?当第一个和第三个参数被绑定时,我没有意识到它会终止(除了其他情况)。您可以通过 cTI 看到这一点(上面的链接)。
      • @hardmath:交换目标会使ackermann(b,b,f) 终止——目前情况并非如此。上面的 cTI-link 可用于此目的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多