冒號(hào)課堂
第四課 重溫范式(1)
課前導(dǎo)讀
本課對(duì)函數(shù)式編程與邏輯式編程作了更詳細(xì)的展開,并對(duì)前面介紹的范式進(jìn)行了匯總分析,最后用情景式編程貫穿所學(xué)范式。
本課共分四節(jié)——
- 函數(shù)范式
- 邏輯范式
- 匯總范式
- 情景范式
4.1函數(shù)范式——精巧的數(shù)學(xué)思維
知不知,上;不知不知,病 ——《老子·德經(jīng)》
關(guān)鍵詞:編程范式,函數(shù)式編程,Haskell,Groovy
摘要: 再談函數(shù)式編程
?提問(wèn)
- 掌握編程范式對(duì)編程語(yǔ)感的提高有什么作用?
- 為什么聲明式程序一般比命令式程序更簡(jiǎn)潔?
- 函數(shù)式編程有哪些特征?為何簡(jiǎn)潔而不失強(qiáng)大?
- 函數(shù)的無(wú)副作用性的意義何在?
- 相比過(guò)程式和OOP,函數(shù)式編程的弱點(diǎn)是什么?
:講解
眾人落座之后,冒號(hào)鳴鑼開場(chǎng):“上兩節(jié)課為大家介紹了多種編程范式,雖未將所有類型盡囊其中,但最具代表性的均在其列。我們也不必貪多求全,俗話說(shuō)得好:貪多嚼不爛啊。現(xiàn)在給大家一個(gè)知識(shí)反芻的機(jī)會(huì)。”
問(wèn)號(hào)正感求之不得:“總算可以喘口氣了。我們就像觀光客,被導(dǎo)游帶著從一個(gè)景點(diǎn)趕往另一景點(diǎn)。一天下來(lái),雖然大開眼界,但都是走馬觀花,無(wú)法充分領(lǐng)略各地的風(fēng)光。”
“你說(shuō)得沒錯(cuò),我就是那個(gè)不近情理的導(dǎo)游。”冒號(hào)哈哈一笑,“類似時(shí)下流行的歐洲N國(guó)M日游,大部分人的收獲就是一堆照片和日漸模糊的記憶。不出多日,如果不看標(biāo)注,八成連照片上的背景是在法國(guó)還是在意大利都分不清了。”
逗號(hào)頗有同感:“差不多,目前我的收獲就是一堆幻燈片和似懂非懂的概念。”
冒號(hào)料有此果:“這一點(diǎn)也不奇怪。別說(shuō)幾天游一個(gè)國(guó)家,單一個(gè)羅馬,沒有一個(gè)月是不可能深入了解的。至于編程范式,單一個(gè)OOP,沒有兩年以上的實(shí)踐和思考,是難以真正領(lǐng)會(huì)其精髓的。”
嘆號(hào)深表懷疑:“OOP需要兩年以上才能領(lǐng)會(huì)?!”
“那還得看你是否有足夠的勤奮和悟性。”冒號(hào)加強(qiáng)了語(yǔ)氣,“前面說(shuō)過(guò),單靠記憶只能觸及知識(shí)之表,單靠練習(xí)只能深入知識(shí)之里,唯有培養(yǎng)方能滲透知識(shí)之根。編程范式正處知識(shí)的根部,你們又怎能奢望只聽?zhēng)滋谜n即豁然貫通呢?”
引號(hào)表達(dá)自己的感受:“雖然學(xué)了不少東西,但也存了不少疑惑,擱在心里有點(diǎn)不舒服。”
“我明白你的意思。凡事追根究底是一種良好的學(xué)習(xí)習(xí)慣,也是一種可貴的學(xué)習(xí)精神。” 冒號(hào)表示理解和肯定,“但學(xué)習(xí)如打仗,除了要有直線式的縱深攻擊,還要有曲線式的迂回包抄。回顧我們中學(xué)的課堂,往往是每引入一個(gè)概念或理論,便圍繞其作深入的學(xué)習(xí)和反復(fù)的練習(xí)。在此過(guò)程中的種種疑惑,隨著學(xué)習(xí)的深入都會(huì)煙消云散。這樣穩(wěn)扎穩(wěn)打、層層推進(jìn),學(xué)得扎實(shí),心里也踏實(shí)。但這種方法并不總是最好的,尤其在面臨動(dòng)態(tài)的、開放的知識(shí)體系時(shí),難免左支右絀。為此,我們必須學(xué)會(huì)適度地容忍無(wú)知。請(qǐng)注意,容忍無(wú)知不是放任無(wú)知,而是一種學(xué)習(xí)的技巧,讓無(wú)知成為求知的動(dòng)力而不是障礙。容忍無(wú)知能使我們既不沮喪氣餒,也不急于求成。在學(xué)習(xí)時(shí)不妨略過(guò)一些細(xì)節(jié)或難點(diǎn),先概覽全貌以獲取感性認(rèn)識(shí),然后在逐步積累中升華為理性認(rèn)識(shí)。要而言之,我們不僅需要強(qiáng)調(diào)鉆勁和深度的‘釘子精神’,還需要強(qiáng)調(diào)磨功和廣度的‘刨子精神’。我一口氣兜售這么多編程范式,就是為了刺激大家求知欲,同時(shí)為大家進(jìn)行第一道刨磨。”
引號(hào)得到一些安慰:“看來(lái)今后我們還會(huì)故地重游的。”
“不僅會(huì)重游,而且會(huì)‘深度游’。” 冒號(hào)肯定地說(shuō),“此番我們一路行色匆匆,若能感受到途中景色帶來(lái)的感官?zèng)_擊,便算是不枉此行了。其實(shí),把編程范式類比旅游景點(diǎn)并不十分準(zhǔn)確,或許比作當(dāng)?shù)氐娘L(fēng)俗文化更確切些。”
句號(hào)立刻會(huì)意:“景點(diǎn)是具體的,背后的風(fēng)俗文化是抽象的;編程語(yǔ)言是具體的,背后的編程范式是抽象的。”
“此乃其一。”冒號(hào)右手伸出食指,“其二,如果不了解景點(diǎn)的歷史文化和風(fēng)俗人情,僅僅滿足于表面的奇觀異景,一切很快便會(huì)淡忘。比如說(shuō),你若不了解圣經(jīng)文化、不了解文藝復(fù)興史,則歐洲之行至多只是視覺的盛宴,而非文化的洗禮,收獲將是有限的,印象將是膚淺的。同樣,如果你不了解編程范式,那么眼中的編程語(yǔ)言只是語(yǔ)法、語(yǔ)義、核心庫(kù)、規(guī)范等組成的集合,寫出的代碼雖能編譯、能工作,卻會(huì)顯得生硬、別扭。就像中式英語(yǔ),語(yǔ)法正確、表達(dá)也正確,可就是不正宗、不地道。其癥結(jié)我們?cè)诘谝还?jié)課中已經(jīng)提過(guò)了,即所謂的語(yǔ)感缺失。”
問(wèn)號(hào)實(shí)話實(shí)說(shuō):“可我還是不明白編程范式如何提高我們的編程語(yǔ)感。”
“那就讓我們?cè)僬f(shuō)說(shuō)范式吧。”冒號(hào)并不著急,“范式可以粗略理解為模范、模型、模式、風(fēng)格、流派等等。軟件中的范式除了編程范式外,還有架構(gòu)范式[1]、數(shù)據(jù)庫(kù)范式[2]等。比如,對(duì)象導(dǎo)向式(Object-Oriented)可以應(yīng)用于編程、架構(gòu)和數(shù)據(jù)庫(kù)中,分別成為OOP(Object-Oriented Programming)、OOA(Object-Oriented Architecture)和OODB(object-oriented database);類似地,事件驅(qū)動(dòng)式(Event-Driven)可以是一種編程范式,可以是一種架構(gòu)模型,也可以是一種設(shè)計(jì)模式。總之,每種范式都代表著一套獨(dú)特而有效的解決問(wèn)題的思想和方法。掌握范式對(duì)編程語(yǔ)感的提高至少有兩層作用:首先,編程語(yǔ)言的語(yǔ)法、語(yǔ)義等都是從編程范式的樹根衍生而出的枝葉,把握了這種脈絡(luò)和節(jié)奏,代碼才會(huì)如音樂舞蹈般韻律有致;其次,每種范式擅長(zhǎng)的問(wèn)題領(lǐng)域不盡相同,只有博聞廣識(shí),方可揚(yáng)長(zhǎng)避短,程序才會(huì)如行云流水般流暢自然。”
逗號(hào)添油加醋:“武功練至化境,一定是博采眾長(zhǎng),就像楊過(guò)融合了東邪、西毒、南帝、北丐、中神通等各派武功,才能隨心所欲地打出黯然銷魂掌來(lái)。”
提起武俠人物,眾人俱是逸興遄飛,哪能體會(huì)到半點(diǎn)黯然消魂之傷?
冒號(hào)道:“天下之理,殊途同歸。我們停止玄玄之論,用實(shí)例來(lái)說(shuō)明吧。誰(shuí)來(lái)介紹一下快速排序法(quicksort)?”
眾人飛舞的思緒漸漸收斂,終于由引號(hào)作答:“快速排序法的思想是:在列表中找一個(gè)基準(zhǔn)元素,將所有小于它的元素劃歸一個(gè)子列,置于其前;將所有大于等于它的元素劃歸另一子列,置于其后。然后遞歸地對(duì)前后兩個(gè)子列作同樣處理,直至最終。”
“很好,讓我們用Java來(lái)實(shí)現(xiàn)一下該算法。”冒號(hào)顯示出一段代碼——
/** 快速排序法的Java實(shí)現(xiàn) */
public class Sorter
{
public static <T extends Comparable<? super T>> void qsort(T[] list)
{
qsort(list, 0, list.length - 1);
}
private static <T extends Comparable<? super T>> void qsort(T[] list, int low, int high)
{
if (low >= high) return;
int i = low - 1, j = high + 1;
T pivot = list[low]; // 基準(zhǔn)元素
for ( ; ; )
{
do { ++i; } while (list[i].compareTo(pivot) < 0);
do { --j; } while (list[j].compareTo(pivot) > 0);
if (i >= j) break;
// 交換
T tmp = list[i]; list[i] = list[j]; list[j] = tmp;
}
// 找到分割點(diǎn)j,遞歸
qsort(list, low, j);
qsort(list, j + 1, high);
}
}
“請(qǐng)問(wèn)這里用到了哪些編程范式?”冒號(hào)提問(wèn)。
嘆號(hào)心想,有何難哉?遂答:“既然是用Java實(shí)現(xiàn)的,自然少不了OOP。同時(shí)為了使算法更具普適性,還用到了泛型編程。”
“你好像忘記了最重要的過(guò)程式,反倒是OOP的色彩極淡。”冒號(hào)顯然不滿意他的答案。
嘆號(hào)不解:“不是說(shuō)Java是100%的OOP語(yǔ)言嗎?”
冒號(hào)頗為不屑:“不要輕信這種浮淺之論。且不說(shuō)Java的基本類型(primitive type)不屬于類(class),本就不是100%的OOP,即使是100%的OOP,那與過(guò)程式也不矛盾啊。此例中的Sorter類連一個(gè)實(shí)例成員(instance member)也沒有,唯一與OOP沾邊的是作為interface的Comparable,在C中也可用函數(shù)指針代替。如果不考慮泛型式的特征,本例無(wú)論用Java還是用C,并沒有本質(zhì)差別。事實(shí)上,對(duì)于這類純算法的問(wèn)題,OOP范式本無(wú)太多用武之地。換句話說(shuō),quicksort雖然是通過(guò)以OOP著稱的Java來(lái)實(shí)現(xiàn)的,但用的主要還是過(guò)程式的思想和方法。”
問(wèn)號(hào)趕緊問(wèn)道:“還能用其他范式來(lái)實(shí)現(xiàn)嗎?”
此問(wèn)正合冒號(hào)之意:“我們改用純函數(shù)式語(yǔ)言Haskell來(lái)試試——”
-- 快速排序法的Haskell實(shí)現(xiàn)
qsort :: (Ord a) => [a] -> [a] --函數(shù)聲明
qsort[] = [] --遞歸終點(diǎn)
qsort(pivot : rest) = qsort[x| x <- rest, x < pivot] --對(duì)前面的子列遞歸
++ [pivot]
++ qsort[x| x <- rest, x >= pivot] --對(duì)后面的子列遞歸
嘆號(hào)幾不能信:“竟然可以如此精煉?”
“上面的Java代碼很難再精簡(jiǎn)了,但與Haskell代碼相比還是太冗長(zhǎng)了。后者省去了所有的賦值、迭代和流程控制,只有單純的遞歸,反映了典型的函數(shù)式特征。”冒號(hào)解說(shuō)著,“鑒于你們對(duì)Haskell不太熟悉,我稍微解釋一下。第一步,聲明函數(shù)類型[3]:同類型列表之間的變換,其中Ord可類比Java中的Comparable,以保證列表元素之間能進(jìn)行比較;第二步,聲明遞歸終點(diǎn):空列排序后仍是空列;第三步,描述遞歸原則:基準(zhǔn)元素pivot與剩余子列rest進(jìn)行排序后的列表,正是將小于基準(zhǔn)的子列和超過(guò)基準(zhǔn)的子列分別排序,中間插入基準(zhǔn)元素后的結(jié)果。”
句號(hào)思有所得,不禁喜形于色:“我明白了,這兩段代碼生動(dòng)地反映了命令式編程與聲明式編程之間的差別:前者需要指定計(jì)算的過(guò)程,后者只需指定計(jì)算的原則。一個(gè)著重微觀的細(xì)節(jié),一個(gè)著重宏觀的方向,自有繁簡(jiǎn)之別。”
冒號(hào)亦有所慰:“非常好!類似的話我以前也說(shuō)過(guò),但你們自己說(shuō)的才是真正的收獲啊。我們還提過(guò),過(guò)程式與函數(shù)式的差別同時(shí)也是機(jī)器思維與數(shù)學(xué)思維的差別。不妨對(duì)比Haskell表達(dá)式與數(shù)學(xué)中的集合表達(dá)式,它們是多么地相近!”
黑板上出現(xiàn)兩行式子——
數(shù)學(xué)表達(dá)式: {x| x ∈ rest, x < pivot}
Haskell表達(dá)式:[x| x <- rest, x < pivot]
逗號(hào)仔細(xì)打量著:“嗯,的確像,跟哥倆似的,連符號(hào)<-都是仿照集合屬于符∈的。”
“還有另一種表達(dá)方法。”冒號(hào)又添加了一行——
Haskell表達(dá)式2:(filter (< pivot) rest)
“雖然與前一表達(dá)式的簡(jiǎn)潔度相差無(wú)幾,但可讀性更強(qiáng)。filter即是過(guò)濾,將列表rest中的元素進(jìn)行篩選,條件是小于基準(zhǔn)元素。”冒號(hào)講解道。
問(wèn)號(hào)略感迷惑:“(< pivot)的用法看起來(lái)有點(diǎn)怪異。”
“它是一個(gè)函數(shù),也是filter的第一個(gè)參數(shù),用來(lái)判斷第二個(gè)參數(shù)rest的元素是否合格,即 < pivot。這體現(xiàn)了函數(shù)式的一個(gè)重要特征:函數(shù)是頭等公民(first-class citizen)[4]——可作為傳遞參數(shù)、可作為表達(dá)式的值、可嵌入數(shù)據(jù)結(jié)構(gòu)、也可與某變量綁定,與普通的基本數(shù)據(jù)類型毫無(wú)二致。這是函數(shù)式編程簡(jiǎn)潔而強(qiáng)大的重要根源。”冒號(hào)細(xì)加解釋,“大家還記得上節(jié)課談到的回調(diào)函數(shù)吧?callback無(wú)非是將函數(shù)作為參數(shù)來(lái)傳遞,本質(zhì)上是將代碼當(dāng)數(shù)據(jù)來(lái)使用,回調(diào)機(jī)制的巨大威力均拜此高級(jí)用法所賜。”
眾人又一段筋脈被打通了。
引號(hào)提出一個(gè)很實(shí)際的問(wèn)題:“函數(shù)式編程的確很酷,可Java并不支持。如果采用Haskell之類的函數(shù)式語(yǔ)言,會(huì)不會(huì)帶來(lái)系統(tǒng)集成上的困難?”
冒號(hào)打消了他的疑慮:“Java平臺(tái)下已經(jīng)集成了不少的支持函數(shù)式編程的語(yǔ)言,如JRuby、Jython、Groovy、Scala等,甚至Haskell在JVM下也有相應(yīng)的Jaskell。其中Groovy與Java的結(jié)合最為自然,我們看一下它是如何實(shí)現(xiàn)quicksort的——”
/** 快速排序法的Groovy實(shí)現(xiàn) */
def qsort(list) {
if (list.size() <= 1) return list
def pivot = list[0]
return (qsort(list.findAll{x -> x < pivot})
+ list.findAll{x -> x == pivot}
+ qsort(list.findAll{x -> x > pivot}))
}
“雖然比Haskell的代碼略長(zhǎng)了些,并且還帶著過(guò)程式的烙印,但總體思想還是函數(shù)式的。”冒號(hào)緊扣本質(zhì),“函數(shù)式還有一個(gè)重要特征:無(wú)副作用或盡量減少副作用[5]。所謂無(wú)副作用,是指一個(gè)函數(shù)在被調(diào)用前后保持程序的狀態(tài)不變。無(wú)副作用的函數(shù)不會(huì)改變非局部變量的值,不會(huì)改變傳入的參數(shù),也沒有I/O操作。”
逗號(hào)脫口而出:“什么狀態(tài)都不變,那這樣的函數(shù)有什么用?”
冒號(hào)不以為奇:“你的這種想法源自根深蒂固的命令式思維。我們?cè)衙钍匠绦虮茸鳡顟B(tài)自動(dòng)機(jī),其運(yùn)行過(guò)程就是在不斷地修改機(jī)器的狀態(tài)。而函數(shù)式程序則是進(jìn)行表達(dá)式變換,一般不會(huì)改變變量的值。其實(shí)函數(shù)式并非完全不改變內(nèi)存,只不過(guò)改變的是棧內(nèi)存(stack)罷了。換言之,無(wú)副作用函數(shù)的作用關(guān)鍵在于其估值結(jié)果,按過(guò)程式的說(shuō)法是返回值。”
逗號(hào)如夢(mèng)初醒。
問(wèn)號(hào)仍有疑問(wèn):“藥物最好沒有副作用,函數(shù)沒有副作用的好處是什么?”
冒號(hào)嘴一咧:“好處太多了。首先,沒有副作用的函數(shù)易于重構(gòu)、調(diào)試和單元測(cè)試。其次,代碼有效性與函數(shù)順序無(wú)關(guān),方便并發(fā)處理和優(yōu)化處理。舉個(gè)簡(jiǎn)單的例子,計(jì)算兩個(gè)函數(shù)的乘積:f(x) * g(y)。由于無(wú)副作用,f(x) 和g(y)的估值過(guò)程是獨(dú)立的,估值順序也不重要,因此理論上可以對(duì)二者并行計(jì)算。另外,還可利用惰性計(jì)算(lazy evaluation):如果算出f(x)為零,那么不用計(jì)算g(y)便可知乘積為零了。”
嘆號(hào)忍不住贊嘆:“聽起來(lái)真不錯(cuò)!”
冒號(hào)言猶未盡:“最后,沒有副作用的函數(shù)是引用透明的(referential transparency),即一個(gè)表達(dá)式隨時(shí)可以用它的值來(lái)替換[6],正如數(shù)學(xué)中的函數(shù)一樣,保證了數(shù)學(xué)思維的貫徹和運(yùn)用。”
引號(hào)自感獲益頗豐:“前面介紹范式時(shí),覺得函數(shù)式最為神秘。現(xiàn)在總算有了些感性認(rèn)識(shí)了。”
冒號(hào)道出緣由:“函數(shù)式編程不僅有許多獨(dú)特的概念和方法,還有很深的數(shù)學(xué)背景——λ-演算(λ-Calculus)。如果一開始便傾囊相授,你們必會(huì)望而卻步,我豈不是打草驚蛇了?”
眾人始覺:老冒原來(lái)是在誘敵深入啊。
“盡管函數(shù)式有這么多優(yōu)點(diǎn),運(yùn)算能力從理論上比諸過(guò)程式也毫不遜色[7],但一直沒有成為主流范式,”冒號(hào)話鋒一轉(zhuǎn),“細(xì)究之,至少有兩方面的原因:主觀上,程序員更習(xí)慣機(jī)器風(fēng)格的過(guò)程式思維和現(xiàn)實(shí)風(fēng)格的OOP思維,不容易接納數(shù)學(xué)風(fēng)格的函數(shù)式思維;客觀上,函數(shù)式語(yǔ)言在表現(xiàn)力和運(yùn)行效率等方面與過(guò)程式和OOP語(yǔ)言也有一定的差距。饒是如此,支持它的語(yǔ)言還是越來(lái)越多,其簡(jiǎn)潔精巧的特性也為越來(lái)越多的人所青睞。它的整體應(yīng)用雖然主要集中于數(shù)學(xué)計(jì)算、人工智能等領(lǐng)域,但局部應(yīng)用早已遍地開花了。”
,插語(yǔ)
[1] 如OOA(Object-Oriented Architecture),COA(Component-Oriented Architecture),SOA(Service- Oriented Architecture) 、EDA(Event-Driven Architecture)等。
[2] 如關(guān)系數(shù)據(jù)庫(kù)(relational database)、對(duì)象導(dǎo)向式數(shù)據(jù)庫(kù)(object-oriented database)等。
[3] 這一步可省略,但出于對(duì)代碼的清晰度以及性能、調(diào)試等方面的考慮,最好保留。
[4] 這類函數(shù)更數(shù)學(xué)化的說(shuō)法是高階函數(shù)(higher-order function)。
[5] 沒有副作用的函數(shù)式語(yǔ)言被稱為純函數(shù)式(purely functional),如Haskell和SISAL;有副作用的被稱為非純函數(shù)式(impurely functional),如Lisp和ML。
[6] 這是因?yàn)楹瘮?shù)的無(wú)副作用性保證了相同的輸入一定有相同的輸出。
[7] λ-演算被證明是圖靈完備的。
。總結(jié)
- 學(xué)習(xí)知識(shí)之表須通過(guò)記憶,掌握知識(shí)之里須通過(guò)練習(xí),滲透知識(shí)之根須通過(guò)培養(yǎng)。編程范式正是知識(shí)之根。
- 適度地容忍無(wú)知也是一種學(xué)習(xí)技巧。
- “釘子精神”固然可貴,“刨子精神”也不可少。
- 不同的編程范式代表不同的解決問(wèn)題的思想和方法。不同的編程語(yǔ)言提倡不同的編程范式,并在語(yǔ)法上給予支持。只有掌握范式,才能更合理、更有效地運(yùn)用編程語(yǔ)言的語(yǔ)法和語(yǔ)義特征,并能針對(duì)不同的問(wèn)題領(lǐng)域使用不同的編程風(fēng)格,編寫的代碼才會(huì)和諧自然、富于美感。
- 命令式編程需要指定計(jì)算的過(guò)程,著重微觀的細(xì)節(jié);聲明式編程只需指定計(jì)算的原則,著重宏觀的方向。因此二者繁簡(jiǎn)有別。
- 在函數(shù)式編程中,函數(shù)是程序的核心,是頭等公民,一般沒有或很少副作用,同時(shí)沒有顯式的內(nèi)存管理。
- 由于函數(shù)式編程中的函數(shù)與基本數(shù)據(jù)類型平起平坐,故可將代碼作數(shù)據(jù)用,這是程序既簡(jiǎn)潔又強(qiáng)大的原因之一。回調(diào)機(jī)制采用的正是函數(shù)式風(fēng)格。
- 無(wú)副作用的函數(shù)容易重構(gòu)、調(diào)試和測(cè)試,便于并發(fā)和優(yōu)化處理,并能貫徹和運(yùn)用數(shù)學(xué)思維。
- 相比過(guò)程式和OOP,函數(shù)式編程思想過(guò)于數(shù)學(xué)化和抽象化,語(yǔ)言的表現(xiàn)力和運(yùn)行效率也有所不足。
“”參考
[1] Michael Lee Scott.Programming Language Pragmatics.San Francisco:Morgan Kaufmann,2000.589-620
[2] Stephen H. Kaisler.SOFTWARE PARADIGMS.New Jersey:Wiley,2005.23-24