1. 模块化
学完这章,你能用 module/2 把 Prolog 代码拆成模块、发布为 swipl pack,再也不会在一个文件里找不到谓词了。 前置:Hello world | 难度:★★ | 后续:web server
你一个文件写了多少行了?100?200?Ctrl+F 找个谓词都要翻半天,改一个地方不敢动怕影响其他地方——这感觉我太熟悉了。
学完这章,你能用 module/2 把代码拆成模块,用 use_module/1 组装起来,甚至发布成 pack 给全世界人用。
1.1. 从一团乱麻开始
先看一个典型场景。你写了一个几何计算的小工具:
%% geometry.pl
area(circle, R, A) :- A is pi * R * R.
area(rect, (W, H), A) :- A is W * H.
perimeter(circle, R, P) :- P is 2 * pi * R.
perimeter(rect, (W, H), P) :- P is 2 * (W + H).
%% 后面还有 20 个谓词...
然后你写了一个 main 程序要用它:
%% main.pl
area(circle, 5, A), % 等等,这个 area 是哪来的?
项目大了,问题就来了:
- 命名冲突——两个文件都定义了
area/2怎么办? - 不知道哪个谓词是公开接口,哪个是内部实现
- 想复用代码只能复制粘贴
这是所有脚本语言的通病。Prolog 的解决方案?模块。
1.2. module/2 —— 划地盘
文件开头加一行:
%% geometry.pl
:- module(geometry, [area/3, perimeter/3]).
module 是标准的 Prolog directive。这一行做了两件事:
- 声明模块名 — 这个文件属于
geometry模块 - 定义导出列表 — 外面能用的只有
area/3和perimeter/3,其他全是内部实现
其他编辑器里写模块总有各种仪式感——Prolog 倒是直接,一行搞定。
完整文件:
%% geometry.pl
:- module(geometry, [area/3, perimeter/3]).
area(circle, R, A) :- A is pi * R * R.
area(rect, (W, H), A) :- A is W * H.
perimeter(circle, R, P) :- P is 2 * pi * R.
perimeter(rect, (W, H), P) :- P is 2 * (W + H).
跨语言类比:模块 ≈ Python 的
import+ 命名空间,但比 Python 更明确——不导出的谓词外部完全不可见,不像 Python 的_前缀靠自觉。
1.3. use_module/1 —— 用别人的地盘
有了模块,怎么用?
%% main.pl
:- use_module(geometry).
main :-
area(circle, 5, A),
format("Area: ~f~n", [A]).
use_module/1 找到 geometry.pl,加载它,把导出的谓词引入当前模块的命名空间。
Prolog 时刻:
use_module/1也是 directive,编译时执行。这意味着如果你写错了模块名,在加载时就报错,不会等到运行时才炸。
1.3.1. 模块查找路径
use_module(geometry) 去哪里找文件?默认搜索:
- 当前目录
- SWI-Prolog library 路径
可以手动注册路径:
:- multifile user:file_search_path/2.
user:file_search_path(my_lib, '/path/to/my/libs').
:- use_module(my_lib(geometry)).
或者直接给绝对路径:
:- use_module('/home/user/prolog/geometry.pl').
1.4. 可见性控制——给模块装个门
1.4.1. 借什么,借多少
use_module/1 默认导入全部导出。可以只借你要的:
:- use_module(geometry, [area/3]).
这样 perimeter/3 就不可见,想用必须显式加上。代码可读性更好——读者一看 area/3 就知道来自 geometry。
1.4.2. 模块限定调用
即使不用 use_module,也能通过模块前缀直接调用:
?- geometry:area(circle, 3, A).
A = 28.274333882308138.
调试时特别有用——临时拿来用,不用改代码。
1.4.3. 运算符导出
模块也能导出运算符:
:- module(arith, [op(500, yfx, @), (@)/2]).
:- op(500, yfx, @).
X @ Y :- Z is X + Y.
加载后 @ 就能当运算符用了。
1.5. 创建一个可发布的 Prolog Library
写一个完整的 library 给别人用,怎么做?
1. 目录结构
my_lib/
pack.pl % 包描述
logic.pl % 核心模块
io.pl % I/O 模块
2. pack.pl —— 身份信息
name('my_lib').
version('1.0.0').
title('My Prolog Library').
author('Your Name', 'email@example.com').
home('https://github.com/you/my_lib').
3. 模块之间互相引用
%% logic.pl
:- module(logic, [solve/2]).
:- use_module(io).
solve(X, Y) :-
read_input(X),
compute(X, Y),
output(Y).
4. 安装为 pack
本地安装:
$ swipl pack_install /path/to/my_lib
发布到 GitHub,别人就能:
$ swipl pack_install yourname/my_lib
跨语言类比:pack 系统 ≈ npm 或 pip,但更轻量——不需要 registry 账号,一个 GitHub 仓库就行。
1.6. 模块的注意事项
1.6.1. 动态谓词
模块内用 dynamic,需要在模块作用域内声明:
:- module(db, [add/1, list/1]).
:- dynamic entry/1.
add(X) :- assert(entry(X)).
list(X) :- entry(X).
1.6.2. 多文件模块
一个模块可以跨多个文件:
%% tools.pl
:- module(tools, [foo/1]).
:- multifile tools:bar/1.
%% tools_extra.pl
:- module(tools_extra, []).
:- multifile tools:bar/1.
tools:bar(baz).
1.6.3. 模块与 meta-call
Meta-call 需要指定模块上下文:
call_in_module(Mod, Goal) :-
Mod:Goal.
maplist/2 等高阶谓词会自动处理模块上下文,一般不用手动操心。
1.7. 常见反模式
反模式 1:一个巨型文件装天下
全部代码写一个文件,靠空行分隔。命名冲突、难以测试、没人敢动。
反模式 2:过度模块化
每个谓词一个文件,use_module 列表比代码还长。依赖图绕成毛线球。
反模式 3:导出所有东西
把 helper_1/2、internal_helper/3 全导出。使用者面对一堆不相关的名字。
建议:按功能域分组,一个文件 50-200 行。导出接口谓词,内部帮助谓词藏好。
1.8. 总结
模块系统让 Prolog 项目从脚本集进化成真正的工程。
| 你要做的事 | 用什么 |
|---|---|
| 声明模块+导出 | module/2 |
| 加载依赖 | use_module/1 |
| 选择性导入 | use_module(M, [P/N]) |
| 临时调用 | Module:Goal |
| 库发布 | pack.pl + pack_install |
下章我们用这些模块知识搭一个真正的 web server。
1.9. 参考
- The Power of Prolog — Modules
- SWI-Prolog manual: Module system