理解Prolog列表

9
我是一个有用的助手,我能够翻译文本。

我正在尝试理解Prolog列表以及递归函数在结尾处如何返回/实例化值。

我正在看这个简单的例子:

val_and_remainder(X,[X|Xs],Xs).
val_and_remainder(X,[Y|Ys],[Y|R]) :-
   val_and_remainder(X,Ys,R).

如果我调用val_and_remainder(X, [1,2,3], R).,那么我将得到以下输出:

X = 1, R = [2,3]; 
X = 2, R = [1,3];
X = 3, R = [1,2];
false.

但我对于基本情况(val_and_remainder(X,[X|Xs],Xs).)中为什么要这样出现 Xs 感到困惑。

如果我调用 val_and_remainder(2, [1,2,3], R).,那么它似乎会按照以下程序运行:

% Initial call
val_and_remainder(2, [1,2,3], R).

val_and_remainder(2, [1|[2,3]], [1|R]) :- val_and_remainder(2, [2,3], R).

% Hits base case
val_and_remainder(2, [2|[3]], [3]).

如果上述运行没有问题,那么它是如何得到正确的R值的呢?就像上面的例子中一样,R的值应该是R = [1,3]

1
那么 Xs 如何与 R 统一呢?更重要的是,R 如何获得结果的值? - Guy Coder
@GuyCoder 请查看更新后的问题(基本上是你问我的问题,我不理解)。 - user9467747
你在问题中提供的示例谓词我认为不是学习统一和列表的好选择。一个更好的例子是append/3 - Guy Coder
我的问题基本上是在问你,Prolog如何将一个值放入变量R中返回?它不会这样做。答案返回到第三个位置,即Xs。当Xs与一个值统一时,第三个位置即为Xs将保存结果。当它与调用谓词统一时,未绑定的第三个位置将与Xs的值统一。也许这个答案可以帮助你。 - Guy Coder
你了解统一吗?同时,你了解反向链接吗?如果不了解,首先需要理解它们。如果有人在 StackOveflow 上发布这些问题,而你不能自信地回答它们,那么你需要更深入地学习它们。 - Guy Coder
@GuyCoder 我理解统一性并明白你之前的问题,但我不理解R的结果是如何正确的,因为如果您查看我的程序运行,Xs的值不是最终输出的[1,3],而是[3],它与R相统一(显然我在某个地方遗漏了什么,但我不确定是什么)。 - user9467747
4个回答

6
在Prolog中,你需要将谓词视为关系而不是函数,这与其他语言通常的做法不同。谓词描述的是可能包含帮助定义该关系的参数的关系。
例如,我们来看一个简单的例子:
same_term(X, X).

这是一个谓词,它定义了两个参数之间的关系。通过“统一”,它表明如果第一个和第二个参数被统一(并且定义由我们作为谓词编写者来确定),则它们是相同的。因此,same_term(a, a) 将成功,same_term(a, b) 将失败,并且 same_term(a, X) 将成功,其中 X = a
你也可以用更明确的形式来写这个:
same_term(X, Y) :-
    X = Y.  % X and Y are the same if they are unified

现在让我们来看一下你的例子,val_and_remainder/3。首先,它是什么意思?

val_and_remainder(X, List, Rest)

这意味着XList中的一个元素,而Rest是由所有其余元素(不包括X)组成的列表。(注意:您没有立即解释这个含义,但我从您的示例实现中确定了这个含义。)
现在我们可以写出规则来描述它。首先是一个简单的基本情况:
val_and_remainder(X,[X|Xs],Xs).

这句话的意思是:

Xs 是列表 [X|Xs] 去掉 X 后的余数。

根据 Prolog 中列表 [X|Xs] 的语法定义,这个陈述应该很明显。你需要所有这些参数,因为第三个参数 Xs 必须与列表 [X|Xs] 的尾部(剩余部分)统一,然后也是 Xs(同名变量根据定义被统一)。与之前一样,你可以把它详细地写出来:

val_and_remainder(X, [H|T], R) :-
    X = H,
    R = T.

但是简写实际上更清晰。
现在递归子句是这样说的:
val_and_remainder(X, [Y|Ys], [Y|R]) :- 
    val_and_remainder(X, Ys, R).

所以这意味着:
[Y | R] 是列表 [Y | Ys] 去除元素 X 的余数,如果 R 是列表 Ys 去除元素 X 的余数。
您需要思考这个规则,以确信它在逻辑上是正确的。第二个和第三个参数中的 Y 是相同的,因为它们引用同一个元素,所以它们必须统一。
因此,这两个谓词子句形成了两个规则,涵盖了两种情况。第一种情况是 X 是列表的第一个元素的简单情况。第二种情况是当 X 不是第一个元素时的递归定义。
当您进行查询,例如 val_and_remainder(2, [1,2,3], R). 时,Prolog 会查看是否可以将术语 val_and_remainder(2, [1,2,3], R) 与事实或谓词子句的头部相统一。 它在尝试将其与 val_and_remainder(X,[X|Xs],Xs) 相统一时失败,因为它需要将 X2 统一,这意味着它需要将 [1,2,3][2|Xs] 相统一,但由于列表 [1,2,3] 的第一个元素是 1,而列表 [2|Xs] 的第一个元素是 2,因此失败了。
因此,Prolog 继续并成功地将 val_and_remainder(2, [1,2,3], R)val_and_remainder(X,[Y|Ys],[Y|R]) 相统一,通过将 X 与 2、Y 与 1、Ys 与 [2,3] 和 R 与 [Y|R](注意,这很重要,您调用中的 R 变量与谓词定义中的 R 变量不同,因此我们应该将其命名为 R1,以避免混淆)。我们将您的 R 命名为 R1,并说 R1 与 [Y | R] 统一。

当第二个子句的主体被执行时,它调用val_and_remainder(X,Ys,R),换句话说,val_and_remainder(2, [2,3], R)。这将与第一个子句统一,并给出R = [3]。当您解开所有这些时,您会得到R1 = [Y|[3]],并回想起Y绑定为1,结果是R1 = [1,3]


这很有道理,谢谢!很抱歉立刻又向您提出另一个问题,但是您能否请审核一下我的另一个问题?https://stackoverflow.com/questions/54053834/generating-possible-combinations-in-prolog - user9467747

3

逐步重现Prolog机制通常会导致更多混淆,而不是有所帮助。您可能有一些概念,例如“返回”,这意味着某些非常具体的事情-更适合于命令式语言。

以下是您可以始终使用的不同方法:

询问最一般的查询

...并让Prolog向您解释关系是什么。

?- val_and_remainder(X, Xs, Ys).
   Xs = [X|Ys]
;  Xs = [_A,X|_B], Ys = [_A|_B]
;  Xs = [_A,_B,X|_C], Ys = [_A,_B|_C]
;  Xs = [_A,_B,_C,X|_D], Ys = [_A,_B,_C|_D]
;  Xs = [_A,_B,_C,_D,X|_E], Ys = [_A,_B,_C,_D|_E]
; ... .

所以,XsYs共享一个公共的列表前缀,然后Xs有一个X,接着是一个共同的剩余部分。此查询将继续产生更多答案。有时候,您想要查看所有答案,那么您必须更加具体。但不要过于具体:

?- Xs = [_,_,_,_], val_and_remainder(X, Xs, Ys).
   Xs = [X,_A,_B,_C], Ys = [_A,_B,_C]
;  Xs = [_A,X,_B,_C], Ys = [_A,_B,_C]
;  Xs = [_A,_B,X,_C], Ys = [_A,_B,_C]
;  Xs = [_A,_B,_C,X], Ys = [_A,_B,_C]
;  false.

这里我们列出了四个元素列表的所有可能答案。

在进行特定推理时,坚持基本目标

因此,不要使用val_and_remainder(2, [1,2,3], R).(显然让你头晕),而应该考虑val_and_remainder(2, [1,2,3], [1,3]). 然后再执行 val_and_remainder(2, [2,3],[3])。从这一侧来看,这应该很明显。

从右到左阅读Prolog规则

将Prolog规则视为生产规则。因此,每当规则的右侧都成立时,您就可以得出左侧的结论。因此,:-是20世纪70年代早期表示的一个 ←

以后,您可能还想考虑更复杂的问题。例如:

函数依赖

第一个和第二个参数是否唯一确定最后一个参数?是否满足 X, XsYs

这是一个查询示例,询问同一个 XXsYsYs2 是否不同。

?- val_and_remainder(X, Xs, Ys), val_and_remainder(X, Xs, Ys2), dif(Ys,Ys2).
   Xs = [X,_A,X|_B], Ys = [_A,X|_B], Ys2 = [X,_A|_B], dif([_A,X|_B],[X,_A|_B])
;  ... .

显然,对于给定的XXs,存在不同的Ys值。以下是一个具体的例子:

?- val_and_remainder(x, [x,a,x], Ys).
   Ys = [a,x]
;  Ys = [x,a]
;  false.

这里没有经典的返回。它不止一次返回,更像是一个yield

然而,实际上存在参数之间的函数依赖关系!你能找到它吗?并且你能用Prolog证明它吗(尽管Prolog确实能做出证明)。


请问“参数之间存在函数依赖关系”是什么意思?这是否意味着“制定一个查询,保证始终只会产生一个答案”(变体:不超过一个答案)? - Will Ness
@Will:我们已经定义了关系。现在我们考虑这个关系的属性之一,即函数依赖的概念。从关系数据库到无限关系,这个概念很容易扩展。我现在要问的是:哪些参数可以唯一地确定其他参数。(至于证明:对于大多数一般查询而言无终止的关系的证明也可能是无终止的。好吧,我们必须接受这一点) - false

2
我不理解你的谓词名称。无论如何,它都是一个干扰因素。变量的非统一命名也是一个干扰因素。让我们使用一些中性、简短的单音节名称,以便聚焦于代码本身的最清晰形式。
foo( H, [H | T], T).                          % 1st clause

foo( X, [H | T], [H | R]) :- foo( X, T, R).   % 2nd clause

所以这是内置的 select/3耶!...
现在你问关于查询 foo(2,[1,2,3],R)R 如何正确设置其值。 你概述中主要缺少的是当选择匹配子句时变量的重命名。查询的 解析 过程如下:
|-  foo( 2, [1,2,3], R) ? { } 
  %% SELECT -- 1st clause, with rename
  |-  ?  { foo( H1, [H1|T1], T1) = foo( 2, [1,2,3], R) }
         **FAIL** (2 = 1)
         **BACKTRACK to the last SELECT**
  %% SELECT -- 2nd clause, with rename
  |-  foo( X1, T1, R1) ?
         { foo( X1, [H1|T1], [H1|R1]) = foo( 2, [1,2,3], R) }
         **OK**
    %% REWRITE
    |-  foo( X1, T1, R1) ?
          { X1=2, [H1|T1]=[1,2,3], [H1|R1]=R }
    %% REWRITE
    |-  foo( 2, [2,3], R1) ?  { R=[1|R1] } 
      %% SELECT -- 1st clause, with rename
      |-  ? { foo( H2, [H2|T2], T2) = foo( 2, [2,3], R1), R=[1|R1] }
             ** OK ** 
        %% REWRITE
        |-  ? { H2=2, T2=[3], T2=R1, R=[1|R1] }
        %% REWRITE
        |-  ? { R=[1,3] }
        %% DONE

在符号“|-”和“?”之间的目标是“解”,花括号内的方程式是“替换”。知识库(KB)完全隐含在符号“|-”的左侧。
在每一步中,选择解中最左边的目标,在KB中选择一个具有匹配头部的子句(同时以一致的方式重命名子句中的所有变量,以使解中的任何变量都不被重命名的子句使用,从而避免意外的变量捕获),并用该子句的主体替换所选目标,同时将成功的统一添加到替换中。当解为空时,查询已被证明,我们看到的是整个and-or tree中的一个成功的“and”分支。
这是机器可能会做的方式。这里介绍“重写”步骤以方便人类理解。
因此,我们可以看到第一个成功的子句选择导致了方程式。
     R = [1 | R1     ]

"

",第一个标签,-- "

"
              R1 = [3]

"

",它们共同包含 "

"。
     R = [1,        3]

这种逐步自上而下的实例化/列表详细说明是Prolog的一种典型方式。
针对赏金挑战,关于关系foo/3(即select/3)中的函数依赖:在foo(A,B,C)中,任何两个确定的BC值唯一地确定了A的值(或者A不存在)。
2 ?- foo( A, [0,1,2,1,3], [0,2,1,3]).
A = 1 ;
false.

3 ?- foo( A, [0,1,2,1,3], [0,1,2,3]).
A = 1 ;
false.

4 ?- foo( A, [0,1,2,1,3], [0,1,2,4]).
false.

f ?- foo( A, [0,1,1], [0,1]).
A = 1 ;
A = 1 ;
false.

尝试通过反驳来证明其错误:
10 ?- dif(A1,A2), foo(A1,B,C), foo(A2,B,C).
Action (h for help) ? abort
% Execution Aborted

Prolog无法找到反驳的论据。
尝试通过迭代加深来更仔细地观察正在发生的事情:
28 ?- length(BB,NN), foo(AA,BB,CC), XX=[AA,BB,CC], numbervars(XX), 
      writeln(XX), (NN>3, !, fail).
[A,[A],[]]
[A,[A,B],[B]]
[A,[B,A],[B]]
[A,[A,B,C],[B,C]]
[A,[B,A,C],[B,C]]
[A,[B,C,A],[B,C]]
[A,[A,B,C,D],[B,C,D]]
false.

29 ?- length(BB,NN), foo(AA,BB,CC), foo(AA2,BB,CC), 
      XX=[AA,AA2,BB,CC],  numbervars(XX), writeln(XX), (NN>3, !, fail).
[A,A,[A],[]]
[A,A,[A,B],[B]]
[A,A,[A,A],[A]]
[A,A,[A,A],[A]]
[A,A,[B,A],[B]]
[A,A,[A,B,C],[B,C]]
[A,A,[A,A,B],[A,B]]
[A,A,[A,A,A],[A,A]]
[A,A,[A,A,B],[A,B]]
[A,A,[B,A,C],[B,C]]
[A,A,[B,A,A],[B,A]]
[A,A,[A,A,A],[A,A]]
[A,A,[B,A,A],[B,A]]
[A,A,[B,C,A],[B,C]]
[A,A,[A,B,C,D],[B,C,D]]
false.

"

AAAA2总是被实例化为相同的变量。

数字3没有特殊之处,因此可以通过概括推断出,无论尝试的长度如何,它们始终如一。

"
另一种适用于Prolog的证明尝试:
ground_list(LEN,L):-
  findall(N, between(1,LEN,N), NS), 
  member(N,NS),
  length(L,N), 
  maplist( \A^member(A,NS), L).

bcs(N, BCS):- 
  bagof(B-C, A^(ground_list(N,B),ground_list(N,C),foo(A,B,C)), BCS).

as(N, AS):- 
  bagof(A, B^C^(ground_list(N,B),ground_list(N,C),foo(A,B,C)), AS).

proof(N):-
  as(N,AS), bcs(N,BCS), 
  length(AS,N1), length(BCS, N2), N1 =:= N2.

这段话的意思是比较成功的B-C组合总数与它们所产生的A的数量。如果相等,则表示一对一的对应关系。
因此我们有:
2 ?- proof(2).
true.

3 ?- proof(3).
true.

4 ?- proof(4).
true.

5 ?- proof(5).
true.

因此,对于任何的 N,它都成立。速度越来越慢。编写一个通用的、无限制的查询很容易,但是速度下降似乎是指数级的。

不确定为什么您要用另一个名称生成进一步的定义。问题是关于OP的定义的。 - false
谢谢,我会再多做一些工作......(numbervars仅用于以可读的方式显示结果)。如果您指的是SICStus,则它是非免费的,尽管对于个人使用而言相对便宜。如果不是,您会推荐哪个? - Will Ness
对我来说,SICStus 目前是唯一的选择。但是 SWI 也可能会有所改进。不过你需要在那里付出努力。 - false
@false 非常感谢您的慷慨奖励。 :) 我猜您可能还想要更多。 :) 我不太明白如何在术语中存在未实例化变量时使 bagof 等函数起作用,因此不得不进行广泛的测试。也许应该对第二个列表使用 2N 范围以确保彻底... - Will Ness
正如我在答案中所说,我无法理解“val_and_remainder”的含义。为了不分心,我给它起了一个中性的名称,只关注代码本身。变量名也是一样。对我来说,这样做可以节省精力,而且重复命名更难,如果没有意义,那么简短就更好。这是个人理解/感知的问题。就像他们说的,“YMMV”。 - Will Ness
显示剩余4条评论

2

来自评论:

如何确保R的结果是正确的,因为如果您查看我的程序运行过程中的值,Xs的值不是[1,3],这是最终输出的结果;而是[3],它与R相一致(显然我漏掉了某些步骤,但我不确定是什么)。

这是正确的:

% Initial call
val_and_remainder(2, [1,2,3], R).

val_and_remainder(2, [1|[2,3]], [1|R]) :- val_and_remainder(2, [2,3], R).

% Hits base case
val_and_remainder(2, [2|[3]], [3]).

然而,Prolog不同于其他编程语言,你不能在返回语句处输入和输出。在Prolog中,你通过谓词语句向前移动,统一并继续执行真实的谓词,并在回溯时也对未绑定的变量进行统一。(这在技术上并不完全正确,但如果你这样想会更容易理解。)
你没有考虑到回溯时现在被绑定的未绑定变量。
当你达到基本情况时,Xs已经被绑定为[3]
但是当你回溯时,你需要查看
val_and_remainder(2, [1|[2,3]], [1|R])

特别是对于第三个参数,需要使用[1|R]

由于在调用基本情况时Xs已经与R统一了,即

val_and_remainder(X,[X|Xs],Xs).

R现在有[3]

现在是第三个参数位置在

val_and_remainder(2, [1|[2,3]], [1|R])

[1|R],这也可以写作 [1|[3]],使用语法糖后则变成了 [1,3],而不是仅仅 [3]

现在当查询

val_and_remainder(2, [1,2,3], R).

当运行时,查询 R 的第三个参数会与谓词的第三个参数统一。
val_and_remainder(X,[Y|Ys],[Y|R])

因此,R[Y|R] 统一在一起,在回溯时变成了 [1,3],因此绑定到查询变量 R 的值是 [1,3]


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接