本文經過完成一個簡單的練習題(Exercise),對照在命令式編程(Imperative Programming)語言和函數式編程(Functional Programming)語言中的多種實現,在函數式編程的思惟方式上給予你們一些直觀的感覺和體驗。python
在學習和討論函數式編程的時候,我曾經被問到這麼一個問題:函數式編程有什麼特別優秀的地方?或者說有什麼事情是函數式編程能夠作到而在傳統命令式編程裏作不到或者很難作到的?記得這個問題當時回答得很敷衍,以致於如今都想不起來那時是怎麼說的。以後又通過了長時間的學習,練習和思考,到目前爲止仍是以爲這是一個很難回答的問題。在此我願意記錄一下如今的理解,相信這個不會是最終的答案,隨着學習的深刻和經驗的積累,甚至於未來會有比較大的變化。首先,流行的編程語言,無論是命令式的仍是函數式的都已經被證明具備非凡的表現能力,變幻無窮的軟件系統和應用就是證據。僅僅只有幾條規則的lambda演算被證實是圖靈完整的(https://en.wikipedia.org/wiki...),也就是說理論上僅僅基於lambda演算就能夠實現全部的智能和邏輯。有一門研究性質的編程語言叫Brain F*ck,驗證了只須要6個算符(或者能夠稱做編程原語)就能夠實現一門圖靈完整的編程語言,雖然它的Hello world簡直就是天書(https://en.wikipedia.org/wiki...)。所以我相信咱們不可能找到什麼東西是在命令式編程語言中不能實現的。區別僅在於一些問題域或是思惟方式與函數式編程的方式和風格特別地契合,從而演化出直接,簡明,高效的表述,顯得特別有表現力。但若是咱們試圖在函數式編程語言裏沿用命令式編程的思惟方式,就會遇到不少的困難,出來的結果也將會晦澀難懂,低效易錯,難於修改或擴展。程序員
有一種植物,一年有兩個生長週期,春天高度加倍,夏天高度增長1米,秋冬季休眠。給出該植物在某個冬季的初始高度 i(米),請計算給出在 n 個生長週期後的高度。編碼實現函數 f(i, n)
這是一個來自某個在線解題(Online Judge)網站的題目。題目自己很是的簡單直接。使得咱們沒必要關注於精巧的數據結構和高深的算法,從而專一於不一樣的思惟方式。算法
def f(i, n): l = i for c in range(n): l = l + 1 if (c % 2) else l * 2 return l
這裏咱們選擇了Python來做爲命令式語言的表明。Python的實現直接明瞭。你們能夠打開Python的交互式命令環境,鍵入並測試這段代碼。一些可能的測試結果以下:編程
>>> f (1, 3) 6 >>> f (2, 7) 46 >>> f (2, 2) 5 >>> f (2, 3) 10 >>> f (2, 4) 11
讓咱們簡單分析一下代碼:數組
Haskell是一門基於Lambda演算的「純粹」的函數式編程語言。Haskell裏沒有語句(Statement)的概念,程序是由函數定義和表達式組成。函數和數值都是其中的一等公民(First Class Citizen),能夠被方便地表述(Represent),存儲(Store),傳遞(Pass/Transfer),使用(Use/Apply)和操縱(Manipulate)。一般的函數都是純粹的(Pure),無反作用的(Ineffective),簡單地說就是傳入相同的參數,只能返回相同的結果。
如下是Haskell的一個實現:安全
f :: Int -> Int -> Int f i n = foldl (\acc c -> if (odd c) then acc + 1 else acc * 2) i (take n [0,1..])
你們能夠將以上代碼存成一個.hs文件(Haskell源代碼文件的擴展名),而後加載到ghci中測試。Ghci是Haskell的一個交互式命令環境,關於ghci安裝和使用不在本文的範圍內,請參見網上的其它資料。也能夠在ghci裏直接鍵入以上內容,可是不要忘記先鍵入一行「:{」,而後在結尾添加一行「:}」, :{和:}是ghci中用於鍵入多行代碼必須的標識。如下是在ghci中的函數定義和測試結果:數據結構
Prelude> :{ Prelude| f :: Int -> Int -> Int Prelude| f i n = foldl (\acc c -> if (odd c) then acc + 1 else acc * 2) i (take n [0,1..]) Prelude| :} Prelude> f 1 3 6 Prelude> f 2 7 46 Prelude> f 2 2 5 Prelude> f 2 3 10 Prelude> f 2 4 11
咱們來一步一步地解釋代碼:架構
f :: Int -> Int -> Int
是一個函數的類型申明。能夠理解爲申明瞭一個名爲f的函數,它依次接受兩個類型爲Int的參數,計算並返回一個Int型的值做爲結果。在Haskell的函數申明裏,最後的那一個->XXX能夠被理解爲函數的返回值的類型定義。foldl (+) 0 [1,2,3]
能夠將列表中的全部整數累加起來,而foldl (*) 1 [1,2,3]
則計算出列表中全部整數的乘積。take n [0,1..]
事實上構造了一個和Python中range(n)相同的序列。Haskell的惰性求值(Lazy evaluation)容許咱們方便的構造無窮序列並在適當的時候合理使用。(\acc c ->...)
是一個lambda函數,跟Python/Java中的lambda函數相似,能夠理解爲在申明/定義的地方一次性使用的匿名函數。定義中‘\’標識了lammbda函數定義的開始,以後緊跟函數的參數,多個參數間以空格隔開,以後有一個‘->’,而後就是函數的實現。lambda函數跟一般的函數同樣,其實現都是一個表達式,在實現中能夠訪問全部的參數以及包含該lambda函數的表達式中定義和可訪問的名字。在這裏,咱們僅訪問該lambda函數本身的參數。總結一下,咱們使用foldl函數實現了可控循環,使用條件表達式實現了條件分發,odd函數判斷生長週期的屬性(奇偶),take函數配合無窮的天然數序列構造了一個有限的序列用於循環控制。這個解法雖然工做,可是顯得比較笨拙,特別是那個包含條件表達式的lambda函數。其根本緣由在於咱們一路秉承的命令式編程的思路,而後在函數式語言中尋找相應的結構或函數。那麼在函數式編程語言中咱們能夠怎樣更好地思考並解決這個問題呢?編程語言
記得咱們提到過在函數式編程語言中函數是一等公民嗎?咱們能夠方便地表述,存儲,傳遞而且使用函數?下面咱們來構造一個函數序列來解決這個問題:函數式編程
f' :: Int -> Int -> Int f' i n = foldl (flip ($)) i (take n $ cycle [(*2), (+1)])
foldl和take函數咱們以前已經瞭解了。 讓咱們來看看別的新出現的東西。
2 + 4
時,事實上和函數調用(+) 2 4
徹底等價。(+1)的意思能夠理解爲咱們使用(+)這個接受兩個參數的函數,而且傳入數值1,使其綁定到(+)的第二個參數上,從而生成一個新的函數,該函數接受一個參數,並在該參數上加上數值1做爲計算結果。這樣的函數咱們稱之爲部分應用(Partial Applied)函數,或者更加專業一點的中文說法:偏函數。在C/C++/Python/Java這種嚴格求值(Strict Evaluation)的編程語言裏,咱們很難作到調用一個函數,只傳遞給它部分的參數,使其「停」在這個中間狀態,等待其它參數的傳入(C++的模版庫裏有偏函數的實現,Python也有偏函數的庫,都費了老勁了,使用又不方便,寫出的代碼難於理解,限制還多)。而Haskell是非嚴格求值(Non-strict evaluation)的或者說咱們以前提到過,惰性求值,這使得咱們很容易綁定部分的參數到一個函數,從而造成一個新的,接受更少參數的函數。事實上函數類型上的Int -> Int -> Int
,咱們說過能夠理解爲一個函數依次接受兩個Int型的參數並返回一個Int型的值做爲結果,「->」其實也是一個函數(運算符),它是右結合的, 那麼Int -> Int -> Int
就等價於Int -> (Int -> Int)
,能夠看到這實際上是說該函數接受一個Int類型的參數並返回一個(Int -> Int)類型的函數。回到咱們的主題,這裏(*2)和(+1)均是這樣的偏函數,當傳入一個參數並求值時,分別將參數加倍和加一。cycle [(*2), (+1)]
就是這樣一個無窮序列[(*2),(+1),(*2),(+1),(*2),(+1)......]
f $ a
等價於($) f a
也等價於f a
。因爲$是右結合的,並且優先級最低,咱們經常用它來減小括號的使用。這裏的(take n $ cycle [(*2), (+1)])
就等價於(take n (cycle [(*2), (+1)]))
總結一下:咱們經過cycle [(*2), (+1)]
構造了一個加倍和加一函數交替出現的無窮序列。而後用take函數依據給定的生長週期數目獲得一個有限的函數序列,該序列能夠看做是一個數據加工的動做序列。以後咱們使用foldl函數迭代該動做序列,依次將植物的高度做爲參數餵給序列中的動做並把結果做爲下次動做的參數。而將做爲累積值的植物高度餵給動做函數的任務則由函數(flip ($))來完成。這個解題思路就比較符合函數式編程的思惟方式和風格了。那麼還有沒有更加酷一點的方法呢?有的!至少我以爲下面的實現(3)就還要酷炫一點。
在實現(2)中咱們表述(Represent)了偏函數,而且將偏函數存儲(Store)在列表中,以後將這些偏函數傳遞(Pass/Transfer)給高階函數(高階函數就是能處理函數的函數)foldl和(flip ($)),最後經過高階函數把所需的參數餵給這些偏函數,應用(Apply)了它們並最終求值(Evaluation)成功。不過彷佛少了同樣,說好的操縱(Manipulate)函數呢?別急,這就來:
f'' :: Int -> Int -> Int f'' n = foldl (flip (.)) id $ take n $ cycle [(*2), (+1)]
這裏除了咱們的老朋友foldl,take,cycle以及偏函數(*2),(+1)以外,有這些新的內容:
Int -> Int -> Int
,但是它的實現爲何只有一個參數f'' n而不是像以前的f和f'那樣有兩個參數(f i n和f' i n)呢?這是函數的參數約簡在起做用。在數學上,對於兩個一元函數f和g,若是對全部可能的輸入參數a都有f a = g a,那麼咱們就說函數f等價於函數g,記爲f = g。在Haskell中這條規則徹底成立,無論何時若是有f i = g i,只要f和g中沒有包含i,咱們均可以安全地把參數i約去而獲得f = g。因此實際上f'' n = foldl ...
是由f'' n i = (foldl ...) i
經過兩邊約掉相同的i簡化得來的。那爲何參數i和n的順序變了呢?下面咱們立刻會說到。Int -> Int
。那麼咱們就須要一個一樣類型的函數做爲foldl的累積初始值。注意到這個初始函數會被組合到最後的組合函數鏈的尾端(也就是最內層)。若是咱們爲這個函數命名h的話,最後的組合函數鏈看起來應該像這個樣子.... (+1).(*2).(+1).(*2).h
。而咱們並不但願這個函數會對結果有任何影響或改變,很天然地咱們認定這個函數應該直接返回傳給它的參數而不作任何更改,該函數的定義應該是h=\x->x
。在Haskell裏,預約義的id正是這麼一個函數。因而咱們看到id被做爲foldl的累積初始值。能夠在ghci中鍵入並驗證這個實現,注意參數n和i的順序不一樣於以前的f和f',除非你使用(flip f'')。
Prelude> :{ Prelude| f'' :: Int -> Int -> Int Prelude| f'' n = foldl (flip (.)) id $ take n $ cycle [(*2), (+1)] Prelude| :} Prelude> f'' 3 1 6 Prelude> f'' 7 2 46 Prelude> f'' 2 2 5 Prelude> f'' 3 2 10 Prelude> f'' 4 2 11
總結一下:咱們如方法(2)中同樣構建了一個由偏函數(*2)和(+1)交替出現的動做序列。而後使用foldl和組合函數(.)將序列中的前n個元素組合(函數操縱Manipulate的一種情形)成爲一個大的函數。最後該函數接受到做爲參數的植物的初始高度,求值計算出結果,也就是最後的植物高度。
或者咱們能夠這樣理解:咱們在方法(3)中構建了一套邏輯/理論架構,在給定生長週期數目的時候,該理論架構發生塌縮,生成一個Int -> Int
的函數,咱們能夠理解爲規模較小的理論架構。而後當傳入植物的初始高度時,這個較小的理論架構再次發生塌縮,從而計算/獲得最終的結果,也就是咱們想要獲得的植物的最終高度。這個理解完美地契合了函數式建模/編程的一種世界觀,那就是編程就是設計和建模一個理論架構,而後當接收到外來刺激的時候,該理論架構就會發生塌縮,從而造成/產生在該刺激條件下的你們所期待的完美結果。是的,完美!