乍听之下,不无道理;仔细揣摩,胡说八道

0%

用Prolog开发WEB服务

用 Prolog 开发 WEB 服务

序言

在绝大多数互联网行业开发者看来,Prolog 不是一门会被用在本职开发工作中的语言。更多的时候,谈论起 Prolog,人们联想到的往往是诸如“逻辑编程”、“人工智能”等词语,将它与 SQL 放在一起,视为一种 DSL,而非像 Java、Python 这样的通用编程语言。因此,我一直很好奇能否使用 Prolog 来开发一些更偏向于业务系统的程序。

答案是肯定的。基于 SWI-Prolog 这个实现和它的标准库,我开发出了一个简单的短链服务,验证了 Prolog 的确可以满足开发一个业务系统的各种功能需求。

Prolog 基础知识

由于本文的大多数读者对 Prolog 应当是比较陌生的,因此在开始讲解如何用它开发一个 WEB 应用之前,必须稍作科普,介绍一下 Prolog 的基础知识,包括但不限于:

  1. Prolog 程序的基本结构;
  2. 运行 Prolog 脚本;
  3. 编译 Prolog 程序。

Hello World

Prolog 是一门语言而不是一个具体的解释器或者编译器,为了可以运行 Prolog 脚本或编译源代码,我选择使用 SWI-Prolog。有了它,就可以运行经典的 Hello World 程序了

1
2
3
4
:- initialization(main, main).

main(_) :-
format("Hello, world!~n").

假设上述源代码被保存在文件hello_world.pl中,那么执行它的命令如下

1
swipl ./hello_world.pl

可以看到它打印出了所期望的文本

hello_world的效果

现在让我来稍微介绍一下上述代码中的细节。:- initialization(main, main).是一个给 SWI-Prolog 的“指示”,可以理解为其声明了程序启动后的入口是一个叫做main的函数。而

1
2
main(_) :-
format("Hello, world!~n").

则是函数main的定义,它调用内置的函数format来打印一个字符串到标准输出。

编译 Prolog 程序

利用 SWI-Prolog 可以像运行 Python 脚本一般来运行 Prolog 程序,当然,也可以像 C 程序一样将其从文本形态的源代码编译为一个独立的可执行文件。仍然以前文的源文件hello_world.pl为例,编译的命令如下

1
swipl --stand_alone=true -o hello_world -c hello_world.pl

效果如下图所示

编译prolog程序

在 C 语言中,被编译的程序的入口是约定俗成的,即函数main。而由于在文件hello_world.pl中用指令:- initialization(main, main).

Prolog 的使用

在开发 WEB 服务的过程中,还会遇到许多与 WEB 无关的、Prolog 自身在其它领域的应用知识,例如:

  1. 如何读写磁盘文件;
  2. 如何处理 JSON 格式的数据;
  3. 如何读写 MySQL;
  4. 如何读写 Redis;

因此在这一章节中,将会分别介绍在 Prolog 中如何做到上面的这些事情。

读取磁盘文件

要读取磁盘文件的全部内容,可以使用 SWI-Prolog 的库提供的函数read_file_to_string/3。假设要读取的文件为/tmp/demo.txt,其内容如下

1
2
3
4
5
6
7
Shopping List:

- Milk
- Bread
- Eggs
- Apples
- Coffee

那么read_file_to_string/3的用法如下

1
2
3
4
5
6
7
:- use_module(library(readutil)).

:- initialization(main, main).

main(_) :-
read_file_to_string("/tmp/demo.txt", String, []),
format("file content is: ~s~n", [String]).

这样就可以将读到的文件内容完全打印到控制台上,如下图所示

读取磁盘文件

写入磁盘文件

如果要将数据写入到磁盘文件中——例如,在每次处理完请求后记录日志,那么可以使用函数write。以将前文中的字符串Hello, world!写入到文件中为例,示例代码如下

1
2
3
4
5
6
7
:- initialization(main, main).

main(_) :-
LogContent = "Hello, world!",
open("/tmp/access.log", write, Stream),
write(Stream, LogContent),
close(Stream).

效果如下图所示

写入磁盘文件

解析 JSON 格式

JSON 已经是应用最广泛的数据交互格式之一了,因此如果一门语言要能够投产于业务系统的开发,必然离不开对 JSON 数据的处理能力。假设要处理的 JSON 数据如下

1
2
3
4
5
6
7
8
9
{
"mysql": {
"driver_string": "DRIVER={MySQL ODBC 8.0 Driver};String Types=Unicode;password=1234567;port=3306;server=mysql;user=root"
},
"redis": {
"hostname": "redis",
"port": 6379
}
}

这些内容存储在文件/tmp/config.json中,那么下列代码会取出其中的叶子节点来输出

1
2
3
4
5
6
7
8
9
10
11
12
:- use_module(library(http/json)).

:- initialization(main, main).

main(_) :-
ConfigPath = "/tmp/config.json",
read_file_to_string(ConfigPath, String, []),
% 按照 JSON 格式反序列化为字典类型的数据。
atom_json_dict(String, JSONDict, []),
format("mysql.driver_string = ~s~n", [JSONDict.mysql.driver_string]),
format("redis.hostname = ~s~n", [JSONDict.redis.hostname]),
format("redis.port = ~d~n", [JSONDict.redis.port]).

上文中的函数atom_json_dict将字符串类型的变量String反序列化为变量JSONDict。从这里可以看到,SWI-Prolog 为字典类型提供一个中缀操作符.,使得我们可以像在多数主流语言中引用类的成员变量一般,用简单的语法来获取字典内的字段——即JSONDict.mysql.driver_string这样的代码。

上面的代码的运行效果如下图所示

解析JSON字典的效果

自动导入的库

如果分别查看函数read_file_to_stringatom_json_dict的文档(分别在这里这里),就会发现前者的页面上写着can be autoloaded,而后者没有

自动导入的提示

所以前文关于read_file_to_string的例子中,即便不写上:- use_module(library(readutil)).,也是可以正常调用的

不需要导入的例子

事实与全局变量

在将磁盘上的配置文件的内容加载到内存中后,最好可以将其赋值为一个全局变量以便在所有的函数中访问到。要做到这一点,可以利用 Prolog 的一个特性:事实。

在很多 Prolog 的入门教程中,都会介绍经典的、如何用 Prolog 来回答两个人是否为某种关系的例子。例如,在这个教程中,就给出了如何判断两个人是否为朋友的示例,如下图所示

判断是否为朋友的例子

其中,像

1
2
3
4
friend(john, julia).
friend(john, jack).
friend(julia, sam).
friend(julia, molly).

这样的代码就是 Prolog 中的“事实”。其中,friend在 Prolog 中被称为“谓词”,也就是前文中一直提到的函数。因此,如果想要定义一个全局变量,可以:

  1. dynamic/1声明一个只有一个参数的“动态”谓词;
  2. asserta/1新增一个事实;
  3. 在别的位置,用通常的归一语法就可以绑定全局的值到一个变量上了。

就像下面这样子

1
2
3
4
5
6
7
8
9
:- initialization(main, main).

% 声明为动态的以便允许使用 asserta 修改。
:- dynamic odbc_driver_string/1.

main(_) :-
asserta(odbc_driver_string("This is a global variable")),
odbc_driver_string(DriverString),
format("DriverString = ~s~n", [DriverString]).

效果如下

用事实来定义全局变量

Liutos wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
你的一点心意,我的十分动力。