冒號課堂§4.1:函數范式

           

          冒號課堂

          第四課 重溫范式(1)


          課前導讀

          本課對函數式編程與邏輯式編程作了更詳細的展開,并對前面介紹的范式進行了匯總分析,最后用情景式編程貫穿所學范式。

          本課共分四節——

          1. 函數范式
          2. 邏輯范式
          3. 匯總范式
          4. 情景范式

          4.1函數范式——精巧的數學思維

          知不知,上;不知不知,病                            ——《老子·德經》

           

          關鍵詞:編程范式,函數式編程,Haskell,Groovy

          摘要:   再談函數式編程

           

           提問


          • 掌握編程范式對編程語感的提高有什么作用?
          • 為什么聲明式程序一般比命令式程序更簡潔?
          • 函數式編程有哪些特征?為何簡潔而不失強大?
          • 函數的無副作用性的意義何在?
          • 相比過程式和OOP,函數式編程的弱點是什么?

            講解
           

          眾人落座之后,冒號鳴鑼開場:“上兩節課為大家介紹了多種編程范式,雖未將所有類型盡囊其中,但最具代表性的均在其列。我們也不必貪多求全,俗話說得好:貪多嚼不爛啊。現在給大家一個知識反芻的機會。”

          問號正感求之不得:“總算可以喘口氣了。我們就像觀光客,被導游帶著從一個景點趕往另一景點。一天下來,雖然大開眼界,但都是走馬觀花,無法充分領略各地的風光。”

          “你說得沒錯,我就是那個不近情理的導游。”冒號哈哈一笑,“類似時下流行的歐洲NM日游,大部分人的收獲就是一堆照片和日漸模糊的記憶。不出多日,如果不看標注,八成連照片上的背景是在法國還是在意大利都分不清了。”

          逗號頗有同感:“差不多,目前我的收獲就是一堆幻燈片和似懂非懂的概念。”

          冒號料有此果:“這一點也不奇怪。別說幾天游一個國家,單一個羅馬,沒有一個月是不可能深入了解的。至于編程范式,單一個OOP,沒有兩年以上的實踐和思考,是難以真正領會其精髓的。”

          嘆號深表懷疑:“OOP需要兩年以上才能領會?!”

          “那還得看你是否有足夠的勤奮和悟性。”冒號加強了語氣,“前面說過,單靠記憶只能觸及知識之表,單靠練習只能深入知識之里,唯有培養方能滲透知識之根。編程范式正處知識的根部,你們又怎能奢望只聽幾堂課即豁然貫通呢?”

          引號表達自己的感受:“雖然學了不少東西,但也存了不少疑惑,擱在心里有點不舒服。”

           “我明白你的意思。凡事追根究底是一種良好的學習習慣,也是一種可貴的學習精神。” 冒號表示理解和肯定,“但學習如打仗,除了要有直線式的縱深攻擊,還要有曲線式的迂回包抄。回顧我們中學的課堂,往往是每引入一個概念或理論,便圍繞其作深入的學習和反復的練習。在此過程中的種種疑惑,隨著學習的深入都會煙消云散。這樣穩扎穩打、層層推進,學得扎實,心里也踏實。但這種方法并不總是最好的,尤其在面臨動態的、開放的知識體系時,難免左支右絀。為此,我們必須學會適度地容忍無知。請注意,容忍無知不是放任無知,而是一種學習的技巧,讓無知成為求知的動力而不是障礙。容忍無知能使我們既不沮喪氣餒,也不急于求成。在學習時不妨略過一些細節或難點,先概覽全貌以獲取感性認識,然后在逐步積累中升華為理性認識。要而言之,我們不僅需要強調鉆勁和深度的‘釘子精神’,還需要強調磨功和廣度的‘刨子精神’。我一口氣兜售這么多編程范式,就是為了刺激大家求知欲,同時為大家進行第一道刨磨。”

          引號得到一些安慰:“看來今后我們還會故地重游的。”

          “不僅會重游,而且會‘深度游’。” 冒號肯定地說,“此番我們一路行色匆匆,若能感受到途中景色帶來的感官沖擊,便算是不枉此行了。其實,把編程范式類比旅游景點并不十分準確,或許比作當地的風俗文化更確切些。”

          句號立刻會意:“景點是具體的,背后的風俗文化是抽象的;編程語言是具體的,背后的編程范式是抽象的。”

          “此乃其一。”冒號右手伸出食指,“其二,如果不了解景點的歷史文化和風俗人情,僅僅滿足于表面的奇觀異景,一切很快便會淡忘。比如說,你若不了解圣經文化、不了解文藝復興史,則歐洲之行至多只是視覺的盛宴,而非文化的洗禮,收獲將是有限的,印象將是膚淺的。同樣,如果你不了解編程范式,那么眼中的編程語言只是語法、語義、核心庫、規范等組成的集合,寫出的代碼雖能編譯、能工作,卻會顯得生硬、別扭。就像中式英語,語法正確、表達也正確,可就是不正宗、不地道。其癥結我們在第一節課中已經提過了,即所謂的語感缺失。”

          問號實話實說:“可我還是不明白編程范式如何提高我們的編程語感。”

          “那就讓我們再說說范式吧。”冒號并不著急,“范式可以粗略理解為模范、模型、模式、風格、流派等等。軟件中的范式除了編程范式外,還有架構范式[1]、數據庫范式[2]等。比如,對象導向式(Object-Oriented)可以應用于編程、架構和數據庫中,分別成為OOPObject-Oriented Programming)、OOAObject-Oriented ArchitectureOODBobject-oriented database);類似地,事件驅動式(Event-Driven)可以是一種編程范式,可以是一種架構模型,也可以是一種設計模式。總之,每種范式都代表著一套獨特而有效的解決問題的思想和方法。掌握范式對編程語感的提高至少有兩層作用:首先,編程語言的語法、語義等都是從編程范式的樹根衍生而出的枝葉,把握了這種脈絡和節奏,代碼才會如音樂舞蹈般韻律有致;其次,每種范式擅長的問題領域不盡相同,只有博聞廣識,方可揚長避短,程序才會如行云流水般流暢自然。”

          逗號添油加醋:“武功練至化境,一定是博采眾長,就像楊過融合了東邪、西毒、南帝、北丐、中神通等各派武功,才能隨心所欲地打出黯然銷魂掌來。”

          提起武俠人物,眾人俱是逸興遄飛,哪能體會到半點黯然消魂之傷?

          冒號道:“天下之理,殊途同歸。我們停止玄玄之論,用實例來說明吧。誰來介紹一下快速排序法(quicksort)?”

          眾人飛舞的思緒漸漸收斂,終于由引號作答:“快速排序法的思想是:在列表中找一個基準元素,將所有小于它的元素劃歸一個子列,置于其前;將所有大于等于它的元素劃歸另一子列,置于其后。然后遞歸地對前后兩個子列作同樣處理,直至最終。”

          “很好,讓我們用Java來實現一下該算法。”冒號顯示出一段代碼——

          /** 快速排序法的Java實現 */

          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]; // 基準元素

                  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;

                  }

                  // 找到分割點j,遞歸

                  qsort(list, low, j);

                  qsort(list, j + 1, high);

              }

          }

          “請問這里用到了哪些編程范式?”冒號提問。

          嘆號心想,有何難哉?遂答:“既然是用Java實現的,自然少不了OOP。同時為了使算法更具普適性,還用到了泛型編程。”

          “你好像忘記了最重要的過程式,反倒是OOP的色彩極淡。”冒號顯然不滿意他的答案。

          嘆號不解:“不是說Java100%OOP語言嗎?”

          冒號頗為不屑:“不要輕信這種浮淺之論。且不說Java基本類型primitive type)不屬于class),本就不是100%OOP,即使是100%OOP,那與過程式也不矛盾啊。此例中的Sorter類連一個實例成員instance member)也沒有,唯一與OOP沾邊的是作為interfaceComparable,在C中也可用函數指針代替。如果不考慮泛型式的特征,本例無論用Java還是用C,并沒有本質差別。事實上,對于這類純算法的問題,OOP范式本無太多用武之地。換句話說,quicksort雖然是通過以OOP著稱的Java來實現的,但用的主要還是過程式的思想和方法。”

          問號趕緊問道:“還能用其他范式來實現嗎?”

          此問正合冒號之意:“我們改用純函數式語言Haskell來試試——”

          -- 快速排序法的Haskell實現

          qsort :: (Ord a) => [a] -> [a] --函數聲明

          qsort[] = []                             --遞歸終點

          qsort(pivot : rest) = qsort[x| x <- rest, x < pivot]      --對前面的子列遞歸

                                    ++ [pivot]

                                    ++ qsort[x| x <- rest, x >= pivot]   --對后面的子列遞歸

          嘆號幾不能信:“竟然可以如此精煉?”

          “上面的Java代碼很難再精簡了,但與Haskell代碼相比還是太冗長了。后者省去了所有的賦值、迭代和流程控制,只有單純的遞歸,反映了典型的函數式特征。”冒號解說著,“鑒于你們對Haskell不太熟悉,我稍微解釋一下。第一步,聲明函數類型[3]:同類型列表之間的變換,其中Ord可類比Java中的Comparable,以保證列表元素之間能進行比較;第二步,聲明遞歸終點:空列排序后仍是空列;第三步,描述遞歸原則:基準元素pivot與剩余子列rest進行排序后的列表,正是將小于基準的子列和超過基準的子列分別排序,中間插入基準元素后的結果。”

          句號思有所得,不禁喜形于色:“我明白了,這兩段代碼生動地反映了命令式編程與聲明式編程之間的差別:前者需要指定計算的過程,后者只需指定計算的原則。一個著重微觀的細節,一個著重宏觀的方向,自有繁簡之別。”

          冒號亦有所慰:“非常好!類似的話我以前也說過,但你們自己說的才是真正的收獲啊。我們還提過,過程式與函數式的差別同時也是機器思維與數學思維的差別。不妨對比Haskell表達式與數學中的集合表達式,它們是多么地相近!”

          黑板上出現兩行式子——

          數學表達式     {x| x rest, x < pivot}

          Haskell表達式[x| x <- rest, x < pivot]

          逗號仔細打量著:“嗯,的確像,跟哥倆似的,連符號<-都是仿照集合屬于符∈的。”

          “還有另一種表達方法。”冒號又添加了一行——

          Haskell表達式2(filter (< pivot) rest)

          “雖然與前一表達式的簡潔度相差無幾,但可讀性更強。filter即是過濾,將列表rest中的元素進行篩選,條件是小于基準元素。”冒號講解道。

          問號略感迷惑:“(< pivot)的用法看起來有點怪異。”

          “它是一個函數,也是filter的第一個參數,用來判斷第二個參數rest的元素是否合格,即 < pivot。這體現了函數式的一個重要特征:函數是頭等公民first-class citizen[4]——可作為傳遞參數、可作為表達式的值、可嵌入數據結構、也可與某變量綁定,與普通的基本數據類型毫無二致。這是函數式編程簡潔而強大的重要根源。”冒號細加解釋,“大家還記得上節課談到的回調函數吧?callback無非是將函數作為參數來傳遞,本質上是將代碼數據來使用,回調機制的巨大威力均拜此高級用法所賜。”

          眾人又一段筋脈被打通了。

          引號提出一個很實際的問題:“函數式編程的確很酷,可Java并不支持。如果采用Haskell之類的函數式語言,會不會帶來系統集成上的困難?”

          冒號打消了他的疑慮:“Java平臺下已經集成了不少的支持函數式編程的語言,如JRubyJythonGroovyScala等,甚至HaskellJVM下也有相應的Jaskell。其中GroovyJava的結合最為自然,我們看一下它是如何實現quicksort的——”

          /** 快速排序法的Groovy實現 */

          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的代碼略長了些,并且還帶著過程式的烙印,但總體思想還是函數式的。”冒號緊扣本質,“函數式還有一個重要特征:無副作用或盡量減少副作用[5]。所謂無副作用,是指一個函數在被調用前后保持程序的狀態不變。無副作用的函數不會改變非局部變量的值,不會改變傳入的參數,也沒有I/O操作。”

          逗號脫口而出:“什么狀態都不變,那這樣的函數有什么用?”

          冒號不以為奇:“你的這種想法源自根深蒂固的命令式思維。我們曾把命令式程序比作狀態自動機,其運行過程就是在不斷地修改機器的狀態。而函數式程序則是進行表達式變換,一般不會改變變量的值。其實函數式并非完全不改變內存,只不過改變的是棧內存stack)罷了。換言之,無副作用函數的作用關鍵在于其估值結果,按過程式的說法是返回值。”

          逗號如夢初醒。

          問號仍有疑問:“藥物最好沒有副作用,函數沒有副作用的好處是什么?”

          冒號嘴一咧:“好處太多了。首先,沒有副作用的函數易于重構、調試和單元測試。其次,代碼有效性與函數順序無關,方便并發處理和優化處理。舉個簡單的例子,計算兩個函數的乘積:f(x) * g(y)。由于無副作用,f(x) g(y)的估值過程是獨立的,估值順序也不重要,因此理論上可以對二者并行計算。另外,還可利用惰性計算lazy evaluation):如果算出f(x)為零,那么不用計算g(y)便可知乘積為零了。”

          嘆號忍不住贊嘆:“聽起來真不錯!”

          冒號言猶未盡:“最后,沒有副作用的函數是引用透明的(referential transparency),即一個表達式隨時可以用它的值來替換[6],正如數學中的函數一樣,保證了數學思維的貫徹和運用。”

          引號自感獲益頗豐:“前面介紹范式時,覺得函數式最為神秘。現在總算有了些感性認識了。”

          冒號道出緣由:“函數式編程不僅有許多獨特的概念和方法,還有很深的數學背景——λ-演算(λ-Calculus)。如果一開始便傾囊相授,你們必會望而卻步,我豈不是打草驚蛇了?”

          眾人始覺:老冒原來是在誘敵深入啊。

          “盡管函數式有這么多優點,運算能力從理論上比諸過程式也毫不遜色[7],但一直沒有成為主流范式,”冒號話鋒一轉,“細究之,至少有兩方面的原因:主觀上,程序員更習慣機器風格的過程式思維和現實風格OOP思維,不容易接納數學風格的函數式思維;客觀上,函數式語言在表現力和運行效率等方面與過程式和OOP語言也有一定的差距。饒是如此,支持它的語言還是越來越多,其簡潔精巧的特性也為越來越多的人所青睞。它的整體應用雖然主要集中于數學計算、人工智能等領域,但局部應用早已遍地開花了。”


           插語

          [1] OOAObject-Oriented Architecture),COA(Component-Oriented Architecture)SOA(Service- Oriented Architecture) EDAEvent-Driven Architecture)等。

          [2] 如關系數據庫(relational database)、對象導向式數據庫(object-oriented database)等。

          [3] 這一步可省略,但出于對代碼的清晰度以及性能、調試等方面的考慮,最好保留。

          [4] 這類函數更數學化的說法是高階函數(higher-order function)。

          [5] 沒有副作用的函數式語言被稱為純函數式(purely functional),如HaskellSISAL;有副作用的被稱為非純函數式(impurely functional),如LispML

          [6] 這是因為函數的無副作用性保證了相同的輸入一定有相同的輸出。

          [7] λ-演算被證明是圖靈完備的。


            總結


          • 學習知識之表須通過記憶,掌握知識之里須通過練習,滲透知識之根須通過培養。編程范式正是知識之根。
          • 適度地容忍無知也是一種學習技巧。
          • “釘子精神”固然可貴,“刨子精神”也不可少。
          • 不同的編程范式代表不同的解決問題的思想和方法。不同的編程語言提倡不同的編程范式,并在語法上給予支持。只有掌握范式,才能更合理、更有效地運用編程語言的語法和語義特征,并能針對不同的問題領域使用不同的編程風格,編寫的代碼才會和諧自然、富于美感。
          • 命令式編程需要指定計算的過程,著重微觀的細節;聲明式編程只需指定計算的原則,著重宏觀的方向。因此二者繁簡有別。
          • 在函數式編程中,函數是程序的核心,是頭等公民,一般沒有或很少副作用,同時沒有顯式的內存管理。
          • 由于函數式編程中的函數與基本數據類型平起平坐,故可將代碼作數據用,這是程序既簡潔又強大的原因之一。回調機制采用的正是函數式風格。
          • 無副作用的函數容易重構、調試和測試,便于并發和優化處理,并能貫徹和運用數學思維。
          • 相比過程式和OOP,函數式編程思想過于數學化和抽象化,語言的表現力和運行效率也有所不足。

           “”參考


          [1] Michael Lee ScottProgramming Language PragmaticsSan FranciscoMorgan Kaufmann2000589-620

          [2] Stephen H. KaislerSOFTWARE PARADIGMSNew JerseyWiley200523-24

          posted on 2008-11-30 22:27 鄭暉 閱讀(3231) 評論(3)  編輯  收藏 所屬分類: 冒號課堂

          評論

          # re: 冒號課堂§4.1:函數范式 2008-12-01 09:28 i

          good.  回復  更多評論   

          # re: 冒號課堂§4.1:函數范式[未登錄] 2008-12-08 15:51 alex

          看得太過癮了。
          在一個浮躁的年代,越來越多“XX天掌握XXX”的書籍,越來越多的HR說“大學里的課程都沒用”,越來越多的“流行技術”被掛在嘴邊。
          作為真正思想獨立的人,就不能缺少表面背后的思考。作為一名在校學生,希望將來能從事更具思考性的工作。這樣的文章,每一個希望積攢“內功”的程序員都喜歡。  回復  更多評論   

          # re: 冒號課堂§4.1:函數范式 2008-12-08 15:56 鄭暉

          @alex
          多謝你的肯定。作為一名在校學生,你能有這樣的想法,實在難能可貴。  回復  更多評論   

          導航

          統計

          公告

          博客搬家:http://blog.zhenghui.org
          《冒號課堂》一書于2009年10月上市,詳情請見
          冒號課堂

          留言簿(17)

          隨筆分類(61)

          隨筆檔案(61)

          文章分類(1)

          文章檔案(1)

          最新隨筆

          積分與排名

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 苍梧县| 无棣县| 盘锦市| 陆川县| 北安市| 吴堡县| 翁牛特旗| 开鲁县| 林西县| 巴彦淖尔市| 濉溪县| 香格里拉县| 尚志市| 嘉祥县| 江口县| 平山县| 汤阴县| 昌乐县| 黎城县| 察哈| 锡林郭勒盟| 尼木县| 工布江达县| 时尚| 云南省| 泾源县| 噶尔县| 闽侯县| 渑池县| 江城| 汉寿县| 兴和县| 宣恩县| 彭泽县| 漳浦县| 扎囊县| 太康县| 龙门县| 都匀市| 尼木县| 宽甸|