上一篇對Prolog有了一個感性的認識,今天介紹下Prolog中一些基本概念,想要用Prolog解決一些實際問題之前必須要先了解它們。這些概念在《七周七語言》這本書中都有介紹,我簡單提煉匯總下,就當給這門小眾語言做個宣傳吧。
變量/規則/知識庫
在Prolog中變量的命名是有特殊要求的,如果一個詞以小寫字母開頭,它就是一個原子(atom),類似於其他語言中的符號(symbol),如果一個詞以大寫或下划線開頭,那么它就是一個變量,和其他語言一樣變量值可以改變,可以賦值(不過更靈活)。
符號組成一些事實:
likes(zhangsan,lisi).
likes(wangwu,lisi).
likes(chenliu,maqi).
符號和變量在一起可以用來定義規則:
friend(X,Y):- \+(X = Y),likes(X,Z),likes(Y,Z).
事實是我們對這個世界直接觀察的結果。規則是關於現實世界的邏輯推論。
事實 + 規則 = 知識庫。
上面的規則可以叫做friend/2因為它有兩個參數(類似C#方法中的形參),:-讀作“如果”,“如果”后面是由一系列“子目標”組成,子目標之間可以是且的關系,用“,”分割,也可以是或者的關系,用“.”表示。Prolog就是通過驗證規則來回到我們yes或no的,如果參數能滿足所有子目標就是yes。
合一(unification)
合一是Prolog中一個非常重要的概念。簡單的來說合一就相當於其它語言中的賦值:
cat(lion). cat(tiger). dorothy(X,Y,Z) :- X = lion,Y = tiger,Z = bear. twin_cast(X,Y) :- cat(X),cat(Y).
合一的意思是:找出那些使規則匹配的值。
所以執行dorothy(lion,tiger,bear).這句,Prolog會返回yes:
dorothy/3規則的右側,Prolog將lion賦值給X,tiger賦值給Y,bear賦值給Z,就像在命令式語言中這樣:
var X = lion; var Y = tiger; var Z = bear;
這些值和左側(也就是dorothy(lion,tiger,bear))對應的值相匹配,所以合一成功。
不過真正讓合一發揮作用的是因為它在規則的兩側都能工作,在GNU Prolog中執行下面的語句:
dorothy(One,Two,Three).
會得到這樣的結果:
在規則右側Prolog還是分別將X,Y,Z和lion,tiger,bear進行綁定,而在規則的右側,Prolog是的One,Two,Three分別和X,Y,Z進行合一了:
One = X = lion; Two = Y = tiger; Three = Z = bear;
並且合一的情況有時候不是唯一的,我們如下執行上面的twin_cast規則:
twin_cast(One,Two).
可以通過“;”來進行追問,有時候我們可能不滿足於一個答案。
怎么樣你是不是已經預見到合一在Prolog中發揮的至關重要的作用了:輸出。
列表/元組
只要是語言都會有數據結構,因為程序 = 算法 + 數據結構。
Prolog中有兩個非常重要的數據結構:列表和元祖。列表是變長的容器:像[1,2,3]這樣表示,元組是定長的容器,像(1,2,3)這樣表示。兩個數據結構非常簡單,但是和合一配合起來的時候會十分強大。
| ?- (1,2,3) = (1,2,3). yes
| ?- [A,B,C] = [A,B,C].
yes
如果兩個元組擁有的元素數量相同並且每個元素可以合一 ,則它們整體就是合一的。
加入變量會更有趣:
| ?- [A,B,C] = [1,2,3]. A = 1 B = 2 C = 3 yes
並且變量在哪一側無所謂,只要Prolog認為它們可以相同,那么就可以合一:
| ?- [A,2,C] = [1,B,3]. A = 1 B = 2 C = 3 (16 ms) yes
這讓我想到小時候玩的一種撲克牌游戲了,當抓到大王或小王的時候,我可以把它看做是任何一張牌。
另外列表擁有一個元組不擁有的功能,這個功能在后面介紹的遞歸中會被廣泛使用。 那就是通過[Head|Tail]解析列表,很簡單看個例子就明白了:
| ?- [Head|Tail] = [1,2,3,4]. Head = 1 Tail = [2,3,4] yes
Head綁定到1,Tail綁定到剩下的元素,Tail仍然是一個列表。因為Prolog的變量是沒有數據類型之分的,所以它可以很容易的綁定為列表或元組,這點有點動態語言的性質,Prolog中也有匿名變量:
| ?- [a,b,c,d,e]=[_,_|[Head|_]]. Head = c yes
這樣我們就能指定提取列表中某一個元素了。
遞歸
我們趁熱打鐵看看Prolog中遞歸是怎么發揮作用的,留心它和命令式語言中的不同。
我們就以經典的斐波那契數列做例子吧,我們先來看下命令式語言是如何實現,這個再熟悉不過了:
static void Main(string[] args) { Console.WriteLine(Fibonacci(8)); Console.ReadKey(); }
static int Fibonacci(int n)
{ if (n == 1) return 0; if (n == 2) return 1; return Fibonacci(n - 2) + Fibonacci(n - 1); }
在看Prolog是如何實現之前,我們先來描述一下關於斐波那契數列的既定事實和規則,因為Prolog正是基於事實和規則的一門語言:
- 第一個數是0;
- 第二個數是1;
- 從第三個數開始等它前面兩個數之和。
好了和上面對應的Prolog的代碼也是三行:
fibonacci(1,0). fibonacci(2,1). fibonacci(N,V) :- N > 2, N1 is N -1, N2 is N -2, fibonacci(N1,V1),fibonacci(N2,V2), V is V1 + V2.
注:is是Prolog中內置的一個謂詞。
比較起來你可能覺得和C#代碼差不多,都很簡潔,Prolog有什么特別的或者說優勢呢?其中區別各位自己體會吧,我也在慢慢體會,這個區別就是聲明式語言和命令式語言之間的區別。
這個例子太簡單不能很好體現Prolog的優勢,下一篇文章我們看下用Prolog是如何解決數獨和八皇后問題的。我們來看下《七周七語言》這本書中幾個遞歸的例子,也許你能感興趣不妨試試:
count(0,[]). count(Count,[Head|Tail]):- count(TailCount,Tail),Count is TailCount + 1. sum(0,[]). sum(Total,[Head|Tail]):- sum(Sum,Tail),Total is Sum + Head. average(Average,List):- sum(Sum,List),count(Count,List),Average is Sum/Count.
count是求列表元素的個數,sum是求和,average是求平均值。
內置謂詞
所謂內置的謂詞,可以簡單理解為Prolog提供的一些基本“功能”,就像.net中的一些類庫一樣。上面提到的is就是。
length:獲取列表的長度:
| ?- length([1,2,3],L). L = 3 yes
append:可以用來合並兩個列表,當然還有很多其他功能:
| ?- append([1],[2],What). What = [1,2] yes
用於列表的減法:
| ?- append([1],What,[1,2,3]). What = [2,3] yes
排列組合:
| ?- append(One,Two,[1,2,3,4]). One = [] Two = [1,2,3,4] ? ; One = [1] Two = [2,3,4] ? ; One = [1,2] Two = [3,4] ? ; One = [1,2,3] Two = [4] ? ; One = [1,2,3,4] Two = [] (15 ms) yes
fd_domain:驗證值是否在一個范圍之內:
| ?- fd_domain(1,1,4). yes | ?- fd_domain([1,2,3,4],1,4). yes | ?- fd_domain([1,2,3,5],1,4). no
fd_all_different:檢查列表中是否有重復元素:
| ?- fd_all_different([1,2,3]). yes | ?- fd_all_different([1,2,1]). no
member:檢查某一個值是否在一個列表內:
| ?- member(1,[1,2]). true ? yes | ?- member(3,[1,2]). no
好了關於Prolog的基本概念就介紹到這,下一篇文章我們會看下通過這些基本概念Prolog是如何解決一些復雜問題的。想了解更多推薦大家去看看這本書。