在特定位置插入元素

insert Element at specific position

我开始使用 Prolog(只是为了我自己),但我正在为递归而苦苦挣扎。

我想要一个 "method",它可以在列表中的特定位置插入一个元素。 到目前为止我尝试的是:

insertAt(Element,Position,List,ResultList)

insertAt(Element,0,L,[Element|L]).
insertAt(Element,Pos,[E|L],ZL):-
    Pos1 is Pos-1,
    insertAt(Element,Pos1,L,ZL),
    append(E,ZL1,ZL).

我觉得我很复杂,因为我无法理解递归究竟是如何工作的...

也许有人可以帮助我。

您的代码有几个特点让初学者难以理解。特别是,在以有趣(实际上也是严肃)的方式与程序交互时,使用模式化的低级算术是一个严重的障碍。

例如,要了解关系,从最一般的查询开始很有用。这只问 "Is there any solution at all, and if so, what does a solution look like?"。在您的具体示例中,最一般的查询如下所示:

?- insertAt(E, Pos, Ls0, Ls).

由于您使用的是非声明性算术谓词,这几乎立即产生 instantiation error

?- insertAt(E, Pos, Ls0, Ls).
Pos = 0,
Ls = [E|Ls0] ;
ERROR: insertAt/4: Arguments are not sufficiently instantiated

此外,您使用命令式名称 ("insert...") 阻碍了良好的声明式阅读。这使得培养对关系编程的感觉变得不必要地困难。

因此,我建议您:(1) 使用更具声明性的谓词名称,以及 (2) 使用自然数的符号表示,使谓词更易于理解和更通用。

我将使用数字 0 来表示零,用 s(X) 来表示数字 X 的后继。有关此表示的更多信息,请参阅

进行这些更改后,代码变为:

element_at(E, 0, [_|Ls], [E|Ls]).
element_at(E, s(X), [L|Ls0], [L|Ls]) :-
        element_at(E, X, Ls0, Ls).

为了理解这个程序,我们声明式地读它:第一个子句是true if位置是0,最终列表的头部是 E,尾部......等等。第二个子句是 true if element_at(E, X, Ls0, Ls) holds, head of ...等

很好,最通用的查询现在工作得更好了:

?- element_at(E, Pos, Ls0, Ls).
Pos = 0,
Ls0 = [_G1071|_G1072],
Ls = [E|_G1072] ;
Pos = s(0),
Ls0 = [_G1073, _G1079|_G1080],
Ls = [_G1073, E|_G1080] ;
Pos = s(s(0)),
Ls0 = [_G1073, _G1081, _G1087|_G1088],
Ls = [_G1073, _G1081, E|_G1088] .

请注意这里有一些不公平的地方:剩余位置的答案在哪里?为了更公平的枚举,我们使用length/2,预先说明我们正在考虑的列表的长度:

?- length(Ls0, _), element_at(E, Pos, Ls0, Ls).
Ls0 = [_G1124],
Pos = 0,
Ls = [E] ;
Ls0 = [_G1124, _G1127],
Pos = 0,
Ls = [E, _G1127] ;
Ls0 = [_G1124, _G1127],
Pos = s(0),
Ls = [_G1124, E] .

现在更清楚了不同参数如何相互作用,因为您已经看到谓词描述的术语的各种示例。


事实上,为了减少我们需要跟踪的参数和变量名的数量,我在描述列表时经常使用DCG notation,我也想向您展示这个替代版本:

element_at(Element, 0, [_|Ls]) -->
        [Element],
        list(Ls).
element_at(Element, s(X), [L|Ls]) -->
        [L],
        element_at(Element, X, Ls).

list([]) --> [].
list([L|Ls]) --> [L], list(Ls).
?- length(Ls0, _), phrase(element_at(E, Pos, Ls0), Ls).
Ls0 = [_G1148],
Pos = 0,
Ls = [E] ;
Ls0 = [_G1148, _G1151],
Pos = 0,
Ls = [E, _G1151] ;
Ls0 = [_G1148, _G1151],
Pos = s(0),
Ls = [_G1148, E] .

一旦您阅读了 符号,您就会清楚这个版本。


最后,您可能会说 "Well, that's nice, but s(X) notation still seems quite strange",并且您可能希望在程序中对整数使用更广泛使用的印度-阿拉伯语表示法。

为此,我们可以简单地采用上面的任一版本,并用具有 CLP(FD) 约束的声明性整数算法替换 s(X) 符号。例如,第一个版本:

:- use_module(library(clpfd)).

element_at(E, 0, [_|Ls], [E|Ls]).
element_at(E, X, [L|Ls0], [L|Ls]) :-
        X #> 0,
        X #= X0 + 1,
        element_at(E, X0, Ls0, Ls).

这也适用于所有方向,正如我们对声明性一般的谓词所期望的那样:

?- length(Ls0, _), element_at(E, Pos, Ls0, Ls).
Ls0 = [_G2095],
Pos = 0,
Ls = [E] ;
Ls0 = [_G2095, _G2098],
Pos = 0,
Ls = [E, _G2098] ;
Ls0 = [_G2095, _G2098],
Pos = 1,
Ls = [_G2095, E] .

请参阅 了解更多信息,我希望这篇 post 能鼓励您更相关地思考您的 Prolog 代码,从各个方面尝试它,并以声明的方式阅读它。 (描述的是什么?)

Prolog 的一个特性是模式匹配,即基于谓词参数的规则选择。这种特性是 Prolog 符号的关键,允许对关系进行紧凑的描述,特别是对于递归术语,如列表。请注意,列表只是 'syntactic sugar' 用于递归术语,带有常规函子(术语名称,在日常用语中)。

insertAt(Element,0,L,[Element|L]). % ok
insertAt(Element,Pos,[E|L],[E|ZL]):- % you forgot to cons back E
    Pos1 is Pos-1,
    insertAt(Element,Pos1,L,ZL). % done, append is useless
    %append(E,ZL1,ZL).

SWI-Prolog有nth1/4和nth0/4,可以执行插入:

?- nth0(1,L,x,[1,2,3]).
L = [1, x, 2, 3].

same_length/2, append/3, and length/2处理递归!

insertAt(E,N,Xs,Ys) :-
   same_length([E|Xs],Ys),
   append(Before,Xs0,Xs),
   length(Before,N),
   append(Before,[E|Xs0],Ys).

示例查询:

?- insertAt(X, N, [a,b,c,d,e], Ys).
(  N = 0, Ys = [X,a,b,c,d,e]
;  N = 1, Ys = [a,X,b,c,d,e]
;  N = 2, Ys = [a,b,X,c,d,e]
;  N = 3, Ys = [a,b,c,X,d,e]
;  N = 4, Ys = [a,b,c,d,X,e]
;  N = 5, Ys = [a,b,c,d,e,X]
;  false
).