Prolog编程的陷阱
摘要
关于Prolog编程中常见陷阱的指南,强调使用纯声明式构造而非不纯的构造,如cut、全局状态和低级I/O。
暂无内容
查看缓存全文
缓存时间: 2026/05/18 00:51
# Prolog 编程恐怖
来源:https://www.metalevel.at/prolog/horror
我仿佛听到了那低语般的呐喊:“恐怖!恐怖!”(约瑟夫·康拉德,《黑暗的心》)
## 你为何在此?
作为 Prolog 程序员,你很可能有种叛逆的倾向。在很多情况下,正是这种倾向引导你远离当今整个行业解决主流问题的方式,去关注那些*超越*常规的东西。本页的目的是向你展示,在哪些情况下追随这种倾向很可能*不*是个好主意——因为代价高昂,且毫无益处。
只需少数几条规则就能写出优秀的 Prolog 代码。违反这些规则会导致程序在一个或多个方面存在缺陷。
*视频*:Prolog 反模式 (https://www.metalevel.at/prolog/videos/antipatterns)
## 恐怖:丢失解
一个*终止*且效率可接受的 Prolog 程序可能在两个主要方面存在*缺陷*:
1. 它报告了*错误*的答案。
2. 它*未能报告*预期的解。
这两种情况哪种更糟糕?好好想想!假设一个程序*仅仅*在第一种方式上有缺陷。你是否还能做些什么来只获得正确的结果?那么,假设一个程序*仅仅*在第二种方式上有缺陷。你又有什么办法能设法获得所有预期的解呢?
让程序在*第二种*方式上出现缺陷的主要手段是使用*不纯*和*非单调*的语言结构。例子包括 `!/0`、`(->)/2` 和 `var/1`。摆脱这种局面的声明式方法是使用*干净* (https://www.metalevel.at/prolog/data#clean) 的数据结构、像 `dif/2` (https://www.metalevel.at/prolog/concepts#builtins) 这样的*约束*,以及像 **if_/3** (https://www.metalevel.at/prolog/metapredicates#if_3) 这样的元谓词。
## 恐怖:全局状态
作为初学者,你可能会倾向于修改 Prolog 的*全局数据库* (https://www.metalevel.at/prolog/global)。这会在你的程序中引入*隐含依赖*。所谓“隐含”,是指你的程序中没有*强制*这些依赖关系的东西。例如,如果你以不同于预期的顺序使用这类谓词,它们可能会意外失败或产生奇怪的结果。
让程序以这种方式出现缺陷的主要手段是使用像 `assertz/1` 和 `retract/1` 这样的谓词。声明式的出路是使用谓词*参数*或*半上下文表示法* (https://www.metalevel.at/prolog/dcg#semicontext) 来传递状态。
## 恐怖:不纯的输出
作为初学者,你有时可能会倾向于将答案*打印*到系统终端,而不是让顶层报告它们。例如,你的程序可能包含这样的代码:
```
solve :-
solution(S),
format("the solution is: ~q\n", [S]).
```
这种方法的一个主要缺点是*你很难对这种输出进行推理*,因为它只出现在系统终端上,在程序内部无法作为 Prolog 项使用。因此,你不会为这种输出编写测试用例,从而增加了引入更改破坏这类谓词的可能性。另一个严重缺陷是,这阻止你将代码用作真正的*关系*。
为了从关系的完全通用性中获益,请*用 Prolog 代码描述*一个解,然后*让顶层来负责打印*:
```
solution(S) :-
constraint_1(S),
etc.
```
有时,你可能需要特殊的格式化。在这种情况下,你仍然可以以纯方式描述输出,例如使用非终结符 **format\_//2** (https://www.metalevel.at/prolog/dcg#output)。这使得编写测试用例变得容易。
## 恐怖:低级语言结构
有些 Prolog 程序员可能认为没有必要使用较新的语言结构。例如,CLP(FD) 约束广泛可用才大约 20 年,这对 Prolog 来说算是*相对*近期的发展。如果你认为低级结构已经很好地服务了你,何必费心学习新东西?数百万学生*并没有*因低级结构而受益这一事实也不必困扰你。
不幸的是,坚持使用低级结构代价高昂:它使语言变得比必要情况下更难教、更难学、更难理解。它要求学生*同时*学习声明式语义和操作语义,这在几乎所有情况下都信息量过大。
让 Prolog 比必要情况下更难教的主要手段是向初学者介绍低级算术谓词,如 `(is)/2`、`(=:=)/2` 和 `(>)/2`。声明式的出路是*改用约束来教学*。参见 **声明式整数算术** (https://www.metalevel.at/prolog/clpfd)。
## 恐怖阶乘
为了看看上述缺陷的具体体现,请目睹***恐怖阶乘***:
```
horror_factorial(0, 1) :- !.
horror_factorial(N, F) :-
N > 0,
N1 is N - 1,
horror_factorial(N1, F1),
F is N*F1.
```
注意在*最通用查询* (https://www.metalevel.at/prolog/concepts#toplevel) 时*丢失解*的恐怖:
```
?- horror_factorial(N, F).
N = 0, F = 1.
```
没有 `!/0` 的版本几乎同样可怕:
```
horror_factorial(0, 1).
horror_factorial(N, F) :-
N > 0,
N1 is N - 1,
horror_factorial(N1, F1),
F is N*F1.
```
低级语言结构的恐怖弥漫其中:
```
?- horror_factorial(N, F).
N = 0, F = 1
; caught: error(instantiation_error,'(is)'/2)
```
如果你接受这一点,你就:
- 被过时的语言结构所限制。
- 将关系误认为是函数。
- 不关心最通用查询。
- 通过使用不纯结构阻止*声明式调试* (https://www.metalevel.at/prolog/debugging)。
## 出路:纯度
为了停止恐怖,坚持 Prolog 的*纯* (https://www.metalevel.at/prolog/purity) 单调子集。
从小处着手。例如,用更声明式的替代方案代替低级整数算术:
```
horror_factorial(0, 1) :- !.
horror_factorial(N, F) :-
N #> 0,
N1 #= N - 1,
horror_factorial(N1, F1),
F #= N*F1.
```
这仍然比什么都没有好。然后,移除 `!/0`:
```
n_factorial(0, 1).
n_factorial(N, F) :-
N #> 0,
N1 #= N - 1,
n_factorial(N1, F1),
F #= N*F1.
```
这个版本也适用于*最通用*查询:
```
?- n_factorial(N, F).
N = 0, F = 1
; N = 1, F = 1
; N = 2, F = 2
; N = 3, F = 6
; ... .
```
相当不错:一些简单的更改已经得到了一个相当通用的逻辑程序。
## 结论
总结一下,我建议你*在合理的地方叛逆*,并且*只在*那里叛逆。固守过时的特性是方向错误的叛逆,因为生活不会倒退,也不会停留在昨天。
在你的 Prolog 程序中使用声明式结构,让它们更通用,同时保持可接受的性能。
**更多关于 Prolog 的内容** (https://www.metalevel.at/prolog)
**主页** (https://www.metalevel.at/)
相似文章
我对Prolog的不满
Hillel Wayne的一篇博文,详细讲述了他对Prolog编程语言的不满,包括字符串问题、缺乏函数、数据类型有限以及cut操作等。
用宝可梦解释 Prolog 基础
通过宝可梦属性相克作为示例,介绍 Prolog 编程,展示逻辑编程如何优雅地建模关系数据。
Prolog的诞生(1996)
一篇1996年回顾Prolog编程语言起源与发展的文章。
状态思维
本文解释了从命令式编程转向声明式编程所需的概念转变,并通过Prolog来阐述如何从关系而非可变状态的角度进行思考。
谓词逻辑速成课程
一篇为程序员提供谓词逻辑速成课程的博客文章,解释谓词、布尔运算符和语法,使形式逻辑更易理解。