Prolog一个颇有用的特征就是可让使用者归纳地描述事物,对其进行抽象。好比咱们若是想描述Vincent喜欢汉堡,能够这么写:编程
enjoys(vincent, X) :- burger(X).
可是在现实中总会存在例外。也许Vincent不喜欢Big Kahuna汉堡。即,正确的规则是:Vincent喜欢汉堡,除了Big Kahuna汉堡。好了,咱们如何在Prolog中描述呢?安全
做为第一步,先介绍另外一个Prolog内置的谓词:fail/0。正如它的名字所示,fail/0是一个当Prolog运行到这个目标时会当即失败的特殊标识。这可能听上去没有什么用处,可是请记住,若是Prolog失败了,它会尝试回溯。因此fail/0能够看做一个强制回溯的指令。并且在和中断一块儿使用时,因为中断会阻止回溯,这会让咱们写出一些有趣的程序,特别是,它能够定义通用规则中的一些异常和特殊的状况。工具
思考下面的代码:性能
enjoys(vincent, X) :- big_kahuna_burger(X), !, fail. enjoys(vincent, X) :- burger(X). burger(X) :- big_mac(X). burger(X) :- big_kahuna_burger(X). burger(X) :- whopper(X). big_mac(a). big_kahuna_burger(b). big_mac(c). whopper(d).
前两行代码描述了Vincent的喜爱。后六行代码描述了汉堡的类型和具体四个汉堡:a, b, c, d。假设前两行真实地描述了Vincent的喜爱(即,他喜爱全部的汉堡除了Big Kahuna汉堡),那么他应该喜爱汉堡a,c和d,但没有b。事实上,这是正确的:学习
?- enjoys(vincent, a). true ?- enjoys(vincent, b). false ?- enjoys(vincent, c). true ?- enjoys(vincent, d). true
这是如何起做用的?关键在于第一行代码中!和fail/0的组合使用(这个甚至有一个名字:称为“中断-失败”组合)。当咱们进行查询enjoys(vincent, b)时,会首先使用第一个规则,而后到达中断。这会提交咱们已经作出的选择,并且特别须要说明的是,会阻止使用(回溯)第二个规则。可是随后就会到达fail/0,。这会强制尝试回溯,可是中断阻止了回溯,因此查询会失败。code
这颇有趣,可是不够理想。首先,注意规则的顺序是关键:若是咱们调换了前两行的顺序,就得不到想要的结果了。相似地,中断也是关键:若是咱们移除它,程序也不会按照相同的方式运行(因此这是一个红色中断)。简而言之,咱们获得的是两个互相依赖的子句。从这个例子出发,若是咱们可以从中提取一些有用的部分,而且包装为更健壮的通用方式会更好。变量
确实能够这么作。关键点在于第一个子句是从本质上描述了Vincent不喜欢X若是X是一个Big Kahuna汉堡。即,“中断-失败”组合看上去就是某种形式的__否认__。事实上,这就是关键的抽象:“中断-失败”组合让咱们定义了某种形式的否认称为:使用否认做为失败断定。下面是实现:程序
neg(Goal) :- Goal, !, fail. neg(Goal).
对于任意Prolog目标,neg(Goal)将会为真当Goal为假时。总结
使用新定义的谓词neg/1,咱们能够更清晰地描述Vincent的喜爱:查询
enjoys(vincent, X) :- burger(X), neg(big_kahuna_burger(X)).
即,Vincent喜欢X若是X是一个汉堡并且X不是Big Kahuna汉堡。这很接近咱们原始的描述:Vicent喜欢汉堡,除了Big Kahuna汉堡。
使用否认做为失败断定是一个重要的工具。不只仅在于它提供了有用的表述性(描述异常状况的能力),更在于它提供了相对安全的方式。经过使用否认进行失败断定(而不是低层次的中断-失败组合形式),咱们能够更好地进行失败断定,避免使用红色中断而致使的一些错误。事实上,否认做为失败断定十分的有用,以致于成为了标准Prolog的内置实现,因此咱们不用再定义它了。在标准的Prolog实现中,操做符+就是否认做为失败断定,因此咱们能够从新定义Vincent的喜爱:
enjoys(vincent, X) :- burger(X), \+ big_kahuan_burger(X).
可是,有一些使用否认做为失败断定的建议:不要认为否认做为失败断定就是逻辑否。它并非,思考下面的“汉堡”世界:
burger(X) :- big_mac(X). burger(X) :- big_kahuna_burger(X). burger(X) :- whopper(X). big_mac(a). big_kahuna_burger(b). big_mac(c). whopper(d).
若是咱们进行查询enjoys(vincent, X),获得正确的回答:
?- enjoys(vincent, X). X = a; X = c; X = d; false
可是假设咱们重写了第一行代码实现:
enjoys(vincent, X) := \+ big_kahuna_burger(X), burger(X).
注意从声明性来看,这里没有什么不一样:毕竟,burger(X)和不是big kahuna burger(X)在逻辑上等同于:不是big kahuna burger(X)和burger(X)。然而,下面是咱们进行相同的查询获得的结果:
?- enjoys(vincent, X). false
发生了什么?在更新后的知识库中,Prolog首先会判断+ big_kahuna_burger(X)是否成立,这意味着必须证实big_kahuna_burger(X)失败。可是这是可以成功的。由于,知识库中有包含big_kahuna_burger(b)这个事实。因此查询 + big_kahuna_burger(X)会失败,同时致使原始查询也会失败。在内核中,两个程序关键的不一样在于原来的版本(可以正常工做的)中,咱们在将变量X初始化后再使用的+,在新的版本中,咱们在变量初始化前就使用了+,这就是关键的不一样。
总结一下,使用否认做为失败断定并不等于逻辑否认,咱们必须理解其程序维度上的含义。然而,这是一个重要的编程思路:一般状况下,使用否认做为失败断定会优于直接使用红色中断的程序。可是,“一般”并不意味着“老是”。有些特殊的时候,使用红色中断会更好一些。
好比,假设咱们须要写出代码如何以下逻辑:若是a和b都成立,或者a不成立可是c成立,那么p成立。在否认做为失败断定的帮助下,咱们可以写出的代码以下:
p :- a, b. p :- \+ a, c.
可是设想若是a是一个很复杂的目标,须要很长时间的计算。上面这样的程序意味着咱们须要计算a两次,这一般会致使不能接受的性能问题。若是是那样,可能使用以下的程序会更好:
p :- a, !, b. p :- c.
注意这里是一个红色中断:移除它会改变程序的行为。
关于否认做为失败断定的介绍到此为止,这里没有广泛适用的原则能够覆盖到全部的状况。编程更像是科学的艺术:这使得它更加有趣。你须要尽量熟悉你学习的语言的一切(不管是Prolog, Java, Perl仍是其余的任何语言),理解须要解决的问题,找到合适的解决方案。而后:尽你所能地尝试和完善!