学习到这个阶段,你可能已经使用append/3和member/2写了很多程序。你可能每次都需要将它们的实现代码拷贝到使用它们的程序文件中。而且,经过几次这样做之后,你就会感觉每次不停的拷贝是非常重复和麻烦的事情。如果你可以在一个文件中定义它们,然后在需要的地方使用,这将会是令人愉快的,而且也是更加合理的做法。当然,Prolog提供了这样的方式去组织程序。
程序中的读操作
事实上,你已经知道有一种方式,可以告诉Prolog读取文件中的谓词定义,如下:
[FileName1]
并且这个命令可以告诉Prolog对文件中的内容进行编译。但是有两件有用的事情你也应该知道:第一,你可以一次编译多个文件:
[FileName1, FileName2, ..., FileNameN]
第二,更为重要的是,文件编译不仅仅可以用于交互环境,如果你将下面代码:
:- [FileName1, FileName2, ..., FileNameN].
放在你的程序代码文件头部(比如命名为main.pl的文件),那么事实上你是告诉Prolog首先编译列出的文件,然后再读取程序的其他部分。
这个特性给予了我们重用谓词的简单方式。比如,假设你将所有基础列表操作相关的谓词放在一个文件中(比如append/3,member/2,reverse/2等等),名字是listPredicates.pl,如果你想要使用它们,可以这么输入:
:- [listPredicates].
到需要使用它们的程序文件的开头。Prolog将会编译listPredicates当读取程序文件时,所以listPredicates中定义的所有谓词能可用了。
这里有一点需要注意的,当Prolog装载读取文件时,它不会检查这些文件是否已经编译过。如果文件中的谓词已经存在在知识库中,因为文件在之前已经编译过,Prolog还是会重新编译它们,虽然这完全没有必要。而且在编译非常大的文件时,会导致性能的损耗。
内置的谓词 ensure_loaded/1 可以更加智能的加载读取文件,它的效果和之前的一致,使用如下:
:- ensure_loaded([listPredicates]).
Prolog将会检查listPredicates.pl是否已经加载过,并且当文件有修改时,才会再次加载。
模块
现在设想你正在写一个程序进行电影知识库的管理。你已经设计了一个谓词printActors用于打印一部电影的主要演员,以及一个谓词printMovies用于打印某个导演的所有电影。两个不同的定义储存在不同的文件中,名为printActors.pl和printMovies.pl,同时两个文件都有名为displayList/1的辅助谓词。下面是第一个文件内容:
% This is the file: printActors.pl
printActors(File) :-
setof(Actor, starring(Actor, File), List),
displayList(List).
displayList([]) :- nl.
displayList([X | L]) :-
write(X), tab(1),
displayList(L).
下面是第二个文件内容:
% This is the file: printMovies.pl
printMovies(Director) :-
setof(File, directed(Director, File), List),
displayList(List).
displayList([]) :- nl.
displayList([X | L]) :-
write(X), nl,
displayList(L).
注意displayList/1在两个文件中有不同的定义:打印演员是用按行的方式(使用tab/1),而打印电影是用按列的方式(使用nl/0)。这会让Prolog困惑吗?让我们看看,我们使用下面的代码将两个文件同时加载:
% This is the file: main.pl
:- [printActors].
:- [printMovies].
上面的代码写在main.pl文件的开头,编译主程序文件会报出如下的提示信息:
?- [main].
{consulting main.pl...}
{consulting printActors.pl...}
{printActors.pl consulted, 10 msec 296 bytes}
{consulting printMovies.pl...}
The procedure displayList/1 is being redefined.
Old file: printActors.pl
New file: printMovies.pl
Do you really want to redefine it? (y, n, p, or ?)
发生了什么?由于printActors.pl和printMovies.pl两个文件都定义了名为displayList/1的谓词,所以Prolog需要选择其中的一个定义(同一谓词在知识库中不能有不同的定义)。
如何解决这个问题呢?在有些情况下,你可能真的需要重新定义谓词。但是现在你不能——因为打印演员和打印电影希望使用不同的方式进行。有一种解决方法是:给其中一个谓词另外一个名字。但是这个方法是很笨拙的。你希望每个文件都是逻辑自包含的程序实体,不希望浪费时间和精力为了其他文件而进行谓词的重新命名。所以获取概念独立性最自然的方式是使用Prolog的模块系统。
模块从根本上允许隐藏谓词定义。你可以决定哪些谓词应该是对外公开的(即,可以由其他文件的程序来调用),以及哪些谓词应该是私有的(即,只能在模块内部调用)。这样的话,就不允许在模块外部调用私有的谓词,这样两个模块内部名字一样的私有谓词就不会发生冲突了。在我们的例子中,displayList/1这个谓词是私有谓词的最好候选:它都是在各自的文件中起辅助作用的,并且两个实现之间没有任何关联。
可以将文件转换为模块,只需要在文件开头进行模块声明。模块声明的形式如下:
:- module(ModuleName, List_of_Predicates_to_be_Exported).
上面的声明指定了模块的名字,及其公共谓词的列表,即你希望导出的谓词列表。这些谓词是能够在模块外部被访问的。
让我们将电影知识库程序的文件模块化。我们只需要在第一个文件的开头包含下面一行:
% This is ths file: printActors.pl
:- module(printActors, [printActors/1]).
printMovies(Director) :-
setof(File, directed(Director, File), List),
displayList(List).
displayList([]) :- nl.
displayList([X | L]) :-
write(X), nl,
displayList(L).
这里我们定义了一个称为printActors的模块,有一个公共谓词printActors/1。谓词displayList/1仅仅在模块printActors内部可见,所以它的定义不会对其他模块有任何影响。
类似地,我们可以将第二个文件也模块化:
% This is the file: printMovies.pl
:- module(printMovies, [printMovies/1]).
printMovies(Director) :-
setof(File, directed(Director, File), List),
displayList(List).
displayList([]) :- nl.
displayList([X | L]) :-
write(X), nl,
displayList(L).
同样地,displayList/1的定义只在printMovies这个模块中可见,所以在加载两个模块文件中Prolog不会出现冲突和崩溃了。
可以使用内置谓词use_module/1加载模块,模块中所有公共谓词都会被导入到当前知识库中。换种说法,所有公共谓词都是能够访问的。修改main.pl文件:
:- use_module(printActors).
:- use_module(printMovies).
如果你不希望使用模块中的所有公开谓词,而只是其中的一部分,你可以使用两个参数版本的use_module,其中第二个参数是你真正希望使用的公开谓词列表,如下:
% This is the file: main.pl
:- use_module(printActors, [printActors/1]).
:- use_module(printMovies, [printMovies/1]).
在main文件的头部,我们显式地指出需要使用的谓词是:printActors/1和printMovies/1,而且不需要其他的谓词(当然在这个例子中,也没有其他公开的谓词可以使用了)。
库
很多通用的谓词在大多数Prolog实现中,已经通过一种或者多种方式预先定义了。如果你在使用SWI Prolog,可能已经注意到诸如append/3和member/2这些谓词已经是系统的组成部分。这是SWI Prolog特有的。其他一些Prolog的实现,比如SICStus,并没有内置这些谓词,都是通过库的方式提供的。
库是定义了通用谓词的一些模块,并且能够使用相同的命令进行加载使用。当你指定想要使用的特定库名字时,你必须要告诉Prolog将模块作为库的形式来加载,这样Prolog才知道去什么地方找到这些模块(即,Prolog有特定的存放库的路径,并不是程序代码所在的路径)。比如,在程序文件的头部输入下面的命令:
:- use_module(library(lists)).
将会告诉Prolog加载名字为lists的库。在SICStus Prolog中,这个库包含了一系列通用的列表处理谓词。
库十分有用并且能够提高开发效率。而且,库中的代码都是优秀程序员写的,几乎都是执行效率很高,并且错误很少的。但是不同的Prolog实现中,库的组织和实现的谓词并没有统一的标准。这意味着如果你想要你的程序运行在不同的Prolog实现下,定义自己的库模块是更加容易和效率的,这样不用尝试去理解和解决不同Prolog实现下的兼容性问题。