Prolog 开发最容易踩的 4 个坑
Prolog 写得不对?新手最容易踩的四个坑
Prolog 的味道和其他语言很不一样。别人都在用命令式或面向对象的方式写代码,它却让你用“声明式逻辑”去思考问题。刚开始觉得挺有意思,但写到后面容易出问题。
其实只要避开几个常见误区,写出来的程序就能更稳定,也更容易测试。下面就来说说 Prolog 开发里最容易犯的四类错误。
第一个坑:用“剪切”把解法掐死
你写了一个谓词,测几个具体值都没问题。但真正用起来,有人用更通用的方式去查,却发现该有的答案全没了。
问题往往出在 !/0(cut)、->/2(if-then-else)和 var/1 这类“不干净”的操作上。它们为了图省事,把本来可以回溯的解法强行砍掉,结果程序只剩下一条路可走。
% 坏例子:用 cut 去“优化”
factorial(0, 1) :- !.
factorial(N, F) :-
N > 0,
N1 is N - 1,
factorial(N1, F1),
F is N * F1.
上面这段代码在查询 ?- factorial(N, F). 时,只会给出 N = 0, F = 1,后面就没结果了。可实际上还有很多组合都应该成立。
更好的做法是:用 dif/2 这类约束谓词,或者借助高阶谓词,让程序保持通用性。
第二个坑:用 assertz 和 retract 修改全局状态
很多人在刚学 Prolog 时,会觉得 assertz/1 和 retract/1 很神奇——可以直接在运行时修改知识库,感觉功能强大。
但这也带来了隐形的依赖。程序的执行顺序一旦不对,结果就会莫名其妙。状态藏在全局数据库里,看不见、摸不着,测试的时候特别容易互相干扰。
正确的做法是:把状态通过参数显式地传递,而不是偷偷改全局。
第三个坑:把输出直接写进逻辑里
你可能见过这样的代码:
solve_and_print :-
solution(S),
format("The solution is: ~q~n", [S]).
表面看功能正常,但你没法测试,也没法把结果当成数据再用。输出只存在于屏幕上,程序本身却拿不到。
解法很简单:逻辑和展示分开。让谓词只负责描述解法,顶层再负责输出。如果需要特殊格式,可以用 DCG 来声明式地描述。
第四个坑:死守老的算术谓词
虽然现在 Prolog 已经支持 CLP(FD) 好几年了,但还是有人习惯用 is/2、=:=/2 和 >/2 去处理数字。
这些低级操作让代码更像“过程”,而非“声明”。初学者读起来会更吃力,也更容易写出只在特定条件下工作的程序。
改用 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、依赖低级算术、缺乏通用性。
怎么写出更好的 Prolog 代码
想要程序更稳,记住这几条原则:
- 用声明式的方式写,而不是命令式的小技巧。
- 状态通过参数传,不要改全局数据库。
- 逻辑和展示分开,让谓词只描述解法。
- 优先使用 CLP(FD) 等现代工具。
这样写出来的代码,能回答最通用的查询,也更容易维护和测试。Prolog 的优势,也只有在这种情况下才能真正发挥出来。