Prolog和逻辑编程

Prolog是我最喜欢的语言之一。ruby给人的感觉是轻松灵巧,还会有点儿惊艳,写ruby程序就像是拥抱自由;和ruby不同,Prolog规规矩矩,编程模式和常见编程语言大相径庭,通过已知自动推断未知。提供一些事实,制定一些规则,Prolog就会自动给出某些判断是yes还是no。

Prolog基础

通过一个例子来介绍:

animal(cat).
animal(dog).

isAnimal(X) :- animal(X).

在Prolog中,变量若以小写字母开头,那就是一个原子,类似ruby中的符号;大写字母或下划线开头的才是真正的变量。

这个小程序给出两个事实:cat是animal,dog也是animal。

然后有一个规则:如果animal(X),那么isAnimal(X)。

虽然这个例子几乎毫无实用价值,不过它确实是一个完整的Prolog程序,而且我相信通过这么一个没用的程序,可以对Prolog有一个初步印象。

我们看一下这个程序的运行情况:

GNU Prolog 1.4.4 (64 bits)
Compiled Apr 23 2013, 16:05:07 with cl
By Daniel Diaz
Copyright (C) 1999-2013 Daniel Diaz
compiling D:/Learning/Language/Prolog/1.pl for byte code...
D:/Learning/Language/Prolog/1.pl compiled, 4 lines read - 423 bytes written, 16 ms
| ?- animal(cat).

yes
| ?- animal(big).

no
| ?- animal(dog).

yes
| ?- isAnimal(cat).

yes
| ?- isAnimal(dog).

yes
| ?- isAnimal(pig).

no

可以看到,Prolog就像一个诚实的人,虽然只会说yes和no。如果你有兴趣,可以试一些其他范型的编程语言中的常见语句,事实Prolog会怎么处理。

Prolog既然号称是面向逻辑语言,就一定有它自己的逻辑能力,推导就是它最基本的能力之一。再看一个稍微有用一些的例子:

diff(a,b).
diff(b,a).
diff(a,c).
diff(c,a).
diff(b,c).
diff(c,b).

color(A, B, C, D) :-
	diff(A,B),
	diff(A,D),
	diff(B,C),
	diff(C,D).

这里给出六个事实,abc互不相同。然后color是一个有四个变量的推论,满足相邻变量diff则是一个合法color。不太理解的话看一下输出:

| ?- color(a,b,c,b).

true ? 

yes
| ?- color(c,a,c,a).

true ? 

yes
| ?- color(b,a,c,c).

no
| ?- color(a,b,c,What).

What = b ? 

yes
| ?- color(a,b,c,What).

What = b ? ;

no
| ?- color(a,b,a,What).

What = b ? ;

What = c ? ;

(16 ms) no
| ?- 

看到了吧,Prolog可以自动推测变量来满足事实。这是很强大的功能,事实上,刚才的diff-color小程序就解决了经典了四色问题,相邻国家地图不能同色,做过OI或者ACM的人都知道,这是搜索的最入门题目,C语言二三十行左右搞定,但是Prolog只需要最最基础的事实-推论,然后自动得出结论。

递归

还是用例子来说明吧,Prolog语法少,理解简单,说来说去都不如一个好例子有意思。

fib(1,1).
fib(2,1).
fib(N,Ans) :-
	(N > 2),
	A is N - 1,
	B is N - 2,
	fib(A,Ans1),
	fib(B,Ans2),
	Ans is Ans1 + Ans2.

斐波那契数列的Prolog版本。给出两个事实,fib(1,1)和fib(2,1),其实它们表示的是斐波那契数列的第一项和第二项分别是1和1。不过对于Prolog,只有事实。可以看到下面的推论,fib(N,Ans),用了递归的思想。

用Prolog的面向逻辑思维来解释就是,fib(N,Ans)是一个推论,后面是这个推论满足的条件。有6个条件,这六个条件都满足,就完成了推论。

合一

合一是理解Prolog的关键,理解了合一,Prolog就理解了一大半了。

我觉得合一很好理解,虽然听起来很玄学。Prolog的合一和调用函数的形参实参概念类似,都是把变量或者原子对应到另一个变量或者原子。不同的是,函数调用会有形参实参,但是合一在=左右有效,在推论中也有效。

为了理解合一,需要知道Prolog中常用的一个数据结构:列表。几乎所有面向对象语言和函数式编程语言,列表都是很重要的类型,Prolog中也是。[]中括号形成空列表,中括号里头可以有原子、数字和字符串,形成列表。

列表可以分解。 [1,2,3,4,5] = [Head|Tail] ,列表被分解,Head = 1, Tail = [2,3,4,5]。这就是合一的过程,Head被赋为1,Tail被赋为[2,3,4,5]。

还有一个重要规则需要学习,append。

append([1,2,3], [4,5], Ans),通过append规则,Prolog得出结论:Ans = [1,2,3,4,5]。

append还能做减法,append([1,2,3], Ans, [1,2,3,4,5]),Prolog得出结论: Ans = [4,5]。

看一个简单的求平均数的例子:

count(0, []).
count(Count, [Head|Tail]) :- count(TailCount, Tail), Count is TailCount + 1.

sum(0, []).
sum(Total, [Head|Tail]) :- sum(TailSum, Tail), Total is TailSum + Head.

average(Average, List) :- sum(Sum, List), count(Count, List), Average is Sum/Count.

请自行理解,讲解甚是无聊,而且也啰啰嗦嗦说不清楚。按照Prolog的思维来看这个程序:事实,逻辑,推论。

通过几个程序自己学习

我发现学习Prolog的最好方法就是自己写小程序来理解它面向逻辑的精髓。多说无益,写代码是最好的学习方式。

阶乘:

jiecheng(1,1).
jiecheng(N,Ans) :-
	N >= 1,
	M is N-1,
	jiecheng(M, Temp),
	Ans is N * Temp.

最小值:

zuixiaozhi([], nil).
zuixiaozhi([A], A).
zuixiaozhi(List, Ans) :-
	List = [Head|Tail],
	zuixiaozhi(Tail, Temp),
	Ans is min(Head, Temp).

翻转列表:

reverseList([],[]).
reverseList(List,Rlist) :-
	List = [Head|Tail],
	reverseList(Tail, Temp),
	append(Temp,[Head],Rlist).

快速排序:

divide([], _, [], []).
divide([Head|Tail], Middle, Small, Big) :-
	(	Head =< Middle ->
		Small = [Head|Rest],
		divide(Tail, Middle, Rest, Big)
	;
		Big = [Head|Rest],
		divide(Tail, Middle, Small, Rest)
	).

qsort([], []).
qsort([Head|Tail], Sorted) :-
	divide(Tail, Head, Small, Big),
	qsort(Small, A),
	qsort(Big, B),
	append(A, [Head], Temp),
	append(Temp, B, Sorted).

注意括号和if语句很相似。

八皇后问题:

valid_queen(Row, Col) :- member(Col, [1,2,3,4,5,6,7,8]).
valid_board([]).
valid_board(Head|Tail) :- valid_queen(Head), valid_board(Tail).

cols([],[]).
cols([(_,Col)|QueensTail], [Col|ColsTail]) :-
	cols(QueensTail, ColsTail).
	
diags1([],[]).
diags1([(Row,Col)|QueensTail], [Diag|DiagsTail]) :-
	Diag is Col - Row,
	diags1(QueensTail, DiagsTail).

diags2([],[]).
diags2([(Row,Col)|QueensTail], [Diag|DiagsTail]) :-
	Diag is Col + Row,
	diags2(QueensTail, DiagsTail).

eight_queen(Board) :-
	Board = [(1,_),(2,_),(3,_),(4,_),(5,_),(6,_),(7,_),(8,_)],
	valid_board(Board),
	cols(Board,Cols),
	diags1(Board,Diags1),
	diags2(Board,Diags2),
	fd_all_different(Cols),
	fd_all_different(Diags1),
	fd_all_different(Diags2).

数独问题:

valid([]).
valid(List) :-
	List = [Head|Tail],
	fd_all_different(Head),
	valid(Tail).
	
sudoku(Puzzle, Solution) :-
	Solution = Puzzle,
	Puzzle = [S11,S12,S13,S14,
			  S21,S22,S23,S24,
			  S31,S32,S33,S34,
			  S41,S42,S43,S44],
	fd_domain(Solution,1,4),
	Row1 = [S11,S12,S13,S14],
	Row2 = [S21,S22,S23,S24],
	Row3 = [S31,S32,S33,S34],
	Row4 = [S41,S42,S43,S44],
	
	Col1 = [S11,S21,S31,S41],
	Col2 = [S12,S22,S32,S42],
	Col3 = [S13,S23,S33,S43],
	Col4 = [S14,S24,S34,S44],
	
	Squ1 = [S11,S12,S21,S22],
	Squ2 = [S13,S14,S23,S24],
	Squ3 = [S31,S32,S41,S42],
	Squ4 = [S33,S34,S43,S44],
	
	valid([Row1,Row2,Row3,Row4,
		   Col1,Col2,Col3,Col4,
		   Squ1,Squ2,Squ3,Squ4]).

可以看到,Prolog是解决资源受限问题的利器。事务处理、逻辑游戏等等问题,用Prolog解决,会比其他语言方便太多。

关于编程语言

最开始写程序,我用Pascal,那时候常常羡慕会C语言的,莫名觉得比我们要先进。后来觉得其实什么语言都是一样的,再后来学会了C,更加坚定了这一看法。

不过随着不同类型语言的学习,觉得语言其实是有优劣之分的,最起码个性是很明显的。每一种语言都代表了一种思维,如果只会C,那么凡事都要一句话一句话来;反之如果只会Python,那么凡事都不用想,编程能力和对计算机的领悟到不了一定水平。虽然不必要每种语言都精通,而且那也是不可能的,但是多学习不同思路,还是有很大好处的。