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。这一行做了两件事:

  1. 声明模块名 — 这个文件属于 geometry 模块
  2. 定义导出列表 — 外面能用的只有 area/3perimeter/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) 去哪里找文件?默认搜索:

  1. 当前目录
  2. 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/2internal_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. 参考

Copyright © zhzluke96 2020 all right reserved,powered by Gitbook该文件修订时间: 2026-06-30 16:08

results matching ""

    No results matching ""