函数式编程常用术语
??近年來函數(shù)式編程這種概念漸漸流行起來,尤其是在React/Vuejs這兩個前端框架的推動下,函數(shù)式編程就像股新思潮一般瞬間席卷整個技術(shù)圈。雖然博主接觸到的前端技術(shù)并不算深入,可這并不妨礙我們通過類似概念的延伸來理解這種概念。首先,函數(shù)式編程是一種編程范式,而我們所熟悉的常見編程范式則有命令式編程(Imperative Programmming)、函數(shù)式編程(Functional Programming)、邏輯式編程(Logic Programming)、聲明式編程(Declarative Programming)和響應(yīng)式編程(Reactive Programming)等。現(xiàn)代編程語言 在發(fā)展過程中實際上都在借鑒不同的編程范式,比如Lisp和Haskell 是最經(jīng)典的函數(shù)式編程語言,而SmartTalk、C++和Java則是最經(jīng)典的命令式編程語言。微軟的C#語言最早主要借鑒Java語言,在其引入lambda和LINQ特性以后,使得C#開始具備實施函數(shù)式編程的基礎(chǔ),而最新的Java8同樣開始強化lambda這一特性,為什么lambda會如此重要呢?這或許要從函數(shù)式編程的基本術(shù)語開始說起。
什么是函數(shù)式編程?
??我們提到函數(shù)式編程是一種編程范式,它的基本思想是將計算機運算當作是數(shù)學(xué)中的函數(shù),同時避免了狀態(tài)和變量的概念。一個直觀的理解是,在函數(shù)式編程中面向數(shù)據(jù),函數(shù)是第一等公民,而我們傳統(tǒng)的命令式編程中面向過程,類是第一等公民。為什么我們反復(fù)提到lambda呢?因為函數(shù)式編程中最重要的基礎(chǔ)是lambda演算(Lambda Calculus),并且lambda演算的函數(shù)可以接受函數(shù)作為參數(shù)和返回值,這聽起來和數(shù)學(xué)有關(guān),的確函數(shù)式編程是面向數(shù)學(xué)的抽象,任何計算機運算在這里都被抽象為表達式求值,簡而言之,函數(shù)式程序即為一個表達式。值得一提的是,函數(shù)式編程是圖靈完備的,這再次說明數(shù)學(xué)和計算機技術(shù)是緊密聯(lián)系在一起的。雖然在博主心目中認為,圖靈這位天縱英才的英國數(shù)學(xué)家,是真正的計算機鼻祖,但歷史從來都喜歡開玩笑的,因為現(xiàn)代計算機是以馮.諾依曼體系為基礎(chǔ)的,而這一體系天生就是面向過程即命令式的,在這套體系下計算機的運算實則是硬件的一種抽象,命令式程序?qū)嶋H上是一組指令集。因此,函數(shù)式程序目前依然需要編譯為該體系下的計算機指令來執(zhí)行,這聽起來略顯遺憾,可這對我們來說并不重要,下面讓我們來一窺函數(shù)式編程的真容:
squares = map(lambda x: x * x, [0, 1, 2, 3, 4]) print squares這是使用Python編寫的函數(shù)式編程風(fēng)格的代碼,或許看到這樣的代碼,我們內(nèi)心是完全崩潰的,可是它實現(xiàn)得其實是這樣一個功能,即將集合{0, 1, 2, 3, 4}中的每個元素進行平方操作,然后返回一個新的集合。如果使用命令式編程,我們注定無法使用如此簡單的代碼實現(xiàn)這個功能。而這個功能在.NET中其實是一個Select的功能:
int[] array = new int[]{0, 1, 2, 3, 4}; int[] result = array.Select(m => m * m).ToArray();這就是函數(shù)式編程的魅力,我們所做的事情都是由一個個函數(shù)來完成的,這個函數(shù)定義了輸入和輸出,而我們只需要將數(shù)據(jù)作為參數(shù)傳遞給函數(shù),函數(shù)會返回我們期望的結(jié)果。好了,下面再看一個例子:
sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4]) print sum即使我們從來沒有了解過函數(shù)式編程,從命名我們依然可以看出這是一個對集合中的元素求和的功能實現(xiàn),這就是規(guī)范命名的重要性。幸運的是.NET中同樣有類似的擴展方法,我喜歡Linq,我喜歡lambda:
int[] array = new int[]{0, 1, 2, 3, 4}; int result = array.Sum();考慮到博主寫不出更復(fù)雜的函數(shù)式編程的代碼示例,這里不再列舉更多的函數(shù)式編程風(fēng)格的代碼,可是我們從直觀上來理解函數(shù)式編程,就會發(fā)現(xiàn)函數(shù)式編程同lambda密不可分,函數(shù)在這里扮演著重要的角色。好了,下面我們來了解下函數(shù)式編程中的常用術(shù)語。
函數(shù)式編程的常用術(shù)語
??函數(shù)式編程首先是一種編程范式,這意味著它和面向?qū)ο缶幊桃粯?#xff0c;都是一種編程的思想。而函數(shù)式編程最基本的兩個特性就是不可變數(shù)據(jù)和表達式求值。基于兩個基礎(chǔ)特性,我們延伸出了各種函數(shù)式編程的相關(guān)概念,而這些概念就是函數(shù)式編程的常用術(shù)語。常用的函數(shù)式編程術(shù)語有高階函數(shù)、柯里化/局部調(diào)用、惰性求值,遞歸等。在了解這些概念前,我們先來理解,什么是函數(shù)式編程的不可變性。不可變性,意味著在函數(shù)式編程中沒有變量的概念,即操作不會改變原有的值而是修改新產(chǎn)生的值。舉一個基本的例子,.NET中IEnumerable接口提供了大量的如Select、Where等擴展方法,而這些擴展方法同樣會返回IEnumerable類型,并且這些擴展方法不會改變原來的集合,所有的修改都是作用在一個新的集合上,這就是函數(shù)式編程的不可變性。實現(xiàn)不可變性的前提是純函數(shù),即函數(shù)不會產(chǎn)生副作用。一個更為生動的例子是,如果我們嘗試對一個由匿名類型組成的集合進行修改,會被提示該匿名類型的屬性為只讀屬性,這意味著數(shù)據(jù)是不可改變的,如果我們要堅持對數(shù)據(jù)進行“修改”,唯一的方法就是調(diào)用一個函數(shù)。
高階函數(shù)(Higer-Order-Function)
??高階函數(shù)是指函數(shù)自身能夠接受函數(shù),并返回函數(shù)的一種函數(shù)。這個概念聽起來好像非常復(fù)雜的樣子,其實在我們使用Linq的時候,我們就是在使用高階函數(shù)啦。這里介紹三個非常有名的高階函數(shù),即Map、Filter和Fold,這三個函數(shù)在Linq中分別對應(yīng)于Select、Where和Sum。我們可以通過下面的例子來理解:
- Map函數(shù)需要一個元素集合和一個訪問該元素集合中每一個元素的函數(shù),該函數(shù)將生成一個新的元素集合,并返回這個新的元素集合。通過C#中的迭代器可以惰性實現(xiàn)Map函數(shù):
- Filter函數(shù)需要一個元素集合和一個篩選該元素結(jié)合的函數(shù),該函數(shù)將從原始元素集合中篩選中符合條件的元素,然后組成一個新的元素集合,并返回這個新的元素集合。通過C#中的Predicate委托類型,我們可以寫出下面的代碼:
- Fold函數(shù)實際上代表了一系列函數(shù),而最重要的兩個例子是左折疊和右折疊,這里我們選擇相對簡單地左折疊來實現(xiàn)累加的功能,它需要一個元素集合,一個累加函數(shù)和一個初始值,我們一起來看下面的代碼實現(xiàn):
相信現(xiàn)在大家應(yīng)該理解什么是高階函數(shù)了,這種聽起來非常數(shù)學(xué)的名詞,當我們嘗試用代碼來描述的時候會發(fā)現(xiàn)非常簡單。相信大家都經(jīng)歷過學(xué)生時代,臨近期末考試的時候死記硬背名詞解釋的情形,其實可以用簡潔的東西描述清楚的概念,為什么需要用這種方式來理解呢?為什么我這里選擇了C#中的委托來編寫這些示例代碼呢?自然是同樣的道理啦,因為我們都知道,在C#中委托是一種類似函數(shù)指針的概念,因為當我們需要傳入和返回一個函數(shù)的時候,選擇委托這種特殊的類型可謂是恰如其分啦,這樣并不會影響我們?nèi)ダ斫飧唠A函數(shù)。
柯里化(Curring)/局部套用
??柯里化(Curring)得名于數(shù)學(xué)家Haskell Curry,你的確沒有看錯,這位偉大的數(shù)學(xué)家不僅創(chuàng)造了Haskell這門函數(shù)式編程語言,而且提出了局部套用(Currin)這種概念。所謂局部套用,就是指不管函數(shù)中有多少個參數(shù),都可以函數(shù)視為函數(shù)類的成員,而這些函數(shù)只有一個形參,局部套用和部分應(yīng)用息息相關(guān),尤其是部分應(yīng)用是保證函數(shù)模塊化的兩個重要技術(shù)之一(部分應(yīng)用和組合(Composition)是保證函數(shù)模塊化的兩個重要技術(shù))。眾所周知,在C#中一個函數(shù)一旦完成定義,那么它的參數(shù)列表就是確定的,即相對靜態(tài)。它不能像Python和Lua一樣去動態(tài)改變參數(shù)列表,雖然我們可以通過缺省參數(shù)來減少參數(shù)的個數(shù),可是在大多數(shù)情況下,我們都需要在調(diào)用函數(shù)前準備好所有參數(shù),而局部套用所做的事情與這個理念截然相反,它的目標是用非完全的參數(shù)列表去調(diào)用函數(shù)。我們來一起看下面這個例子:
Func<int,int,int> add = (x,y) => {return x + y;};這是一個由匿名方法定義的委托類型,顯然我們需要在調(diào)用這個方法前準備好兩個參數(shù)x和y,這意味著C#不允許我們在改變參數(shù)列表的情況下調(diào)用這個方法。而通過局部套用:
Func<int,int,int> curriedAdd => (x) => {return (y) => { return x + y;}; };實際上在這里兩個參數(shù)x和y的順序?qū)ψ罱K結(jié)果沒有任何影響,我們這樣寫僅僅是為了符合人類正常的認知習(xí)慣,而此時我們注意到我們在調(diào)用curriedAdd時會發(fā)生質(zhì)的的變化:
//x和y同時被傳入add add(x,y) //x和y可以不同時被傳入curriedAdd curriedAdd(x)(y);而如果我們將這里的函數(shù)用Lambda表達式來表示,則會發(fā)現(xiàn):
Func<int,int,int> add = (x,y) => return x + y; Func<int,Fucn<int,int>> curriedAdd = x = > y => x + y;至此,對一般的局部套用,存在:
Func<...> f = (part1, part2, part3, ...) => ... 可轉(zhuǎn)換為: Func<...> cf = part1 => part2 => part3 ... => ...則稱后者為前者的局部套用形式。
惰性求值
??我們在前文中曾經(jīng)提到過,在函數(shù)式編程中函數(shù)是第一等公民,而這里的函數(shù)更接近數(shù)學(xué)意義上的函數(shù),即將函數(shù)視為一個可以對表達式求值的純函數(shù),所以我們這里自然而然地就提到了惰性求值。首先,博主這里想說說求值策略這個問題,求值策略通常有嚴格求值和非嚴格求值兩種,而對C#語言來講,它在大多數(shù)情況下使用嚴格求值策略,即參數(shù)在傳遞給函數(shù)前求值。與之相對應(yīng)的,我們將參數(shù)在傳遞給函數(shù)前不進行求值或者延遲求值的這種情況,稱為非嚴格求值策略。一個經(jīng)典的例子是C#中的“短路”效應(yīng):
bool isTrue = (10 < 5) && (MyCheck())因為在這里表達式的第一部分返回值為false,因此在實際調(diào)用中第二部分根本不會執(zhí)行,因為無論第二部分返回true還是false,實際上對整個表達式的結(jié)果都不會產(chǎn)生影響。這是一個非常經(jīng)典的非嚴格求值的例子,同樣的,布爾運算中的”||”運算符,同樣存在這個問題。所以,至此我們可以領(lǐng)會到惰性求值的優(yōu)點,即使程序的執(zhí)行效率更好,尤其是在避免高昂運算代價的時候,我們要牢記:懶惰是程序員的一種美德,使用更簡潔的代碼來滿足需求,是一名游戲程序員的永恒追求。我們可以聯(lián)想那些在代碼片段中優(yōu)先return的場景,這大概勉強可以用這種理論來解釋吧!例如我們強大的Linq,原諒我如此執(zhí)著于舉Linq的例子,Linq的一個特點是當數(shù)據(jù)需要被使用的時候開始計算,即數(shù)據(jù)是延遲加載的,而在此之前我們所有對數(shù)據(jù)的操作,從某種意義上來講,更像是定義了一系列函數(shù),這好像和數(shù)據(jù)庫中的事務(wù)非常相近啦,其實這就是在告訴我們,懶惰是一種美德啊,哈哈!
函數(shù)式編程的利弊探討
??好了,現(xiàn)在讓我們從函數(shù)式編程的各種術(shù)語中解放出來,高屋建瓴般地從更高的層面上探討下函數(shù)式編程的利弊。當你討論一種東西的利弊時,一種習(xí)慣性的做法是找一種東西來和它作比較,如果Windows和Linux、SQL和NoSQ、面向?qū)ο蠛秃瘮?shù)式…等等,我們常常關(guān)注一件事物的利弊,而非去尋找哪一個是最好。可惜自以為是的人類,常常以此來自我設(shè)限,劃分各自的陣營,這當真是件無聊的事情,就像我一直不喜歡SQL和正則表達式,所以我就去了解數(shù)據(jù)庫的設(shè)計、模式匹配相關(guān)內(nèi)容,最終感覺頗有一番收獲,我想這是我們真正的目的吧!好了,下面我們說說函數(shù)式編程有哪些優(yōu)缺點?首先,函數(shù)式編程極大地改善了程序的模塊化程度,高階函數(shù)、遞歸和惰性求值讓程序充分函數(shù)化,函數(shù)式讓編程可以以一種聲明式的風(fēng)格來增強程序語義。當然,函數(shù)式編程的缺點是,我們這個現(xiàn)實世界本來就不是純粹的,函數(shù)式編程強調(diào)的數(shù)據(jù)不可變性,意味著我們無法去模擬事物狀態(tài)變化,因此我們不能為了追求無副作用、無鎖而忽視現(xiàn)實,這個世界上總有些骯臟的問題,無法讓我們用純函數(shù)的思維去解決,這個時候我們不能說要讓設(shè)計去適應(yīng)這個世界,任何技術(shù)或者框架的誕生歸根到底是為了解決問題,而函數(shù)式編程或者是面向?qū)ο缶幊?#xff0c;本質(zhì)都是一種編程思想,我們最終是為了解決問題,就像這個世界有時候并不是面向?qū)ο蟮?#xff0c;我們用面向?qū)ο髞砻枋鲞@個世界,或許僅僅是我們自己的理解,這個世界到底是什么樣子的,大概只有上帝會知道吧!
本文小結(jié)
??本文主要對函數(shù)式編程及其常見術(shù)語進行了簡要討論,主要根據(jù)《C#函數(shù)式程序設(shè)計》一書整理并輔以博主的理解而成。首先,函數(shù)式編程中強調(diào)無狀態(tài)、不可變性,認為函數(shù)是一等公民,并且在函數(shù)式編程中每一個函數(shù)都是一個純函數(shù),它是數(shù)學(xué)概念咋計算機領(lǐng)域的一種延伸,和馮.諾依曼計算機體系不同,函數(shù)式編程的核心思想是以lambda演算為基礎(chǔ)的表達式求值,并且函數(shù)式編程強調(diào)無副作用。本文對函數(shù)式編程中的常見術(shù)語如高階函數(shù)、局部套用/柯里化、惰性求值等結(jié)合C#語言進行了簡單分析。或許對我們而言,函數(shù)式編程是一個新鮮事物,可正如我們第一次接觸面向?qū)ο缶幊虝r一樣,我們并不知道這樣一種編程思想會持續(xù)到今天。我不認為函數(shù)式編程會徹底替代面向?qū)ο缶幊?#xff0c;就像Web開發(fā)無法徹底替換原生開發(fā)一樣,函數(shù)式編程會作為面向?qū)ο蟮囊环N延伸和補充,所以本文對函數(shù)式編程的理解實際上是非常膚淺的,可這個世界本來就是在不斷變化的,希望我們可以在恰當?shù)膱鼍跋氯?quán)衡選擇什么樣的技術(shù),對這個世界而言,我們永遠都是探索者,或許永遠都不存在完全能滿足現(xiàn)實場景的編程范式吧!
總結(jié)
- 上一篇: 绿色软件在Windows10中设置开机自
- 下一篇: 厌倦只是一瞬间的事 2012-03-29