當然,我們還可以把程序更精簡一些,利用函數式編程中的利器,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)
遞歸是在描述什么是斐波那契數列,這個數列的定義就是一個數等于他的前兩項的和,并且已知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()