小貼士
參與文末話題討論,有機(jī)會獲得贈書~
導(dǎo)語:函數(shù)式編程并不是新概念,實際上,在計算機(jī)科學(xué)出現(xiàn)之初,函數(shù)式編程就已經(jīng)嶄露頭角,教科書式的函數(shù)式編程語言LISP在1958年就誕生了,但是,為什么一直都是命令式編程和面向?qū)ο缶幊檀笮衅涞滥兀?/span>
函數(shù)式編程歷史
說來話長,想當(dāng)年,阿蘭·圖靈和馮·諾依曼祖師爺開天辟地,創(chuàng)立了計算機(jī)這門學(xué)科,因為這行前無古人,所以最早的一批學(xué)者都有其他專業(yè)的背景,有的是電子電氣方面的專家,有來自物理學(xué)科,還有的本來是數(shù)學(xué)家。不同的背景,也就帶來了對計算機(jī)發(fā)展方向的不同觀點。
當(dāng)時,數(shù)學(xué)家們提出的編程語言模型自然具有純數(shù)學(xué)的氣質(zhì),也是最優(yōu)雅、最易于管理的解決方法。這其中的代表人物就是阿隆佐·邱奇(Alonzo Church),邱奇在計算機(jī)誕生之前就提出Lambda演算(Lambda Calculus)的概念,也就是用純函數(shù)的組合來描述計算過程。根據(jù)Lambda演算,如果一個問題能夠用一套函數(shù)組合的算法來描述,那就說明這個問題是可計算的,很自然,也就可以用編程語言的函數(shù)組合的方式來實現(xiàn)這樣的計算過程。
可是,數(shù)學(xué)家們的理念,并沒有在計算機(jī)發(fā)展初期被大范圍應(yīng)用,為什么呢?因為當(dāng)時的硬件制造技術(shù)還很不發(fā)達(dá),電子元件遠(yuǎn)沒有當(dāng)今這樣的水平,那時候每一個電子元件制造成本高,而且體積大,無法在一小片芯片上放置很多元件,無論是運算元件還是存儲元件,都是又慢又貴。
既然物理硬件昂貴,那么只好省著點用了,這種情況下,和硬件靠得最近的物理學(xué)家和電子電氣工程師們掌握了編程語言的主流方向,命令式編程就是這樣發(fā)展起來的。
早期的編程工作中,程序員必須考慮硬件的架構(gòu),如何使用CPU計算資源,如何巧妙利用有限的那么幾個寄存器(register),如果不考慮的話,性能肯定無法過關(guān)。在這樣的硬件條件下,函數(shù)式編程的想法要實現(xiàn),只能通過一層軟件模擬來復(fù)現(xiàn)數(shù)學(xué)家設(shè)想的模型,這多出來的一層無疑要耗費性能,所以光是性能這一個因素,就讓函數(shù)式編程的實踐難以推廣。
還好,電子技術(shù)在飛速發(fā)展,計算機(jī)的運算能力和存儲能力不斷提高。1965年,電子芯片公司Intel的創(chuàng)始人戈登·摩爾根據(jù)觀察,做了這樣的斷言:“當(dāng)價格不變時,集成電路上可容納的元器件的數(shù)目,約每隔18~24個月便會增加一倍,性能也將提升一倍?!边@也就是著名的“摩爾定律”,根據(jù)這個定律,計算機(jī)的計算能力是以指數(shù)趨勢增長,從那之后很長一段時間,軟件行業(yè)也一直在享受計算能力增長帶來的紅利。
但是,進(jìn)入21世紀(jì)之后,大家發(fā)現(xiàn)“摩爾定律”漸漸不管用了,集成電路上的元器件數(shù)目不能增長得這么快,因為,電子部件的密度快要達(dá)到物理極限了,一個集成電路上沒法聚集更多的器件,雖然工程師們還在進(jìn)一步提高CPU的性能,但是,普遍認(rèn)同的觀點是,單核的運算能力不可能保持摩爾定律的增長速度。
這時候,芯片的發(fā)展方向轉(zhuǎn)為多核,軟件架構(gòu)也向分布式方向發(fā)展。這種轉(zhuǎn)化很合理,既然一個核一秒鐘只能做N次運算,那么我用8個核,一秒就能進(jìn)行8*N次運算;同樣,一個CPU中核的數(shù)量雖然是有限的,但是可以把計算量分布在不同的計算機(jī)上,假如一臺計算機(jī)一秒鐘的運算能力是N,那么1000臺計算機(jī),一秒鐘的計算能力就是1000*N。
既然硬件的解決方案只能如此,剩下的唯一問題就是,如何把運算分布到不同的核或者不同的計算機(jī)上去呢?如果是用命令式編程,真的很難,因為編寫協(xié)調(diào)多核或者分布式的任務(wù)處理程序非常困難,讓每個開發(fā)者都做這樣的工作,那真是非常不現(xiàn)實;然而,函數(shù)式編程卻能夠讓大部分開發(fā)者不需要操心任務(wù)處理,所以非常適合分布式計算的場景。
聲明式的函數(shù),讓開發(fā)者只需要表達(dá)“想要做什么”,而不需要表達(dá)“怎么去做”,這樣就極大地簡化了開發(fā)者的工作。至于具體“怎么去做”,讓專門的任務(wù)協(xié)調(diào)框架去實現(xiàn),這個框架可以靈活地分配工作給不同的核、不同的計算機(jī),而開發(fā)者不必關(guān)心框架背后發(fā)生了什么。
與此同時,計算機(jī)業(yè)界也發(fā)現(xiàn),隨著CPU性能和存儲設(shè)備性能的提高,當(dāng)初導(dǎo)致函數(shù)式編程性能問題的障礙,現(xiàn)在都不是問題了,這也給函數(shù)式編程崛起增加了助推力。
可能讀者會問,基于現(xiàn)在的計算機(jī)硬件架構(gòu),用函數(shù)式編程寫出的程序,肯定性能會比命令式編程寫出來的要低一些吧?其實未必。首先,當(dāng)今軟件已經(jīng)是一個很復(fù)雜的系統(tǒng),性能并不完全由是否更直接翻譯為機(jī)器語言決定。其次,在性能相當(dāng)?shù)那闆r下,軟件的開發(fā)速度要比運行速度重要得多。打個比方,用命令式編程,開發(fā)一個網(wǎng)絡(luò)服務(wù)花費6個月,每個請求處理時間是1毫秒;用函數(shù)式編程,開發(fā)同樣的網(wǎng)絡(luò)服務(wù)花費3個月,每個請求處理時間是10毫秒,是否值得花3個月去獲得這9毫秒的性能增長呢?
要知道,從客戶端感知到的反應(yīng)速度,不光包含服務(wù)器端的計算處理時間,還包含網(wǎng)絡(luò)傳輸時間,比如平均網(wǎng)絡(luò)傳輸時間是200毫秒,200毫秒是一個比較正常的網(wǎng)絡(luò)延遲,那么訪問命令式編程服務(wù)的反應(yīng)時間是201毫秒,訪問函數(shù)式編程服務(wù)的反應(yīng)時間是210毫秒。201毫秒和210毫秒,用戶感知的性能沒有那么大的區(qū)別,這時候,我們當(dāng)然更愿意選擇能夠提高開發(fā)速度的方法。
語言演進(jìn)
除了硬件性能和軟件開發(fā)需求的推動,語言的演進(jìn)也是推動函數(shù)式編程被接受的一大動因。
曾幾何時,只有Haskell和LISP這樣的純函數(shù)式編程語言才高舉這面大旗,但是后來,一些本來屬于命令式陣營或者面向?qū)ο箨嚑I的編程語言也開始添加函數(shù)式的特性,這樣,更多的開發(fā)者能夠接觸到函數(shù)式這種編程思想。增加了函數(shù)式特性的這些語言,當(dāng)然包括JavaScript。
如果要學(xué)習(xí)最正統(tǒng)的函數(shù)式編程,比如Haskell,可能需要極大的耐心,因為學(xué)習(xí)過程中要涉及很多數(shù)學(xué)概念和推演,在開始實踐函數(shù)式編程之前,就要面對這么龐大的背景知識,很有可能學(xué)著學(xué)著就睡著了。在《深入淺出RxJS》中,不會灌輸給讀者繁復(fù)的數(shù)學(xué)理論,而是盡量用淺顯易懂的語言和代碼示例來介紹函數(shù)式編程,空有理論沒有實踐是無意義的。
函數(shù)式編程和面向?qū)ο缶幊痰谋容^
要介紹函數(shù)式編程(Functional Programming)就不得不拿另一個編程范式面向?qū)ο缶幊蹋∣bject Oriented Programming)作對比,因為面向?qū)ο缶幊淘?jīng)統(tǒng)治了業(yè)界很長一段時間,而函數(shù)式編程正在逐漸挑戰(zhàn)面向?qū)ο蟮牡匚弧?/span>
這兩種編程方式都可以讓代碼更容易理解,不過方式不同。簡單說來,面向?qū)ο蟮姆椒ò褷顟B(tài)的改變封裝起來,以此達(dá)到讓代碼清晰的目的;而函數(shù)式編程則是盡量減少變化的部分,以此讓代碼邏輯更加清晰。
面向?qū)ο蟮乃枷胧前褦?shù)據(jù)封裝在類的實例對象中,把數(shù)據(jù)藏起來,讓外部不能直接操作這些對象,只能通過類提供的實例方法來讀取和修改這些數(shù)據(jù),這樣就限制了對數(shù)據(jù)的訪問方式。對于毫無節(jié)制任意修改數(shù)據(jù)的編程方式,面向?qū)ο鬅o疑是巨大的進(jìn)步,因為通過定義類的方法,可以控制對數(shù)據(jù)的操作。
但是,面向?qū)ο箅[藏數(shù)據(jù)的特點,帶來了一個先天的缺陷,就是數(shù)據(jù)的修改歷史完全被隱藏了。有人說,面向?qū)ο缶幊烫峁┝艘环N持續(xù)編寫爛代碼的方式,它讓你通過一系列補(bǔ)丁來拼湊程序。這話有點過激,但是也道出了面向?qū)ο缶幊痰娜秉c。
當(dāng)我們在代碼中看到一個對象實例的時候,即使知道了對象的當(dāng)前狀態(tài),也沒法知道這個對象是如何一步一步走到這個狀態(tài)的,這種不確定性導(dǎo)致代碼可維護(hù)性下降。
函數(shù)式編程中,傾向于數(shù)據(jù)就是數(shù)據(jù),函數(shù)就是函數(shù),函數(shù)可以處理數(shù)據(jù),也是并不像面向?qū)ο蟮念惛拍钜粯影褦?shù)據(jù)和函數(shù)封在一起,而是讓每個函數(shù)都不要去修改原有數(shù)據(jù)(不可變性),而且通過產(chǎn)生新的數(shù)據(jù)來作為運算結(jié)果(純函數(shù))。