Prolog 最吓人的地方,不是它长得不像主流语言。
更吓人的是:程序终止了,速度也还行,答案看起来也对,但它悄悄漏掉了一些本该存在的解。Conrad 那句 “The horror! The horror!” 放在这里很合适。恐怖不在语法,在漏解而不自知。
metalevel.at 的《Prolog Coding Horror》讲的不是语言猎奇。它把 Prolog 常见反模式压成四类:丢失解、全局状态、不纯输出、低级构造。核心判断很硬:别用命令式习惯,把逻辑编程最值钱的关系表达能力拆掉。
四类 horror:问题不在难看,在不可推理
这篇文章最有用的地方,是把坏味道从“我不喜欢这种写法”变成了可检查的工程风险。
| horror | 常见写法 | 真实代价 | 更好的方向 |
|---|---|---|---|
| 丢失解 | !/0、(->)/2、var/1 | 查询成功,但合法解被切掉 | clean data structures、dif/2、if_/3 |
| 全局状态 | assertz/1、retract/1 | 隐式依赖调用顺序,测试和复用变脆 | 参数传递状态、DCG semicontext |
| 不纯输出 | 直接 format/2 打到终端 | 结果不再是可组合的 term,测试困难 | 先描述解,再交给 toplevel 或 DCG 输出 |
| 低级构造 | is/2、=:=/2、>/2 | 初学者被拖进实例化顺序和运行细节 | CLP(FD) 约束 |
这里最关键的是第一类:丢失解。
程序报错,至少会拦你一下。漏解不会。它像账本里少记了一笔,页面很干净,数字也能对上某个局部结果,但后面的推理全歪了。
这也是 Prolog 比很多语言更敏感的地方。命令式程序通常追求“给定输入,算出输出”。Prolog 更常见的价值,是把一个谓词当关系用:既能从 N 算 F,也能反过来问哪些 N, F 成立。写法一旦不纯,关系就变成单向通道。
阶乘例子:同一段逻辑,三种命运
原文用 horror_factorial 压住了这个问题。例子很小,但足够说明 Prolog 的坏代码为什么危险。
带 cut 的阶乘,查询最一般形式 horror_factorial(N, F),只返回:
N = 0, F = 1
后面的 1!、2!、3! 不是不存在,也不是算不出来,而是被 cut 切没了。
去掉 cut,也没有立刻变好。低级算术还在,is/2 和 >/2 要求变量先实例化。最一般查询会先得到 N = 0, F = 1,然后撞上 instantiation_error。
换成 CLP(FD) 约束,再去掉 cut,程序才重新像一个关系:
N = 0, F = 1 ; N = 1, F = 1 ; N = 2, F = 2 ; N = 3, F = 6 ; ...
这不是炫技。CLP(FD) 也不是今天冒出来的新热点。原文说它已经广泛可用约 20 年。问题在于,很多人仍把 Prolog 写成“带回溯的 C”。能跑,但关系性被掐断了。
这里也要留一个边界:原文不是说 Prolog 永远不能用 cut、全局数据库或终端输出。工程里有性能、接口和遗留系统的约束。批评的对象是默认滥用:把不纯、非单调、隐式状态当常规工具,再反过来说 Prolog 难维护。
这对两类读者影响最直接。
| 读者 | 该怎么调整 |
|---|---|
| 写过一点 Prolog 的开发者 | 先检查会不会漏解,而不是只看单个查询能不能跑;能用 dif/2、if_/3、CLP(FD) 和显式状态传递,就别急着上 cut、var/1、动态数据库 |
| 关心语言设计和声明式范式的人 | 别只把 Prolog 当古怪语法样本看;重点看它如何把“程序是否可推理”变成工程能力,而不是审美偏好 |
接下来最该观察的,也不是谁写出更花的 Prolog 技巧。更实际的问题是:代码库里哪些谓词还能被当关系使用,哪些已经被副作用、剪枝和低级算术锁死成单向过程。
老派经验,有时只是技术债换了件衣服
我更在意的是这篇文章背后的态度。
很多所谓老派经验,确实救过火。但它也常常把火种埋进系统里。assertz/1、retract/1 很顺手,顺手到状态藏进全局数据库;format/2 很直接,直接到结果不再能被测试组合;!/0 很有效,有效到你不知道它切掉了哪些答案。
这有点像早期铁路公司各铺各的轨距。短期看,每家都能跑车;长期看,连接成本由后来者支付。不完全一样,但结构相似:局部方便,系统偿债。
Prolog 里的低级写法也是这样。老手觉得稳,初学者看见的是一堆运行顺序的暗门。维护者更惨:他不知道一个谓词到底是在声明关系,还是在执行一段带副作用的小程序。
所谓经验,不能只看它救过多少火,也要看它制造了多少烟。
逻辑编程真正该反叛的对象,不是声明式纯度这点基本纪律,而是主流命令式惯性。你选择 Prolog,本来就是为了把“怎么算”退后,把“什么关系成立”放到前面。结果又把全局状态、剪枝、打印副作用和低级算术塞回来,那就等于买了显微镜,却拿它砸钉子。
这篇文章最好的地方,是把“纯”从道德词拉回工程词。纯不是洁癖,是可推理性。单调不是学院派,是新增条件时,旧答案不会被某个隐藏控制流偷偷抹掉。
回到开头那个恐怖点。Prolog 的怪,不该拿来当免罪牌。真正的分水岭很简单:你的程序是在描述关系,还是只是在假装关系。
