若說學程式是訓練邏輯的好方法,想必大部分程式人基本上都不會反對。因為,許多開發者也會說,撰寫程式解決問題時,真正重要的是使用正確、清晰的邏輯,來表達對這些問題的想法......,只不過在這類話語中,所謂的邏輯,到底是指什麼呢?邏輯學裡的「邏輯」?
有沒有可能完全地使用「邏輯」來寫程式,若能真能如此,又會是一種什麼樣的體驗?
程式中的「邏輯」
若請你來解釋一下什麼是程式設計上的邏輯,你會如何解釋?仔細想想,當程式人談到程式設計邏輯時,大部份時間指的都是些很模糊的概念。
籠統來說,應該是指面對問題時,試圖以程式設計角度來解決的思考方式。
進一步地說,就是從程式設計面來看,問題該如何分解、解題時的順序為何、表達問題的方式,甚至於抽象化的能力等,所以,如果能合理地運用這些邏輯,問題就能使用程式適當地解決。
如果解釋的對象,是完全沒有接觸過程式設計的人,這樣的解釋必然不夠具體。
此時,也許就該搬出幾個基本語法來說明了,像是用於條件分支的if...else,重複執行的for迴圈,以及結合或修正條件式的and、or、not等;接著,就舉個問題作為範例,示範一下如何將問題分解,直到能使用這些基本元素,以逐步表達出解決問題的方式,這時對方通常就比較能理解一些了,他可能會說:「喔!if、and、or、not......這些確實是邏輯,原來所謂程式設計的邏輯思考是這樣的啊!」
但其實,你心裡想的是:「才不只有這些」。因為,程式語言中有許多語法與元素--語言之外,還有資料結構、演算法等的設計,而不同程式語言還會有不同的表達方式,並不是只有if、and、or、not這些「邏輯」,反正把這些人騙進來學了之後,自然就知道程式設計的邏輯思考是怎麼一回事。
如果只使用「邏輯」?
談到if、and、or、not這些貼近我們生活上思考經驗的邏輯,其實是被稱為一階謂詞邏輯(First-Order Predicate Logic,FOPL)的東西。
事實上,的確是可以完全使用「邏輯」來寫程式,而且神奇地,不必寫下解題的步驟,只要告知事實(Fact),以及關係之間的推斷規則(Inference rule),剩下的就是交給電腦來推理了。
舉個例子來說,如果使用Prolog這門基於邏輯的語言,若已知justin是irene的父母這個事實,我們可以寫成parent(justin,irene).,而且,現在,有下列已知的事實:
parent(justin,irene). parent(irene,bush). parent(witch,rebecca).
就目前而言,我們其實可以單純地提出下列詢問parent(justin,rebecca),而由於這顯然不符合已知的事實,因此結果會是false,而根據以上的事實,雖然可以自行推理並寫出ancestor(justin,bush)這樣的事實,來表達出justin是bush的直系祖先,不過按照直系祖先的推斷邏輯,可以使用Prolog撰寫:
ancestor(X,Y):-parent(X,Y). ancestor(X,Y):-parent(X,Z),ancestor(Z,Y).
第一條規則表示,如果parent(X,Y)成立,那麼ancestor(X,Y)就成立。接著第二條規則是指,如果能找到一個Z,使得X為Z的父母,而且Z為Y的直系祖先,那麼X就是Y的直系祖先。現在,若詢問ancestor(justin,bush),Prolog就會自行推理出true的結果,進一步地,還可以詢問ancestor(X,bush),那麼,Prolog會告知X可以是irene或justin。
如何表達重複呢?
有興趣的話,可以使用其他非基於邏輯的語言,試著實現相同的功能,馬上就會發現,這時除了宣告事實與推斷規則之外,還得列出每個可能的演算步驟。
例如,也許使用Python定義一個parent(x, y)函式,並且,使用迴圈走訪["justin", "irene", "bush"]、["witch", "rebecca"]清單,看看清單中x的下一個是不是y。但這還只是實現了true或false的判斷,若要能透過ancestor(X,bush)就傳回X的可能清單,還得寫下更多的步驟。
能夠自行根據事實與規則來進行推理,確實是很神奇,然而,若只使用「邏輯」,如何能表現出重複呢?
例如,怎麼對清單中數字進行加總?首先,對清單加總來說,一個事實就是空清單的加總值是0,這可以寫為sum(0,[]).,接著,如果有個清單Tail的加總值是Sum,而且有個值Total是Head + Sum,那麼清單[Head|Tail]的加總值,就是Total,使用Prolog來表達,就是:
sum(0,[]). sum(Total,[Head|Tail]):-sum(Sum,Tail),Total is Head + Sum.
如果曾經涉獵過函數式語言,乍看之下,會覺得這與函數式中定義加總函式很像。
確實,函數式或者基於邏輯的語言都是宣告式風格,不同的是,函數式宣告的是將輸出對應至輸出的(數學)函式為何,而基於邏輯的語言是宣告既有的事實,以及推斷規則為何。
因此,sum(Total,[Head|Tail])並不是將Total與[Head|Tail]傳入sum函式,而是指[Head|Tail]的加總是Total,而:-之後,是指sum(Total,[Head|Tail])能夠成立的條件。
不過,兩者都使用了遞迴來達到重複的表達目的,就基於邏輯的語言來說,複雜的邏輯可由許多較不複雜的邏輯推斷而來。
因此,對於平均值的計算,我們可以增加以下的事實與規則:
count(0,[]). count(Count,[Head|Tail]):- count(TailCount,Tail),Count is TailCount + 1. average(Average,List):- sum(Sum,List),count(Count,List),Average is Sum/Count.
解讀average(Average,List)的方式是:如果List的加總是Sum,而且List的加總是Count,Average是Sum/Count,那麼,List的平均就是Average。
想要看看更複雜一些的邏輯,是怎麼寫成的嗎?我們可以趁機看一下快速排序法的版本(https://goo.gl/T05rJU),訓練你的「邏輯」能力。
真正地訓練「邏輯」
也許你發現了,大部份的程式語言在撰寫程式時,只是使用了一些「邏輯」,很少完全地用「邏輯」來寫程式。
而所謂的程式設計邏輯,通常指的是一種思考方式,就像命令式是其中一種思考邏輯,函數式是其中一種思考邏輯,而完全地基於「邏輯」也是一種思考邏輯。
如果對Prolog這類基於邏輯的語言產生了興趣,好奇它如何能在不寫出演算法的過程中推理出結果,可以看看《Good Math》中第四部份〈Logic〉,就能理解背後神奇的原理。如果想要看到更多Prolog的例子,可以看看《Seven Languages in Seven Weeks》中〈Prolog〉的章節,裏頭也有更多Prolog的相關文件資源。
完全地用「邏輯」來寫程式,將會是個非常有趣的體驗,因為,在面對問題時,能真正地訓練以「邏輯」思考的能力。
這會讓你腦袋翻轉過來,驚訝還有這樣的思考邏輯(上次有這種感覺是什麼時候了呢?)所以,下次,若還有人問到「程式設計的邏輯為何?」,你也就能更清楚且確切地加以回答了!