序言
在绝大多数互联网行业开发者看来,Prolog 不是一门会被用在本职开发工作中的语言。更多的时候,谈论起 Prolog,人们联想到的往往是诸如“逻辑编程”、“人工智能”等词语,将它与 SQL 放在一起,视为一种 DSL,而非像 Java、Python 这样的通用编程语言。因此,我一直很好奇能否使用 Prolog 来开发一些更偏向于业务系统的程序。
答案是肯定的。基于 SWI-Prolog 这个实现和它的标准库,我开发出了一个简单的短链服务,验证了 Prolog 的确可以满足开发一个业务系统的各种功能需求。
Prolog 基础知识
由于本文的大多数读者对 Prolog 应当是比较陌生的,因此在开始讲解如何用它开发一个 WEB 应用之前,必须稍作科普,介绍一下 Prolog 的基础知识,包括但不限于:
- Prolog 程序的基本结构;
- 运行 Prolog 脚本;
- 编译 Prolog 程序。
Hello World
Prolog 是一门语言而不是一个具体的解释器或者编译器,为了可以运行 Prolog 脚本或编译源代码,我选择使用 SWI-Prolog。有了它,就可以运行经典的 Hello World 程序了
1 | :- initialization(main, main). |
假设上述源代码被保存在文件hello_world.pl
中,那么执行它的命令如下
1 | swipl ./hello_world.pl |
可以看到它打印出了所期望的文本
现在让我来稍微介绍一下上述代码中的细节。:- initialization(main, main).
是一个给 SWI-Prolog 的“指示”,可以理解为其声明了程序启动后的入口是一个叫做main
的函数。而
1 | main(_) :- |
则是函数main
的定义,它调用内置的函数format
来打印一个字符串到标准输出。
编译 Prolog 程序
利用 SWI-Prolog 可以像运行 Python 脚本一般来运行 Prolog 程序,当然,也可以像 C 程序一样将其从文本形态的源代码编译为一个独立的可执行文件。仍然以前文的源文件hello_world.pl
为例,编译的命令如下
1 | swipl --stand_alone=true -o hello_world -c hello_world.pl |
效果如下图所示
在 C 语言中,被编译的程序的入口是约定俗成的,即函数main
。而由于在文件hello_world.pl
中用指令:- initialization(main, main).
Prolog 的使用
在开发 WEB 服务的过程中,还会遇到许多与 WEB 无关的、Prolog 自身在其它领域的应用知识,例如:
- 如何读写磁盘文件;
- 如何处理 JSON 格式的数据;
- 如何读写 MySQL;
- 如何读写 Redis;
因此在这一章节中,将会分别介绍在 Prolog 中如何做到上面的这些事情。
读取磁盘文件
要读取磁盘文件的全部内容,可以使用 SWI-Prolog 的库提供的函数read_file_to_string/3。假设要读取的文件为/tmp/demo.txt
,其内容如下
1 | Shopping List: |
那么read_file_to_string/3
的用法如下
1 | :- use_module(library(readutil)). |
这样就可以将读到的文件内容完全打印到控制台上,如下图所示
写入磁盘文件
如果要将数据写入到磁盘文件中——例如,在每次处理完请求后记录日志,那么可以使用函数write
。以将前文中的字符串Hello, world!
写入到文件中为例,示例代码如下
1 | :- initialization(main, main). |
效果如下图所示
解析 JSON 格式
JSON 已经是应用最广泛的数据交互格式之一了,因此如果一门语言要能够投产于业务系统的开发,必然离不开对 JSON 数据的处理能力。假设要处理的 JSON 数据如下
1 | { |
这些内容存储在文件/tmp/config.json
中,那么下列代码会取出其中的叶子节点来输出
1 | :- use_module(library(http/json)). |
上文中的函数atom_json_dict将字符串类型的变量String
反序列化为变量JSONDict
。从这里可以看到,SWI-Prolog 为字典类型提供一个中缀操作符.
,使得我们可以像在多数主流语言中引用类的成员变量一般,用简单的语法来获取字典内的字段——即JSONDict.mysql.driver_string
这样的代码。
上面的代码的运行效果如下图所示
自动导入的库
如果分别查看函数read_file_to_string
和atom_json_dict
的文档(分别在这里和这里),就会发现前者的页面上写着can be autoloaded
,而后者没有
所以前文关于read_file_to_string
的例子中,即便不写上:- use_module(library(readutil)).
,也是可以正常调用的
事实与全局变量
在将磁盘上的配置文件的内容加载到内存中后,最好可以将其赋值为一个全局变量以便在所有的函数中访问到。要做到这一点,可以利用 Prolog 的一个特性:事实。
在很多 Prolog 的入门教程中,都会介绍经典的、如何用 Prolog 来回答两个人是否为某种关系的例子。例如,在这个教程中,就给出了如何判断两个人是否为朋友的示例,如下图所示
其中,像
1 | friend(john, julia). |
这样的代码就是 Prolog 中的“事实”。其中,friend
在 Prolog 中被称为“谓词”,也就是前文中一直提到的函数。因此,如果想要定义一个全局变量,可以:
- 用
dynamic/1
声明一个只有一个参数的“动态”谓词; - 用
asserta/1
新增一个事实; - 在别的位置,用通常的归一语法就可以绑定全局的值到一个变量上了。
就像下面这样子
1 | :- initialization(main, main). |
效果如下
在 Docker 中运行 Prolog
在之后的例子中,我还会介绍如何使用 Prolog 来读写数据库。但在摸索的过程中,我发现在 macOS 上无法运行成功,只有在 Docker 内才可行,因此这一节将会先介绍如何在 Docker 中运行 Prolog。
以前文中的 Hello World 程序为例,在已经有了源文件hello_world.pl
的前提下,准备如下的hello_world.dockerfile
文件
1 | FROM swipl:stable |
然后基于这份配置来构建镜像
1 | docker build -f ./hello_world.dockerfile -t hello_world . |
如果无法拉取镜像docker.io/library/swipl:stable
,可以先通过 DaoCloud 下载,然后替换掉标签
1 | docker pull docker.m.daocloud.io/library/swipl:stable |
然后就可以运行这个镜像了
1 | docker run hello_world |
效果如下图所示