最近在思考一個問題杖狼,函數(shù)式編程對于我們的軟件開發(fā)的意義到底有多大澡屡?到底值不值得我們花時間去學習猿挚。因此,寫下這篇文章來記錄自己的思考驶鹉。文章包含了前后端開發(fā)中的一些內(nèi)容绩蜻,大家各自選擇閱讀。
首先還是簡單說下函數(shù)式編程是什么室埋?
它詳細的解釋可以參考維基百科办绝。緣起數(shù)學家Alonzo Church提出了Lambda演算的概念伊约,可以用函數(shù)組合的方式來描述計算過程,換句話來說孕蝉,如果一個問題能夠用一系列函數(shù)組合的算法來表達屡律,那么這個問題就認為是可計算的。
它和面向?qū)ο缶幊桃粯游羟彩且环N編程范式疹尾。強調(diào)執(zhí)行的過程而非結果,通過一系列的嵌套的函數(shù)調(diào)用骤肛,完成一個運算過程纳本。
它主要有以下幾個特點:
- 函數(shù)是"一等公民":函數(shù)優(yōu)先,和其他數(shù)據(jù)類型一樣腋颠。
- 只用"表達式"繁成,不用"語句":通過表達式(expression)計算過程得到一個返回值,而不是通過一個語句(statement)修改某一個狀態(tài)淑玫。
- 無副作用:不污染變量巾腕,同一個輸入永遠得到同一個數(shù)據(jù)。
- 不可變性:前面一提到絮蒿,不修改變量尊搬,返回一個新的值。
函數(shù)式編程的概念其實出來也已經(jīng)好幾十年了土涝,我們能在很多編程語言身上看到它的身影佛寿。比如比較純粹的Haskell,以及一些語言開始逐漸成為多范式編程語言但壮,比如Swift冀泻,還有Kotlin,Java蜡饵,Js等都開始具備函數(shù)式編程的特性弹渔。這么多語言開始逐漸有了支持,F(xiàn)P對于我們的生活到底能夠帶來一些什么好處呢溯祸。
為了討論這個問題肢专,還是舉一些在不同場景下的應用來看看吧。
FP在前端開發(fā)中的應用場景
提到現(xiàn)代前端開發(fā)焦辅,那么React肯定是逃不開的一個話題鸟召。在React技術棧中,F(xiàn)P有哪些體現(xiàn)呢氨鹏?
-
Stateless components
在React 0.14之后推出的,先來看一段代碼
function MyComponent(props) {
return <div>My props name {props.name}</div>
}
這是一個簡單的無狀態(tài)組件压状,我們沒有用createClass
或者是extends React.Component
來創(chuàng)建一個組件仆抵,而是通過一個Pure function返回了一個組件跟继。
那么這里的好處是什么呢?
- 簡潔镣丑,一眼可以看出這個組件的作用舔糖;
- 無副作用,只要傳入同一個props那么render出來的組件一定是相同的莺匠;
- 測試更友好金吗;
- 沒有
this
,要知道this
還是難倒了好多英雄好漢的趣竣; - 更容易實現(xiàn)SSR(這一點我并未考證摇庙,有知道的朋友可以補充)。
當然遥缕,使用Stateless component
并不是萬能的卫袒,可以很明顯的看到?jīng)]有了React的生命周期,這個問題通常我們會結合HOC來解決单匣。你看這一點就印證了前面說的夕凝,通過函數(shù)的組合完成對結果的表達,是不是很有意思户秤。
-
Redux
在前端應用越來越復雜的今天码秉,數(shù)據(jù)流管理是一件很重要的事,Redux就是來解決這個問題的鸡号。它是Flux架構的演化實現(xiàn)转砖,官方Github解釋為Predictable state container(可預測狀態(tài)機)。
在Redux中我們存在一個單的樹形結構的state膜蠢,單一數(shù)據(jù)源降低了多數(shù)據(jù)源的信任問題堪藐。State是通過每個reducer的結果組合而來的,每個reducer都是一個Pure function挑围,如下:
export const isLoading = (state = false, action) => {
switch (action.type) {
case MarketActionsTypes.FETCH_MARKET_DATA_START: {
return false;
}
default:
return false;
}
};
在reducer中不會直接修改每個state中的狀態(tài)礁竞,而是返回一個新的狀態(tài),然后整個state的結果通過一個個reducer的結果歸納出來杉辙。我們來看reducer源碼是怎么工作的:
....
// from https://github.com/reduxjs/redux/blob/b02310b359a0832f65873d024570d411b465ced9/src/combineReducers.js#L162
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
....
我們可以很直觀的看到模捂,最后的state結果是通過將每個reducer生成的局部結果組合起來得到一個新的nextState,而不是直接在原有的state上進行修改蜘矢。
所以狂男,我們再回過頭來看看它的定義——可預測狀態(tài)機。
每個reducer都是一個純函數(shù)品腹,只要輸入恒定岖食,那么輸出肯定是恒定的。同時舞吭,無副作用的特性可以保證state不會被意外修改泡垃,那么整個應用的state都是可以準確的知道的析珊。
當你明白redux是怎么工作之后,你可能會發(fā)現(xiàn)自己都可以寫一個dev-tool出來了蔑穴。
說到這里忠寻,咱們會發(fā)現(xiàn)這和后面要提到的Lambda架構有很多相似的地方,后面再談存和。這里簡單提一下奕剃,最終的結果可以通過一系列的信息組合得來,這是一個很重要的改變捐腿。
FP在后端開發(fā)中的應用
-
Lambda架構
Lambda是將計算施加于大量數(shù)據(jù)的一種通用方法纵朋。
傳統(tǒng)的數(shù)據(jù)系統(tǒng)擅長業(yè)務的處理,但是在面臨數(shù)據(jù)分析叙量、生成報表等任務的時候就顯得很乏力了倡蝙。
當然,Lambda架構有很多的優(yōu)點绞佩,這里只重點討論下在函數(shù)式編程方面的一些體現(xiàn)寺鸥。
這兒要提到的一個詞就是MapReduce
,先貼一個圖品山。
架構中運用函數(shù)式編程的方式將問題進行抽象胆建,拆分成一個映射操作(Map)和一個化簡(Reduce)操作,這有什么好處呢肘交?
首先笆载,函數(shù)式編程無副作用的特點天生對并行編程提供了良好的支持。在使用多線程的方案的時候涯呻,我們常常會遇到共享狀態(tài)的問題凉驻,因此我們可能會采用各種各樣的鎖機制。一旦引入了鎖复罐,那么代碼本身的復雜度也就增加了涝登。而當我們采用FP的時候,不用太擔心這樣的問題效诅,函數(shù)操作無副作用胀滚,不用擔心共享數(shù)據(jù)帶來的問題。
其次乱投,對數(shù)據(jù)分析更加友好咽笼。在Lambda架構中,通常會把數(shù)據(jù)分為兩種類型戚炫,原始數(shù)據(jù) 和衍生數(shù)據(jù)剑刑。那什么叫原始數(shù)據(jù)呢,比如咱們看到的維基百科的頁面双肤,是經(jīng)過了很多次編輯的施掏。那么每次編輯這個操作就叫做原始數(shù)據(jù)层宫,這個動作是不可改的,一旦形成后就記錄在那里了其监。那么最后我們看到的頁面就是經(jīng)過這些原始數(shù)據(jù)Reduce操作后得到的衍生數(shù)據(jù)。
數(shù)據(jù)不可變性很大程度上降低了數(shù)據(jù)庫的復雜度限匣,同時提供了對并行計算的良好支持抖苦。當然,Lambda架構也不是說沒有缺點的米死,比如批處理層過慢锌历,同時維護多個視圖等等,這我了解不深也不是本文的范圍就不聊了峦筒。
這里我們在回過頭來看前面寫到的Redux的設計會發(fā)現(xiàn)有很多異曲同工之妙究西,每個Action出發(fā)的操作都可以看做是不可改的原始數(shù)據(jù),通過reducer純函數(shù)得到的結果始終是穩(wěn)定的物喷,最終的結果就是將多個reducer的結果組合起來卤材。
函數(shù)式編程和函數(shù)響應式編程
可能很多做移動端開發(fā)的朋友會更多的聽到這個概念,比如iOS開發(fā)上最早有ReactiveCocoa峦失,后來又有了RxSwift扇丛,安卓上也有常見的RxJava等等。這里以ReactiveX(Rx)來說尉辑,最早由微軟的架構師Erik Meijer領導的團隊開發(fā)帆精,目前各種版本幾乎覆蓋率主流的編程語言。
那么Rx和函數(shù)式編程的關系是什么呢隧魄?
我的理解是卓练,Rx是一種以函數(shù)式編程為基礎之一的編程模型,引入了流的概念购啄,以一種統(tǒng)一的方式處理異步事件的機制襟企。貼一張官方的圖來看看:
在Rx的世界中,所有的異步事件都可以用一個Observable數(shù)據(jù)流來表示闸溃,數(shù)據(jù)流里的型號可能是一次網(wǎng)絡請求整吆,可能是一次點擊操作。當數(shù)據(jù)到來的時候辉川,我們可以通過一個個函數(shù)的組合對事件進行處理表蝙,最終產(chǎn)生一個結果。如下的一個例子乓旗,將一個搜索框searchBar的輸入事件變?yōu)橐粋€數(shù)據(jù)流府蛇,然后可以對這個數(shù)據(jù)流上組合任何的操作。
let searchResults = searchBar.rx
.text
.orEmpty
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { query -> Observable<[Repository]> in
if query.isEmpty {
return .just([])
}
return searchGitHub(query)
.catchErrorJustReturn([])
}
.observeOn(MainScheduler.instance)
有了函數(shù)式編程的加持屿愚,我們可以很清晰的理解這段代碼的意思:將輸入框的文本變化轉為數(shù)據(jù)流->過濾空字符串->控制數(shù)據(jù)流產(chǎn)生的最小時間時間價格->內(nèi)容沒有變化時信號不會繼續(xù)傳播->然后對最近的信號映射為一次API請求汇跨,然后在主隊列上進行觀測信號务荆。整個代碼的行為都比用命令式編程來的清晰直觀。如果是用傳統(tǒng)的命令式編程的方式來寫這段代碼的話穷遂,光代碼量可能就得膨脹好幾倍函匕,更不用提帶來的復雜度的問題了。當然蚪黑,這里有一個問題就是在debug的時候可能會稍微麻煩一點盅惜,不過這一點可以通過較為完備的測試來解決。
那么到底會不會讓我們生活變得更好
寫了很多例子忌穿,我們回過頭來再討論一下函數(shù)式編程到底對于我們編碼來說意義有多大抒寂。
就我個人來說的話,它會大大增加我的生產(chǎn)力掠剑。但誠然函數(shù)式編程存在很多的優(yōu)點屈芜,但是也并不是一招鮮吃遍天的。
我覺得比較麻煩的一個點就是它的學習成本相對來說要高一點朴译,其實最主要的是思維的轉變井佑。
FP的抽象程度更高,和大家從學校里就開始接觸的編碼方式截然不同动分。所以當團隊平均水平不是那么高的時候毅糟,這一點確實可能會成為我們做技術選型要考量的一個關鍵因素。不過我倒是認為軟件開發(fā)者都應該去學一學函數(shù)式編程澜公。就拿抽象這個事情來說姆另,軟件開發(fā)上有一句所謂的“名言”:“沒有什么問題是不能通過抽象來解決的,如果有坟乾,那就再加一層抽象”迹辐。在函數(shù)式編程中,我們將一個個復雜的問題抽象成一個個過程的表達甚侣,然后再將不同的過程結果組合起來明吩,更加容易找到問題的解決辦法。對于我們在其他領域的編碼也是一樣的道理殷费,剝離問題表面印荔,還原問題本質(zhì)。有了這樣的思維的時候详羡,當你和別人在看同一個問題的時候仍律,你會更容易有一種撥云見日的感覺。
除了抽象的能力实柠,分解問題的能力也是很重要的一個啟發(fā)水泉。將問題化小,分而治之,然后組合結果草则。當然這個能力不僅僅是FP才具備的钢拧,分治法在很多算法里已經(jīng)體現(xiàn)得淋漓盡致了。不過這里還是想再提一下這個話題炕横,分而治之可以在各個維度的工作上進行運用源内,小到一個算法的具體實現(xiàn),然后到一個問題的過程分解份殿,甚至大到一個工作任務的拆解姿锭,都可以用分而治之的思維去尋找解題之法。
當然FP還有其他的一些不足之處伯铣,比如有人會說在FP中數(shù)據(jù)復制可能會比較嚴重,可能會造成性能問題轮纫。這個問題我是這樣看的腔寡,局部來說他可能確實看起來會存在一定的影響。但是從另一個角度來說掌唾,在我們使用FP的時候放前,不用擔心全局變量被破壞,沒有執(zhí)行順序的依賴糯彬。我們在并行編程的時候凭语,也不需要依賴于過多的鎖的,那么反而最終可以提升最終性能撩扒。
最后的話
寫到這里似扔,稍微感覺寫得有點發(fā)散了。但是貫穿全文的其實也都是圍繞函數(shù)式編程的一些特性:抽象搓谆、組合炒辉、不可變等等。
作為軟件行業(yè)的從業(yè)者泉手,即使現(xiàn)在不懂它沒有關系黔寇,但是千萬別因為別人的危言聳聽而不敢去嘗試。記得以前公司里有人要討論TDD到底好不好這個問題斩萌,其中一方直接懟回去“Talk is cheap, show me the code"缝裤,只有當你真正去嘗試了,你才有資格去討論它到底好不好颊郎。這里我要借用下馬云大大的曾經(jīng)說的商機來臨要經(jīng)歷的四個階段了憋飞,“看不見“、“看不起“袭艺、“看不懂”搀崭、“來不及”,對于任何一門新技術來說也是這樣(何況,F(xiàn)P并不是什么新技術)瘤睹。面對一門技術升敲,勇敢地去試一試,然后相信你會有自己的判斷轰传。
我的博客即將搬運同步至騰訊云+社區(qū)驴党,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=2mhtssrg4f8k0
參考資料: