Prolog编程的陷阱

Hacker News Top 新闻

摘要

关于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 — Computer Things

Hillel Wayne的一篇博文,详细讲述了他对Prolog编程语言的不满,包括字符串问题、缺乏函数、数据类型有限以及cut操作等。

用宝可梦解释 Prolog 基础

Lobsters Hottest

通过宝可梦属性相克作为示例,介绍 Prolog 编程,展示逻辑编程如何优雅地建模关系数据。

状态思维

Lobsters Hottest

本文解释了从命令式编程转向声明式编程所需的概念转变,并通过Prolog来阐述如何从关系而非可变状态的角度进行思考。

谓词逻辑速成课程

Lobsters Hottest

一篇为程序员提供谓词逻辑速成课程的博客文章,解释谓词、布尔运算符和语法,使形式逻辑更易理解。