Prolog的附加有什么问题?

Sky*_*yfe 6 append prolog

根据我大学的逻辑课程,我们可以预期Prolog定义的结果与以下查询不同:

append([], a, X)
Run Code Online (Sandbox Code Playgroud)

(统一为X=a).

但是我不明白他们的目标是什么?应该期望什么作为有效的响应,因为append应该统一X(在这个例子中)[]a?的串联?

我认为他们可能期待回归false[a]; 然而,我想应该是级联的结果a[],而不是[]a(因为[]是的尾部[a]).

mat*_*mat 7

这里的关键是,我们预计append/3持有用于名单.

在你展示的查询,a不是一个名单,但append/3 仍然  成立.

因此,这种关系实际上比我们最初期望的更为普遍:它也适用于其他情况!

之所以如此,可以很快从传统定义的第一个条款append/3:

append([], Bs, Bs).

仅此子句已使查询成功!没有额外的纯子句可以阻止这一点.因此,如果我们希望关系保留列表,则必须限制此子句.这意味着,我们必须对第二个参数设置一个约束,我们通过在子句的主体中声明它来做:

append([], Bs, Bs) :- ... (left as an exercise)

这显然是有代价的:性能.

因此,这里的权衡取决于性能和精度.在Prolog中,我们经常接受这样的权衡,因为我们使用预期的术语隐含地使用这些谓词.另一方面,对于许多谓词,如果未使用预期类型调用域错误类型错误,我们希望受益.

  • 这是朝着正确方向迈出的一步,但仍然过于笼统.例如,使用该子句,查询`? - append([],[a | b],Cs).`**以`Cs = [a | b]`成功**,但是`[a | b] `也不是**列表!同样,这表明你必须*改变*这个非常条款:没有额外的纯子句可以防止这种过于笼统的情况,现在仍然是程序的结果. (2认同)

fal*_*lse 5

您的课程针对Prolog编程的非常重要的一点。

手册对于append/3谓词和类似谓词的精确定义常常是草率的。实际上,完整的定义是如此复杂,以至于通常只希望定义实际关系的一部分。考虑一下Prolog序言中的第一个定义:

append(Xs, Ys, Zs)如果Zs是列表Xs和的串联,则为true Ys

注意如果。因此,该定义给出了关系保持但没有明确排除其他情况的情况。为了排除其他情况,应改为iff。提到的情况(我们在谈论列表)是谓词的预期用途。那么,现在可能还包括哪些情况?前提(参数为列表)不成立的情况。

考虑append/3用“ iff”代替“ if”的定义:

append([], Xs, Xs) :-
   list(Xs).
append([X|Xs], Ys, [X|Zs]) :-
   append(Xs, Ys, Zs).

list([]).
list([X|Xs]) :-
   list(Xs).
Run Code Online (Sandbox Code Playgroud)

现在,添加两个列表的费用为| Xs| + | Ys|。与|相比,这是相当大的开销。Xs| 单独。

但是情况更糟。考虑查询:

?- append([1,2], Ys, Zs).
;  Ys = [], Zs = [1,2]
;  Ys = [_A], Zs = [1,2,_A]
;  Ys = [_A,_B], Zs = [1,2,_A,_B]
...
Run Code Online (Sandbox Code Playgroud)

因此,我们可以无限查询此查询。将此与通常的定义进行对比:

?- append([1,2], Ys, Zs).
   Zs = [1,2|Ys].
Run Code Online (Sandbox Code Playgroud)

只有一个答案!它包含所有列表的所有答案以及您观察到的一些奇怪的情况。因此,append的常规定义具有更好的终止属性。实际上,如果第一个或第三个参数是已知长度1的列表,它将终止。

请注意,答案包含Ys。以这种方式,无数个答案可以分解为一个答案。这实际上就是逻辑变量的力量!我们可以用有限的方法来表示许多解决方案。要付出的代价是一些可能导致编程错误的额外解决方案2。因此需要采取一些预防措施。


1它也终止于某些更晦涩的情况,例如append([a|_],_,[b|_])

2 append([a], Zs, Zs).(在许多系统中)也会产生一个答案。


Wil*_*sem 3

但我不明白他们的目的是什么?

如果不问他们,就不可能确切地知道他们的目标。

尽管如此,我认为他们的目的是表明Prolog(或多或少)是非类型化的append/3记录为:

append(?List1, ?List2, ?List1AndList2)

   List1AndList2List1是和的串联List2

很明显,人们期望这三个参数是列表而a不是列表。a不是[]和的串联a,因为人们会认为两者不可“串联”。

现在这仍然成功,因为append/3通常实现为:

append([],T,T).
append([H|T],T2,[H|R]) :-
    append(T,T2,R).
Run Code Online (Sandbox Code Playgroud)

因此,如果您给出它append([],a,X).,它将简单地与第一个子句统一并统一X = a

同样的“奇怪”行为也会发生在append([14],a,X). 这里X = [14|a]也不是一个列表。这是因为 Prolog 解释器不“知道”它正在处理列表。Prolog 与[A|B]任何其他函子一样。

处理此问题的更“类型安全”的方法可能是

append([],[],[]).
append([H|T],T2,[H|R]) :-
    append(T,T2,R).
append([],[H|T],[H|R]) :-
    append([],T,R).
Run Code Online (Sandbox Code Playgroud)

或者更优雅地:

list([]).
list([_|T]) :-
    list(T).

append([],T,T) :-
    list(T).
append([H|T],T2,[H|R]) :-
    append(T,T2,R).
Run Code Online (Sandbox Code Playgroud)

因为在这里我们检查第二个参数是否是一个列表。然而,缺点是现在我们将append/3O(m+n)中, m是第一个列表的长度,n是第二个列表的长度,而在原始代码中只需要O(m)时间。此外请注意,Prolog 在解析时不会引发警告/错误。仅当您查询这些时,它才会无法附加[]a

不检查类型会导致当您将程序提供给解释器时,您对程序是否编译/不引发错误的保证较少。这可能是一件好事,但问题可能是您以他们不期望的方式调用某些谓词,这最终可能会引发错误。这就是有时使用静态类型语言的原因:它们“保证”(至少在某种程度上)如果您调用该问题,则不会发生此类错误。当然,这并不意味着程序不会在其他事情上出错(或者根本没有意义)。例如,

(++) [] t2 = t2
(++) (h:t) t2 = h:((++) t t2)
Run Code Online (Sandbox Code Playgroud)

定义“或多或少”相同,但 Haskell 会推导出 的类型(++)(++) :: [a] -> [a] -> [a]。因为它知道每个函数的输入和输出的类型,所以它可以对其执行微积分,因此在编译(++)时,如果您给出与列表不同的内容,它将引发错误。

这是否是一件好事当然是一个不同的问题:动态类型编程语言是故意这样设计的,因为它允许更大的灵活性。