Recursion 理解Prolog“append”递归定义

Recursion 理解Prolog“append”递归定义,recursion,prolog,Recursion,Prolog,我正在阅读,但我在理解书中介绍的append的递归定义时遇到了问题: append([], List, List). append([X|List1], List2, [X|Result]) :- append(List1, List2, Result). 例如: ?- append([a, b, c], [3, 2, 1], Result). Result = [a, b, c, 3, 2, 1] 据我所知,这个定义说,结果列表应该包含第一个列表的头作为它的头,所以最初结果列表是[a]。然

我正在阅读,但我在理解书中介绍的append的递归定义时遇到了问题:

append([], List, List).
append([X|List1], List2, [X|Result]) :- append(List1, List2, Result).
例如:

?- append([a, b, c], [3, 2, 1], Result).
Result = [a, b, c, 3, 2, 1]

据我所知,这个定义说,结果列表应该包含第一个列表的头作为它的头,所以最初结果列表是[a]。然后,我们递归地在第一个和第三个参数的尾部运行append,保持第二个参数不变,所以第三个参数[a]应该包含新的第一个参数的头作为它的头,所以结果列表是[b,a],它是向后的,所以很明显我没有正确地跟随。在某些情况下,第一个列表是[],结果数组是[c,b,a],因此我们使用基本情况:

append([], List, List).
所以附加[],[3,2,1],[c,b,a],这毫无意义。如果在整个定义中没有对第二个列表执行任何操作,我也不了解如何考虑第二个列表的内容


[…]定义说,结果列表应包含第一个列表的头作为其头,因此最初结果列表为[a]

就像你提到的,定义说结果列表的头是a,但并不是说整个列表是[a]。此外,此列表不会作为参数传递给递归调用

结果列表定义为[X | Result],因此在本例中,X与a统一。我们还不知道关于结果的任何信息,但我们将其作为第三个参数传递给递归调用。因此,总的来说,这意味着输出将是a,后面是递归调用的输出

b和c的步骤完全相同,因此可以想象堆栈如下:

R = [a|R1]
R1 = [b|R2]
R2 = [c|R3]
或者,变平:[a |[b |[c | R3]]。现在注意顺序是否正确

现在唯一剩下的问题是什么是R3?这里的第一个参数是空列表,所以我们讨论了基本情况。这只是说,如果第一个列表为空,则结果是第二个列表。 所以R3=[3,2,1]。在此之后,堆栈展开并将附加列表作为输出提供给您


[…]定义说,结果列表应包含第一个列表的头作为其头,因此最初结果列表为[a]

就像你提到的,定义说结果列表的头是a,但并不是说整个列表是[a]。此外,此列表不会作为参数传递给递归调用

结果列表定义为[X | Result],因此在本例中,X与a统一。我们还不知道关于结果的任何信息,但我们将其作为第三个参数传递给递归调用。因此,总的来说,这意味着输出将是a,后面是递归调用的输出

b和c的步骤完全相同,因此可以想象堆栈如下:

R = [a|R1]
R1 = [b|R2]
R2 = [c|R3]
或者,变平:[a |[b |[c | R3]]。现在注意顺序是否正确

现在唯一剩下的问题是什么是R3?这里的第一个参数是空列表,所以我们讨论了基本情况。这只是说,如果第一个列表为空,则结果是第二个列表。 所以R3=[3,2,1]。在此之后,堆栈展开并将附加列表作为输出提供给您。

在我看来,这样的操作性阅读将使您远离逻辑编程的真正优势,因为它将使您非常容易从输入和输出的角度思考,就像在函数式编程中一样。这样一种程序性的甚至功能性的解读过于有限,因为它不能公正地对待关系的全部普遍性

此外,正如你们已经注意到的,从操作上阅读这个定义是非常困难的。Prolog的精确调用流程非常复杂,对于初学者和专家来说,一般来说都很难理解

在我看来,思考你的定义的一个好方法是考虑这两个分句,理解它们的含义,引导我们进行一个声明性阅读。 首先,考虑:

append([], List, List).
这仅仅说明了什么是有效的,并且很容易被认为是正确的:如果第一个列表为空,那么第二个列表与第三个列表相同

注意措辞:我们甚至没有提到结果列表,因为所有参数都可以指定或不指定

接下来,考虑第二句:

append([X|List1], List2, [X|Result]) :- append(List1, List2, Result).
按原样阅读:-即←。因此,这表示:

如果appendList1、List2、Result保持不变,那么append[X | List1],List2[X | Result]也保持不变

同样,这很容易被认为是正确的,并且允许在所有方向上都适用的读数

在此,你可以考虑结果是否是第三个论点的好名字,而且,正如@威尼斯正确指出的,即使附加/ 3是否是一个很好的名字来描述这个关系。

在我看来,这样的操作性阅读会使你偏离逻辑编程的真正优势,由于这将使人们极有可能从投入和产出的角度来思考问题,李说 在函数式编程中。这样一种程序性的甚至功能性的解读过于有限,因为它不能公正地对待关系的全部普遍性

此外,正如你们已经注意到的,从操作上阅读这个定义是非常困难的。Prolog的精确调用流程非常复杂,对于初学者和专家来说,一般来说都很难理解

在我看来,思考你的定义的一个好方法是考虑这两个分句,理解它们的含义,引导我们进行一个声明性阅读。 首先,考虑:

append([], List, List).
这仅仅说明了什么是有效的,并且很容易被认为是正确的:如果第一个列表为空,那么第二个列表与第三个列表相同

注意措辞:我们甚至没有提到结果列表,因为所有参数都可以指定或不指定

接下来,考虑第二句:

append([X|List1], List2, [X|Result]) :- append(List1, List2, Result).
按原样阅读:-即←。因此,这表示:

如果appendList1、List2、Result保持不变,那么append[X | List1],List2[X | Result]也保持不变

同样,这很容易被认为是正确的,并且允许在所有方向上都适用的读数


在此,您可以考虑结果是否是第三个参数的好名字,而且,正如@威尼斯正确指出的,即使附加/ 3是否是一个好的名称来描述这个关系。

因为定义是尾递归,所以没有堆栈展开,因为定义是尾部递归,所以没有堆栈展开。。该定义指出,结果列表应包含第一个列表的头作为其头,因此最初结果列表是[a | Result],其中结果尚未实例化。然后,Result被实例化为[b | Result2],因此整个列表将被更多地称为[a,b | Result2]。append/3的定义是递归的,这一事实与此无关。您可以跟踪您的查询,并且可以准确地看到在每个递归级别发生了什么,也许您已经没有什么可问的了。这里重要的是谓词定义了三个参数中的每一个必须包含的内容,以便谓词本身在求值时能够成功。@mat的回答详细解释了这一点,我只是觉得重申不会有什么坏处。定义说结果列表应该包含第一个列表的头作为它的头,所以最初结果列表是[a | Result],其中Result尚未实例化。然后,Result被实例化为[b | Result2],因此整个列表将被更多地称为[a,b | Result2]。append/3的定义是递归的,这一事实与此无关。您可以跟踪您的查询,并且可以准确地看到在每个递归级别发生了什么,也许您已经没有什么可问的了。这里重要的是谓词定义了三个参数中的每一个必须包含的内容,以便谓词本身在求值时能够成功。@mat的回答详细地解释了这一点,我只是觉得重申不会有什么坏处。。。。或者append是否也是关系的好名字,例如append;所以我们可以阅读,[A | B],C,[A | D]被附加,当B,C,D被附加时。谢谢,它被更正了!。。。或者append是否也是关系的好名字,例如append;所以我们可以阅读,[A | B],C,[A | D]被附加,当B,C,D被附加时。谢谢,它被更正了!