避免 Prolog 的“恐怖”:纯声明式编程指南
Prolog 在现代编程领域常被视为一种叛逆的选择。对于拥抱它的人来说,这种语言提供了一种通过关注解决方案是什么而非如何计算它来解决问题的方法。然而,许多程序员——尤其是初学者——会陷入一些陷阱,这些陷阱剥夺了 Prolog 最强大的优势,将一个强大的逻辑引擎变成了一个脆弱的命令式风格脚本。
要编写真正有效的 Prolog,必须避免特定的“恐怖”:即导致程序缺陷、隐式依赖以及丧失语言固有通用性的反模式。通过坚持使用语言的纯粹、单调子集,开发者可以创建出不仅正确而且足够灵活以处理最通用查询的程序。
第一种恐怖:丢失解
一个程序可能以两种主要方式出现缺陷:它可能报告错误的答案,或者它可能无法报告预期的解。虽然报告错误答案是有问题的,但无法报告有效的解通常更为隐蔽,因为它限制了程序作为关系的效用。
“丢失解”的主要罪魁祸首是不纯粹且非单调的结构。这些包括:
- Cut 运算符 (
!/0) - If-then-else 结构 (
(->)/2) var/1谓词
当使用这些结构时,程序就不再是纯粹的逻辑描述。声明式的替代方案是使用干净的数据结构、约束(例如 dif/2)以及专门的元谓词(如 if_/3)来维护程序的逻辑完整性。
第二种恐怖:全局状态
初学者往往倾向于像对待命令式语言一样对待 Prolog,通过使用 assertz/1 和 retract/1 等谓词来修改全局数据库。这引入了隐式依赖:程序的行为取决于谓词调用的顺序,但这种依赖并没有在逻辑中显式编码。
如果这些谓词以非预期的顺序执行,程序可能会发生意外失败或产生错误的结果。解决方案是完全避免全局状态。相反,应使用谓词参数或半上下文表示法(semicontext notation)将状态“穿透”程序,使依赖关系变得显式且可维护。
第三种恐怖:不纯的输出
另一个常见的错误是直接将答案打印到系统终端(例如,使用 format/2),而不是允许 Prolog 的顶层(toplevel)来报告它们。
% The Impure Way
solve :-
solution(S),
format("the solution is: ~q\n", [S]).
这种方法有严重的局限性,因为输出仅作为终端上的一个字符串存在,而不是作为一个 Prolog 项(term)。这使得为输出编写自动化测试用例几乎变得不可能,并阻止了代码被用作真正的关系。正确的做法是描述解,并让顶层处理展示:
% The Pure Way
solution(S) :-
constraint_1(S),
etc.
第四种恐怖:低级语言结构
许多程序员坚持使用低级算术谓词,如 is/2、=:=/2 和 >/2。虽然这些谓词已服务于行业数十年,但它们要求程序员同时管理声明式和操作语义,这增加了认知负荷,并使语言更难学习和教授。
这些结构将关系视为函数,这是对 Prolog 本质的根本误解。现代的替代方案是使用有限域上的约束逻辑编程(CLP(FD)),它允许对整数算术进行更声明式的处理。
案例研究:恐怖的阶乘
为了说明这些点,请考虑一个典型的“恐怖”阶乘实现:
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).),cut (!) 会阻止程序找到除 N=0, F=1 以外的任何解。如果你移除 cut,程序仍然会因为 instantiation_error 而失败,因为 is/2 要求其参数必须已实例化。
通过转向使用 CLP(FD) 的纯粹、声明式版本,程序变得真正通用:
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 和 F 的所有可能组合(0,1; 1,1; 2,2; 3,6; 等等),从而将代码从一个狭隘的函数转变为一个强大的数学关系。
综合与观点
虽然追求纯粹性在逻辑上是成立的,但在从业者之间经常引发辩论。一些人认为这些警告是“夸大其词”,认为对于特定的实际应用,命令式快捷方式可能是可以接受的的。其他人则指出,理解 Prolog 需要对操作模型(例如四端口模型)有更深入的把握,,从而才能真正理解为什么这些反模式是成问题的。
社区讨论中一个有趣的发现是,意识到在某些语境下,报告一些稍后可以过滤的“错误”答案比完全错过一个正确的答案更具可接受性——这一观点与 NP 完全问题的挑战相一致,即寻找任何一个有效的解往往就是目标。
总结
探索 Prolog 编程中的常见反模式,并学习如何从不纯粹、低级结构转向纯粹的声明式风格,从而最大限度地发挥逻辑编程的力量。