程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 冒號課堂§4.2:邏輯范式

冒號課堂§4.2:邏輯范式

編輯:關於JAVA

第四課 重溫范式(2)

4.2邏輯范式——當算法失去了控制

道常無為而無不為             ——《老子·道經》

關鍵詞:   編程范式,邏輯式編程,Prolog,算法,邏輯,控制

摘要:  再談邏輯式編程

?提問

衡量軟件復雜度是由代碼的長度決定的嗎?

為什麼邏輯式的編碼一般比過程式的更簡潔?

邏輯式編程相比命令式編程有哪些優勢和劣勢?

:講解

問號提出:“邏輯式編程不是也很特別嗎?前面似乎介紹得也不多。”

“那我們就用邏輯式語言Prolog再實現一次quicksort吧。”冒號說著將幻燈片翻頁——

/*快速排序法的Prolog實現*/
/* 定義劃分法 */
partition(_,[],[],[]).                         /* 劃分遞歸終點 */
partition(Pivot,[X|Rest],[X|Small],Big) :-
X < Pivot, partition(Pivot,Rest,Small,Big).   /* 比基准小的歸入Small */
partition(Pivot,[X|Rest],Small,[X|Big]) :-
X >= Pivot, partition(Pivot,Rest,Small,Big).  /* 比基准大的歸入Big */
/* 定義排序法 */
qsort([],[]).                                /* 排序遞歸終點 */
qsort([Pivot|Rest],Sorted) :-
partition(Pivot,Rest,Small,Big),            /* 按基准劃分子列 */
    qsort(Small,SortedSmall),                 /* 對前面的子列遞歸 */
    qsort(Big,SortedBig),                     /* 對後面的子列遞歸 */
    append(SortedSmall,[Pivot|SortedBig],Sorted)./* 子列合並 */

逗號撓撓頭:“看不太懂哦,好在我記住了您的一句話:容忍無知。我忍了!”

大伙都樂了。

“本節課的焦點不是語言而是范式,因此對Prolog代碼不詳加解說。我只簡單地說三點:首先,Prolog代碼是由一系列事實(fact)、規則(rule)和查詢(query)語句組成的[1]。其次,與大多數語言不同的是,大寫字母或下劃線開頭的標識符是變量,其他的是常量或函數。請注意,這不是約定俗成,而是語法規定。最後,符號‘:-’等價於if;逗號‘,’等價於and。比如,我們可以用Prolog來表達一個斷言:如果一個人未婚且為男士,那麼他就是一光棍。”冒號轉身在黑板上寫下——

/* X is bachelor if X is unmarried and male*/
bachelor (X) :- unmarried(X) , male(X).

聽見下面一陣嘀咕聲,冒號忽地閃過一個念頭:這個例子該不會傷了某位滿足條件的同志吧?頓了一會,繼續說道:“邏輯式實現的排序雖不比函數式更簡潔,但比過程式來還是綽綽有余的。畢竟同屬聲明式,省去了不少有關變量賦值、迭代和流程控制方面的代碼。我們再看一個更加典型的范例。”

黑板上出現了一幅樹狀圖形——

冒號簡作說明:“這是一個三代家譜圖。已知每人的性別和父輩,要求判斷任意兩人之間的關系。我們先用Java來試一試——”

class Person
{
   private Person parent;
   private boolean isMale;
   public Person(Person parent, boolean isMale)
   {
     this.isMale = isMale;
     this.parent = parent;
   }
private boolean isSibling(Person other)
   {
     return parent == other.parent && parent != null && this != other;
   }
   public String getRelation(Person other)
   {
     if (other == null || this == other) return null;
     if (parent == other) return isMale ? "son" : "daughter";
     if (other.parent == this) return isMale ? "father" : "mother";
     if (parent == null) // this是老祖宗
     {
       if (other.parent == null) return null;
       if (other.parent.parent == this) return isMale ? "grandfather" : "grandmother";
       return null;
     }
     if (other.parent == null) // other是老祖宗
     {
       if (parent.parent == other) return isMale ? "grandson" : "granddaughter";
       return null;
     }
     // 非直系
     if (isSibling(other)) return isMale ? "brother" : "sister";
     if (parent.isSibling(other.parent)) return "cousin";
     if (parent.isSibling(other)) return isMale ? "nephew" : "niece";
     if (isSibling(other.parent)) return isMale ? "uncle" : "aunt";
     return null;
   }
   public static void main(String[] args)
   {
     Person a = new Person(null, true);
     Person b = new Person(a, true);
     Person c = new Person(a, true);
     Person d = new Person(a, false);
     Person e = new Person(b, false);
     Person f = new Person(b, true);
     Person g = new Person(c, false);
     Person h = new Person(d, true);
     Person i = new Person(d, false);
     Person j = new Person(d, true);
     // 以下省略。。。
   }
}

“這段代碼很平凡,毋需多言。再來看看邏輯式語言的做法。”冒號不願過多地糾纏於細節,隨即又換成了Prolog代碼——

/* 規則 */
/* 上下兩代直系關系 */
father(X,Y)    :- parent(X,Y), male(X).
mother(X,Y)    :- parent(X,Y), female(X).
child(X,Y)     :- parent(Y,X).
son(X,Y)      :- parent(Y,X), male(X).
daughter(X,Y)   :- parent(Y,X), female(X).
/* 祖孫關系 */
grandparent(X,Y)  :- parent(X,Z), parent(Z,Y).
grandfather(X,Y)  :- grandparent(X,Y), male(X).
grandmother(X,Y)  :- grandparent(X,Y), female(X).
grandchild(X,Y)  :- grandparent(Y,X).
grandson(X,Y)   :- grandparent(Y,X), male(X).
granddaughter(X,Y) :- grandparent(Y,X), female(X).
/* 平輩關系 */
/* 若X與Y有相同的父輩Z,且X不是Y,則X與Y是同胞*/
sibling(X,Y)    :- parent(Z,X), parent(Z,Y), X"==Y.
brother(X,Y)    :- sibling(X,Y), male(X).
sister(X,Y)    :- sibling(X,Y), female(X).
cousin(X,Y)    :- parent(Z,X), parent(W,Y), sibling(Z,W).
/* 上下兩代旁系關系 */
uncle(X,Y)     :- parent(Z,Y), brother(X,Z).
aunt(X,Y)     :- parent(Z,Y), sister(X,Z).
nephew(X,Y)    :- parent(Z,X), sibling(Z,Y), male(X).
niece(X,Y)     :- parent(Z,X), sibling(Z,Y), female(X).
/* 定義一個普適關系relation,方便查詢 */
relation(R, X, Y)    :-relations(Rs), member(R,Rs), Q =..[R,X,Y], call(Q).
/* 事實 */
/* 關系列表 */
relations([parent,father,mother,son,daughter,grandparent,grandfather,
grandmother,grandchild,grandson,granddaughter,
        sibling,brother,sister,cousin,uncle,aunt,nephew,niece]).
parent(a,b). parent(a,c). parent(a,d).
parent(b,e). parent(b,f).
parent(c,g).
parent(d,h). parent(d,i). parent(d,j).
male(a).
male(b).
male(c).
female (d).
female (e).
male(f).
female (g).
male(h).
female (i).
male(j).

歎號沒有看出名堂:“Prolog代碼並不比Java代碼簡短多少啊。”

“評價代碼的復雜度,長短只是一個因素。程序員不是打字員,花在思考上的時間和精力遠遠超過花在鍵盤上。”冒號指出,“就拿此例來說,Java代碼雖然並不復雜,但有不少的選擇分支語句,次序很重要。稍有不慎,就會出現邏輯錯誤。另外如果我們把關系分得更細致些,比如區分叔伯舅、姑姨嬸、堂兄表妹等;再加入姻親關系,比如姑嫂婆媳、妯娌連襟等。這時你再來改寫這段代碼試試?”

引號聽得頭皮有些發麻:“那一定需要不少重重嵌套的if-else語句了。”

問號提出的問題更讓人頭痛:“如果我們不限於三代,再加上曾孫女、曾叔父之類的關系呢?”

逗號聯想到一則笑話:“話說一對父子與一對母女聯姻,作父親的娶了那位女兒,作兒子的娶了那位母親。本來關系已經夠顛倒錯亂了,雪上加霜的是這兩對夫婦又各自有了子女,那位父親終於精神崩潰了。”

大家哄笑著:這下徹底亂套啰。

“前面的Java代碼之所以沒有嵌套,得益於及時退出的一些return語句。如果考慮到超過三代的關系以及多重交叉的關系,許多語句都得改寫。可見上述代碼是多麼地脆弱!” 冒號就棍打腿,“再看Prolog代碼,如果要求更細的血親關系、增加姻親關系或三代以上的關系,只需引入新的規則和事實即可,不會影響原有代碼。下面列出幾個示范語句——”

/* 規則 */
/* 配偶原則 */
father(X,Y)     :- spouse(Z,X), mother(Z,Y).
mother(X,Y)    :- spouse(Z,X), father(Z,Y).
husband(X,Y)   :- spouse(X,Y), male(X).
wife(X,Y)      :- spouse(X,Y), female(X).
/* 父系的堂、姑兄弟姐妹 */
paternal_cousin(X,Y) :- father(Z,X), father(W,Y), sibling(Z,W).
/* 母系的舅、姨兄弟姐妹 */
maternal_cousin(X,Y) :- mother(Z,X), mother(W,Y), sibling(Z,W).
/* 姻親關系 */
father_in_law(X,Y) :- spouse(Y,Z), father(X,Z).
mother_in_law(X,Y) :- spouse(Y,Z), mother(X,Z).
son_in_law(X,Y)  :- spouse(X,Z), daughter(Z,Y).
daughter_in_law(X,Y) :- spouse(X,Z), son(Z,Y).
/* 曾祖孫關系 */
great_grandparent(X,Y) :- grandparent(Z,Y), parent(X,Z).
great_grandchild(X,Y):- grandchild(Z,Y), child(X,Z).
/* 事實 */
/* 新引入的關系 */
relations([husband,wife, paternal_cousin,maternal_cousin,
father_in_law,mother_in_law,son_in_law,daughter_in_law,
great_grandparent,great_grandchild]).
parent(pa,a).
spouse(a,as).
spouse(ds,d).
spouse(cs,c).

句號方悟其妙:“這樣的代碼既無層層嵌套,也無次序分別。比起過程式,編寫輕松得多,程序的可維護性和可擴展性也更高。”

“此外另有妙處。邏輯式與過程式和函數式的一個不同之處是,它沒有明顯的輸入、輸出之分。上面的程序不僅可以用來判斷任意二人之間的關系,還能倒過來通過關系來找人。”冒號板書了幾行字——

輸入查詢:relation(R,a,ds)     /* a與ds的關系是什麼? */
輸出結果:R=father_in_law
輸入查詢:great_grandparent (pa,X) /* pa是誰的曾祖?*/
輸出結果:X=e;X=f;X=g; X=h; X=i; X=j;

引號義務作翻譯:“這告訴我們兩件事:a與ds是翁婿關系,pa有曾孫e、f、g、h、i和j。”

“邏輯式語言著眼於關系而非函數,對付這類問題正是它的拿手好戲。”冒號聲音逐漸高亢,“大家應該都聽說過等式‘算法+數據結構=程序’吧?這是Pascal設計者Niklaus Wirth的一本著作的書名,它刻畫了過程式尤其是結構化編程的思想。後來Robert Kowalski進一步提出:算法=邏輯+控制。其中邏輯是算法的核心,控制主要用於改進算法的效率。在邏輯式編程中,程序員只需表達邏輯,而控制交給編程語言的解釋器或編譯器去管理。”

“所以程序員的負擔大大減輕了。”問號接口道,“邏輯式編程聽起來真是不錯,但不知Prolog程序能否與Java程序對接呢?”

冒號回答:“任何程序之間的對接都是可能的,只是不同的對接方式在復雜度和效率上有所差異而已。除了通過程序之間的通訊(如socket)或可執行文件的直接調用外,Prolog與C、C++、Java、C#、VB、Perl、JavaScript等多種語言之間,還能借助工具進行源代碼轉換[2]或通過雙向編程接口互嵌代碼。具體到Java,一方面可以通過JNI (Java Native Interface)與Prolog引擎相連[3],另一方面可以利用Prolog引擎的Java實現來完成JVM上的集成[4]。”

句號請求:“能否總結一下邏輯式編程的優缺點?”

冒號欣然應允:“由於邏輯式編程模擬人類的邏輯思維,故而在機器證明、專家系統、自然語言處理、博弈等人工智能領域如魚得水,同時在非學術領域的知識管理、智能決策分析等方面也能大顯身手。同為聲明式,它與函數式一樣比命令式更簡潔、更抽象、更少副作用,運用得當能大大提高生產效率,還能用於快速原型(rapid prototyping)開發。但缺點是運行效率偏低,可掌控性較差,與常規的過程式思維差異較大,更適合基於規則(rule-based)而不是基於狀態(state-based)的應用[5]。此外,相對而言邏輯式語言還不夠成熟和完善。”

逗號“抗議”道:“我怎麼感覺經過這麼一反刍,胃裡的負擔更重了?”

冒號略帶歉意地笑了笑:“在所有編程范式中,函數式與邏輯式與傳統思維方式的差別最大,此前的介紹又過於簡單,因此今天特意多談了些。既然有人提意見,那就我就適可而止了。最後請允許我畫蛇添足:在代表計算機最高水平的人工智能領域中,這兩種范式發揮著舉足輕重的作用。單憑這一點,它們也是值得學習和借鑒的。好了,大家先休息十分鐘,出去活動活動筋骨吧。”

,插語

[1] 用數學邏輯的話來說,事實與規則是公理,查詢是定理。

[2] 如Prolog Café和P#能分別將Prolog代碼轉化為Java代碼和C#代碼。

[3] 比如JPL通過JNI與Prolog FLI (Foreign Language Interface)將Java與SWI-Prolog橋接起來。

[4] 比如JIProlog(Java Internet Prolog)是一個用Java實現的Prolog解釋器,為Java和Prolog提供雙向API。類似的還有JLog等。

[5] 交互式或事件驅動式應用通常是基於狀態的。

總結

代碼的長度不是衡量軟件復雜度的唯一標准。其中的邏輯結構越復雜、越微妙、受需求變化的影響越大,軟件越難控制和維護。

算法=邏輯+控制。邏輯式編程將算法中的控制部分大都移交給編程語言,編程人員主要關注算法的核心邏輯。這樣大大減輕了程序員的負擔,編碼也更簡潔易懂,更具可維護性和可擴展性。

有別於過程式和函數式,邏輯式沒有明顯的輸入和輸出之分。

邏輯式編程不僅適用於人工智能方面的學術領域,同樣廣泛適用於各種涉及知識管理、決策分析等方面的應用領域。

相對於命令式,邏輯式更簡潔、更抽象、更少副作用,能提高生產效率,還能用於快速原型開發。但在運行效率、可掌控性、語言成熟度等方面有所欠缺。另外,因其思維方式獨特而鮮為人用,適合基於規則而非基於狀態的應用 。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved