1. 概論html
在過去的近十年的時間裏,面向對象編程大行其道。以致於在大學的教育裏,老師也只會教給咱們兩種編程模型,面向過程和麪向對象。程序員
孰不知,在面向對象產生以前,在面向對象思想產生以前,函數式編程已經有了數十年的歷史。算法
那麼,接下來,就讓咱們回顧這個古老又現代的編程模型,讓咱們看看到底是什麼魔力將這個概念,將這個古老的概念,在21世紀的今天再次拉入了咱們的視野。編程
2. 什麼是函數式編程緩存
在維基百科中,已經對函數式編程有了很詳細的介紹。併發
那咱們就來摘取一下Wiki上對Functional Programming的定義:app
In computer science, functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids state and mutable data.編程語言
簡單地翻譯一下,也就是說函數式編程是一種編程模型,他將計算機運算看作是數學中函數的計算,而且避免了狀態以及變量的概念。函數式編程
接下來,咱們就來剖析下函數式編程的一些特徵。函數
3. 從併發說開來
說來慚愧,我第一個真正接觸到函數式編程,要追溯到兩年之前的《Erlang程序設計》,咱們知道Erlang是一個支持高併發,有着強大容錯性的函數式編程語言。
由於時間過久了,並且一直沒有過真正地應用,因此對Erlang也只是停留在一些感性認識上。在我眼裏,Erlang對高併發的支持體如今兩方面,第一,Erlang對輕量級進程的支持(請注意此處進程並不等於操做系統的進程,而只是Erlang內部的一個單位單元),第二,就是變量的不變性。
4. 變量的不變性
在《Erlang程序設計》一書中,對變量的不變性是這樣說的,Erlang是目前惟一變量不變性的語言。具體的話我記不清了,我不知道是老爺子就是這麼寫的,仍是譯者的問題。我在給這本書寫書評的時候吹毛求疵地說:
我對這句話有異議,切不說曾經的Lisp,再到現在的F#都對賦值操做刮目相看,低人一等。單說現在的Java和C#,提供的final和readonly同樣能夠支持變量的不變性,而這個惟一未免顯得有點太孤傲了些。
讓咱們先來看兩段程序,首先是咱們常見的一種包含賦值的程序:
class Account:
def __init__(self,balance):
self.balance = balance
def desposit(self,amount):
self.balance = self.balance + amount
return self.balance
def despositTwice(self):
self.balance = self.balance * 2
return self.balance
if __name__ == '__main__':
account = Account(100)
print(account.desposit(10))
print(account.despositTwice())
這段程序自己是沒有問題的,可是咱們考慮這樣一種狀況,如今有多個進程在同時跑這一個程序,那麼程序就會被先desposit 仍是先 despositTwice所影響。
可是若是咱們採用這樣的方式:
def makeAccount(balance):
global desposit
global despositTwice
def desposit(amount):
result = balance + amount
return result
def despositTwice():
result = balance * 2
return result
def dispatch(method):
return eval(method)
return dispatch
if __name__ == '__main__':
handler = makeAccount(100)
print(handler('desposit')(10))
print(handler('despositTwice')())
這時咱們就會發現,不管多少個進程在跑,由於咱們自己沒有賦值操做,因此都不會影響到咱們的最終結果。
可是這樣也像你們看到的同樣,採用這樣的方式沒有辦法保持狀態。
這也就是咱們在以前概念中看到的無狀態性。
5. 再看函數式編程的崛起
既然已經看完了函數式編程的基本特徵,那就讓咱們來想一想數十年後函數式編程再次崛起的幕後緣由。
一直以來,做爲函數式編程表明的Lisp,仍是Haskell,更多地都是在大學中,在實驗室中應用,而不多真的應用到真實的生產環境。
先讓咱們再來回顧一下偉大的摩爾定律:
一、集成電路芯片上所集成的電路的數目,每隔18個月就翻一番。
二、微處理器的性能每隔18個月提升一倍,而價格降低一半。
三、用一個美圓所能買到的電腦性能,每隔18個月翻兩番。
一如摩爾的預測,整個信息產業就這樣飛速地向前發展着,可是在近年,咱們卻能夠發現摩爾定律逐漸地失效了,芯片上元件的尺寸是不可能無限地縮小的,這就意味着芯片上所能集成的電子元件的數量必定會在某個時刻達到一個極限。那麼當技術達到這個極限時,咱們又該如何適應日益增加的計算需求,電子元件廠商給出了答案,就是多核。
多核並行程序設計就這樣被推到了前線,而命令式編程天生的缺陷卻使並行編程模型變得很是複雜,不管是信號量,仍是鎖的概念,都使程序員不堪其重。
就這樣,函數式編程終於在數十年後,終於走出實驗室,來到了真實的生產環境中,不管是冷門的Haskell,Erlang,仍是Scala,F#,都是函數式編程成功的典型。
6. 函數式編程的第一型
咱們知道,對象是面向對象的第一型,那麼函數式編程也是同樣,函數是函數式編程的第一型。
咱們在函數式編程中努力用函數來表達全部的概念,完成全部的操做。
在面向對象編程中,咱們把對象傳來傳去,那在函數式編程中,咱們要作的是把函數傳來傳去,而這個,說成術語,咱們把他叫作高階函數。
那咱們就來看一個高階函數的應用,熟悉js的同窗應該對下面的代碼很熟悉,讓哦咱們來寫一個在電子電路中經常使用的濾波器的示例代碼。
def Filt(arr,func):
result = []
for item in arr:
result.append(func(item))
return result
def MyFilter(ele):
if ele < 0 :
return 0
return ele
if __name__ == '__main__':
arr = [-5,3,5,11,-45,32]
print('%s' % (Filt(arr,MyFilter)))
哦,以前忘記了說,什麼叫作高階函數,咱們給出定義:
在數學和計算機科學中,高階函數是至少知足下列一個條件的函數:
- 接受一個或多個函數做爲輸入
- 輸出一個函數
那麼,毫無疑問上面的濾波器,就是高階函數的一種應用。
在函數式編程中,函數是基本單位,是第一型,他幾乎被用做一切,包括最簡單的計算,甚至連變量都被計算所取代。在函數式編程中,變量只是一個名稱,而不是一個存儲單元,這是函數式編程與傳統的命令式編程最典型的不一樣之處。
讓咱們看看,變量只是一個名稱,在上面的代碼中,咱們能夠這樣重寫主函數:
if __name__ == '__main__':
arr = [-5,3,5,11,-45,32]
func = MyFilter
print('%s' % (Filt(arr,func)))
固然,咱們還能夠把程序更精簡一些,利用函數式編程中的利器,map,filter和reduce :
if __name__ == '__main__':
arr = [-5,3,5,11,-45,32]
print('%s' % (map(lambda x : 0 if x<0 else x ,arr)))
這樣看上去是否是更賞心悅目呢?
這樣咱們就看到了,函數是咱們編程的基本單位。
7. 函數式編程的數學本質
忘了是誰說過:一切問題,歸根結底到最後都是數學問題。
編程歷來都不是難事兒,無非是細心,加上一些函數類庫的熟悉程度,加上經驗的堆積,而真正困難的,是如何把一個實際問題,轉換成一個數學模型。這也是爲何微軟,Google之類的公司重視算法,這也是爲何數學建模大賽在大學計算機系如此被看重的緣由。
先假設咱們已經憑藉咱們良好的數學思惟和邏輯思惟創建好了數學模型,那麼接下來要作的是如何把數學語言來表達成計算機能看懂的程序語言。
這裏咱們再看在第四節中,咱們提到的賦值模型,同一個函數,同一個參數,卻會在不一樣的場景下計算出不一樣的結果,這是在數學函數中徹底不可能出現的狀況,f(x) = y ,那麼這個函數不管在什麼場景下,都會獲得一樣的結果,這個咱們稱之爲函數的肯定性。
這也是賦值模型與數學模型的不兼容之處。而函數式編程取消了賦值模型,則使數學模型與編程模型完美地達成了統一。
8. 函數式編程的抽象本質
相信每一個程序員都對抽象這個概念不陌生。
在面向對象編程中,咱們說,類是現實事物的一種抽象表示。那麼抽象的最大做用在我看來就在於抽象事物的重用性,一個事物越具體,那麼他的可重用性就越低,所以,咱們再打造可重用性代碼,類,類庫時,其實在作的本質工做就在於提升代碼的抽象性。而再往大了說開來,程序員作的工做,就是把一系列過程抽象開來,反映成一個通用過程,而後用代碼表示出來。
在面向對象中,咱們把事物抽象。而在函數式編程中,咱們則是在將函數方法抽象,第六節的濾波器已經讓咱們知道,函數同樣是可重用,可置換的抽象單位。
那麼咱們說函數式編程的抽象本質則是將函數也做爲一個抽象單位,而反映成代碼形式,則是高階函數。
9.狀態到底怎麼辦
咱們說了一大堆函數式編程的特色,可是咱們忽略了,這些都是在理想的層面,咱們回頭想一想第四節的變量不變性,確實,咱們說,函數式編程是無狀態的,但是在咱們現實狀況中,狀態不可能一直保持不變,而狀態必然須要改變,傳遞,那麼咱們在函數式編程中的則是將其保存在函數的參數中,做爲函數的附屬品來傳遞。
ps:在Erlang中,進程之間的交互傳遞變量是靠「信箱」的收發信件來實現,其實咱們想想,從本質而言,也是將變量做爲一個附屬品來傳遞麼!
咱們來看個例子,咱們在這裏舉一個求x的n次方的例子,咱們用傳統的命令式編程來寫一下:
def expr(x,n):
result = 1
for i in range(1,n+1):
result = result * x
return result
if __name__ == '__main__':
print(expr(2,5))
這裏,咱們一直在對result變量賦值,可是咱們知道,在函數式編程中的變量是具備不變性的,那麼咱們爲了保持result的狀態,就須要將result做爲函數參數來傳遞以保持狀態:
def expr(num,n):
if n==0:
return 1
return num*expr(num,n-1)
if __name__ == '__main__':
print(expr(2,5))
呦,這不是遞歸麼!
10. 函數式編程和遞歸
遞歸是函數式編程的一個重要的概念,循環能夠沒有,可是遞歸對於函數式編程倒是不可或缺的。
在這裏,我得認可,我確實不知道我該怎麼解釋遞歸爲何對函數式編程那麼重要。我能想到的只是遞歸充分地發揮了函數的威力,也解決了函數式編程無狀態的問題。(若是你們有其餘的意見,請賜教)
遞歸其實就是將大問題無限地分解,直到問題足夠小。
而遞歸與循環在編程模型和思惟模型上最大的區別則在於:
循環是在描述咱們該如何地去解決問題。
遞歸是在描述這個問題的定義。
那麼就讓咱們以斐波那契數列爲例來看下這兩種編程模型。
先說咱們最多見的遞歸模型,這裏,我不採用動態規劃來作臨時狀態的緩存,只是說這種思路:
def Fib(a):
if a==0 or a==1:
return 1
else:
return Fib(a-2)+Fib(a-1)
遞歸是在描述什麼是斐波那契數列,這個數列的定義就是一個數等於他的前兩項的和,而且已知Fib(0)和Fib(1)等於1。而程序則是用計算機語言來把這個定義從新描述了一次。
那接下來,咱們看下循環模型:
def Fib(n):
a=1
b=1
n = n - 1
while n>0:
temp=a
a=a+b
b=temp
n = n-1
return b
這裏則是在描述咱們該如何求解斐波那契數列,應該先怎麼樣再怎麼樣。
而咱們明顯能夠看到,遞歸相比於循環,具備着更加良好的可讀性。
可是,咱們也不能忽略,遞歸而產生的StackOverflow,而賦值模型呢?咱們懂的,函數式編程不能賦值,那麼怎麼辦?
11. 尾遞歸,僞遞歸
咱們以前說到了遞歸和循環各自的問題,那怎麼來解決這個問題,函數式編程爲咱們拋出了答案,尾遞歸。
什麼是尾遞歸,用最通俗的話說:就是在最後一部單純地去調用遞歸函數,這裏咱們要注意「單純」這個字眼。
那麼咱們說下尾遞歸的原理,其實尾遞歸就是不要保持當前遞歸函數的狀態,而把須要保持的東西所有用參數給傳到下一個函數裏,這樣就能夠自動清空本次調用的棧空間。這樣的話,佔用的棧空間就是常數階的了。
在看尾遞歸代碼以前,咱們仍是先來明確一下遞歸的分類,咱們將遞歸分紅「樹形遞歸」和「尾遞歸」,什麼是樹形遞歸,就是把計算過程逐一展開,最後造成的是一棵樹狀的結構,好比以前的斐波那契數列的遞歸解法。
那麼咱們來看下斐波那契尾遞歸的寫法:
def Fib(a,b,n):
if n==0:
return b
else:
return Fib(b,a+b,n-1)
這裏看上去有些難以理解,咱們來解釋一下:傳入的a和b分別是前兩個數,那麼每次我都推動一位,那麼b就變成了第一個數,而a+b就變成的第二個數。
這就是尾遞歸。其實咱們想想,這不是在描述問題,而是在尋找一種問題的解決方案,和上面的循環有什麼區別呢?咱們來作一個從尾遞歸到循環的轉換把!
最後返回b是把,那我就先聲明瞭,b=0
要傳入a是把,我也聲明瞭,a=1
要計算到n==0是把,仍是循環while n!=0
每一次都要作一個那樣的計算是吧,我用臨時變量交換一下。temp=b ; b=a+b;a=temp。
那麼按照這個思路一步步轉換下去,是否是就是咱們在上面寫的那段循環代碼呢?
那麼這個尾遞歸,其實本質上就是個「僞遞歸」,您說呢?
既然咱們能夠優化,對於大多數的函數式編程語言的編譯器來講,他們對尾遞歸一樣提供了優化,使尾遞歸能夠優化成循環迭代的形式,使其不會形成堆棧溢出的狀況。
12. 惰性求值與並行
第一次接觸到惰性求值這個概念應該是在Haskell語言中,看一個最簡單的惰性求值,我以爲也是最經典的例子:
在Haskell裏,有個repeat關鍵字,他的做用是返回一個無限長的List,那麼咱們來看下:
take 10 (repeat 1)
就是這句代碼,若是沒有了惰性求值,我想這個進程必定會死在那裏,但是結果倒是很正常,返回了長度爲10的List,List裏的值都是1。這就是惰性求值的典型案例。
咱們看這樣一段簡單的代碼:
def getResult():
a = getA() //Take a long time
b = getB() //Take a long time
c = a + b
這段代碼自己很簡單,在命令式程序設計中,編譯器(或解釋器)會作的就是逐一解釋代碼,按順序求出a和b的值,而後再求出c。
但是咱們從並行的角度考慮,求a的值是否是能夠和求b的值並行呢?也就是說,直到執行到a+b的時候咱們編譯器才意識到a和b直到如今才須要,那麼咱們雙核處理器就天然去發揮去最大的功效去計算了呢!
這纔是惰性求值的最大威力。
固然,惰性求值有着這樣的優勢也必然有着缺點,我記得我看過一個例子是最經典的:
def Test():
print('Please enter a number:')
a = raw_input()
但是這段代碼若是惰性求值的話,第一句話就不見得會在第二句話以前執行了。
13. 函數式編程總覽
咱們看完了函數式編程的特色,咱們想一想函數式編程的應用場合。
1. 數學推理
2. 並行程序
那麼咱們整體地說,其實函數式編程最適合地仍是解決局部性的數學小問題,要讓函數式編程來作CRUD,來作咱們傳統的邏輯性很強的Web編程,就有些免爲其難了。
就像若是要用Scala徹底取代今天的Java的工做,我想恐怕效果會很糟糕。而讓Scala來負責底層服務的編寫,恐怕再合適不過了。
而在一種語言中融入多種語言範式,最典型的C#。在C# 3.0中引入Lambda表達式,在C# 4.0中引入聲明式編程,咱們某些人在嘲笑C#愈來愈臃腫的同時,卻忽略了,這樣的語法糖,帶給咱們的不只僅是代碼書寫上的遍歷,更重要的是編程思惟的一種進步。
好吧,那就讓咱們忘記那些C#中Lambda背後的實現機制,在C#中,仍是在那些更純粹地支持函數式編程的語言中,盡情地去體驗函數式編程帶給咱們的快樂把!
原文地址:http://www.cnblogs.com/kym/archive/2011/03/07/1976519.html