最近在看近來很火的函數式編程教程《Mostly Adequate Guide》 (中文版:《JS函數式編程指南》),收穫很大。對於函數式編程的初學者,這本書不只深刻淺出,更讓人感覺到函數式編程的優點和美感,強烈推薦給想要學習函數式編程的朋友。html
這篇文章是我我的的一個學習筆記,在總結知識的同時,也嘗試以React組件的輸入事件響應爲例,用函數式編程去應對實際項目中的場景。git
下文涉及React的代碼出於閱讀考慮有必定刪減,完整代碼在個人Github。github
lodash與ramda部分代碼因爲比較簡單,想看運行結果的話能夠直接到lodash或ramda官網打開console運行。chrome
純函數引用原書的描述:編程
純函數是這樣一種函數,即相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用。數組
相同的輸入,永遠會獲得相同的輸出
,一般意味着對外部狀態解耦。安全
所謂外部狀態,最多見的例子就是this,若是你的函數是:ide
function(){ return 'hello, ' + this.name; }
那它不多是純函數——你永遠不知道this.name會被誰改寫,測試用例也不可能覆蓋全部狀況。若是正巧有一個外部函數,它每隔一個月將this.name改寫成'shit'
,你和測試人員熬了幾個通宵沒有發現一點問題,你也信心滿滿——用函數給客人打招呼實在太簡單。項目上線後,你買好機票正準備出門度假,卻接到老闆的電話讓你滾回公司改bug,而你對事情的情況沒有一點頭緒……函數式編程
提倡函數式編程的人認爲,這種共享狀態致使的混亂是絕大多數bug的萬惡之源函數
其實某種程度上這早已成爲共識:不提倡全局變量其實就是這個道理。也許深入意識到純函數的優點還須要一點時間,也許你以爲純函數不錯,但對於如何在項目中使用它徹底沒有頭緒,不用着急,如今咱們暫時先記住:
作一個純粹的函數,一個脫離了低級趣味的函數
curry和compose能夠說是函數式編程的衆妙之門,並且必須相輔相成才見威力。就我我的而言,見過一些講函數式的教程,講了curry,我也知道了什麼是curry,可是curry怎麼用?能帶來什麼好處呢?仍是沒講清楚,而後快馬加鞭地往前講functor講monad,做爲資質不那麼高的函數式菜鳥,很快就雲裏霧中,不明覺厲了。
curry的本質是函數的部分應用
。聽起來有點遙遠,事實上相似的需求咱們常常會遇到:
class Form extends React.Component { setField(key){ return (e)=>{ this.setState({ [key]: e.target.value }) } } render(){ const {name, address} = this.state; return ( <form> <input value={name} onChange={this.setField('name')} /> <input value={address} onChange={this.setField('address')} /> </form> ) } }
藉助高階函數式的function return function,對不一樣的key咱們可以複用響應事件並setState的邏輯,上例能夠認爲就是脫掉了馬甲的部分應用
。
換個寫法試試:
const setFieldOnContext = _.curry(function(context, key, e){ context.setState({ [key]: e.target.value }) }); class Form extends React.Component{ render(){ const {name, address} = this.state; const setField = setFieldOnContext(this); return ( <form> <input value={name} onChange={setField('name')} /> <input value={address} onChange={setField('address')} /> </form> ) } }
部分應用的特性使得咱們能夠把關注點分散到每個參數,在render函數中
const setField = setFieldOnContext(this);
設定了當前上下文,由於你確定不會設置其它component的state,而具體到每個onChange則關注不一樣的目標key。
也許你會想curry是讓代碼變得好看了一點,但也僅此而已,它只是用新的姿式解決問題,並無解決新的問題或產生新的價值。
固然不是,curry真正產生的價值和魅力的地方,是它對組合的友好。
對邏輯進行組合,這樣的需求其實很常見,當我想要:
將一個數組去重,而後篩選,最後排序
不少時候會寫成這樣:
import _ from 'lodash' function filterFn(v){ return typeof v === 'number'; } function sortFn(v){ return Math.abs(v); } _.sortBy(_.filter(_.uniq([1, 1, 3, 4, 2, 'a', -10]), filterFn), sortFn); // -> [1, 2, 3, 4, -10]
嵌套的代碼難以閱讀,就像回調地獄同樣。天然的邏輯應該是順序而非嵌套的,所以不少人會更喜歡」鏈式「寫法:
_([1, 1, 3, 4, 2, 'a', -10]). uniq(). filter(filterFn). sortBy(sortFn). value(); // -> [1, 2, 3, 4, -10]
看起來順眼多了,用瓶子把東西封起來操做的思路很棒(functor
就是這麼幹的,下次咱們會細說)。然而問題在於,_(x)
的原型鏈上可供咱們鏈式調用的函數是有限的,這限制了咱們的邏輯表現力。
一個典型的場景是代碼調試:咱們想知道每一步的返回值,以便定位問題。然而不管是單步調試仍是log打印,在面對鏈式代碼時都顯得有些一籌莫展(chrome devtool能夠選中部分代碼並執行,但對編譯生成的代碼無論用),若是你不想每次debug都把要打印的值扔給臨時變量搞得一地雞毛的話,或許能夠這樣:
//_ is lodash _.prototype.log = function log(label){ var value = this.value(); console.log(label, value); return _(value); }; _([1, 1, 3, 4, 2, 'a', -10]). uniq(). log('does uniq() works right? '). filter(filterFn). sortBy(sortFn). value();
惋惜lodash原生並無提供這樣的log函數。這不難理解,原型鏈有盡而需求場景無窮,擴充原型來知足業務場景是註定被動的。
即便你打算打破教條
不是你的對象不要動
——隔壁老王法則
決定像上面代碼同樣擴充第三方對象的原型,這個log函數仍然有太多怪異的地方,解包var value = this.value()
和封包return _(value)
的過程讓人感到多餘——有種脫掉褲子,放了個屁,而後穿回去的即視感。更重要的是這當中還伴隨着對this關鍵字的依賴,或許你如今以爲沒什麼大不了的,但我但願你在看完這篇文章後能對this有更審慎的想法。
若是你還有其它更好的debug方法和經驗,請必定分享出來。不過如今,讓咱們以Ramda爲例,看看在函數式的世界裏,問題是如何被解決的:
import R from 'ramda' var log = R.curry(function (label, value){ console.log(label, value); return value }); R.compose( R.reverse, log('why we need a reverse ?'), R.sort(sortFn), R.filter(filterFn), R.uniq )([1, 1, 3, 4, 2, 'a', -10]) // -> [1, 4, 3, 2, -10]
R.compose
接收一組函數並返回了一個新的函數,而數據就像通過一條邏輯流水線同樣,從最後一個函數,一步步地向前接受處理。
R.sort(fn, data)
和R.filter(fn, data)
都是curry函數,你應該已經注意到它們和lodash的同名函數有所不一樣——參數順序是相反的。這就是curry與compose協同工做的奧祕:compose一般只能針對一元函數,而curry則使得多元函數能夠一元化。
函數curry化,並把可變性高複用性低的參數後置,是函數式庫的特徵之一,也是寫自定義函數時須要注意的地方。咱們的log函數就遵循了這一點。
組合相比鏈式最大的優點,是函數能夠自由而專一:再也不受原型鏈的約束,也再也不看this的臉色。對比以前的log函數,如今的版本沒有了多餘的解包與封包,也再也不依賴this——如今它是一個純函數。
不少人會用_
而不是R
做爲ramda的變量名,咱們接下來也會這樣
關於組合更多的內容,仍是強烈建議移步《Mostly Adequate Guide》的 第 5 章: 代碼組合
在大體瞭解了函數組合後,讓咱們繼續前面事件響應的例子,先回顧一下,以前咱們用curry改寫了setField
函數,獲得setFieldOnContext
:
const setFieldOnContext = _.curry(function(context, key, e){ context.setState({ [key]: e.target.value }) }); class Form extends React.Component{ render(){ const {name, address} = this.state; const setField = setFieldOnContext(this); return ( <form> <input value={name} onChange={setField('name')} /> <input value={address} onChange={setField('address')} /> </form> ) } }
setFieldOnContext
函數已經能幫咱們節省一些重複代碼,就像它的前輩setField
同樣,然而它的職責還分離得不夠乾淨:對e.target.value
的依賴使得它只能處理原生事件對象。假設咱們有一些第三方組件(好比接下來會遇到的X組件),它們的onChange
拋出了並不標準的事件對象,甚至可能直接把value扔了出來。看起來setFieldOnContext
有些不從心,難道咱們只能回到複製--粘貼--修改
的懷抱嗎?是時候借用組合的力量了:
import _ from 'ramda' const getValueFromEvent = function(e){ return e.target.value; }; const getValueFromX = function(x){ return x.value } const setFieldOnContext = _.curry(function(context, key, value){ context.setState({ [key]: value }) }); class Form extends React.Component{ render(){ const {name, x} = this.state; const setField = setFieldOnContext(this); return ( <form> <input value={name} onChange={_.compose(setField('name'), getValueFromEvent)} /> <X value={address} onChange={_.compose(setField('address'), getValueFromX)} /> </form> ) } }
藉助compose,咱們的函數職責更加分離,setField只關心設值,對值的轉換則由其它函數負責,雖然目前實現的版本用起來還有一些囉嗦,但咱們獲得了三個關注點(職責)高度分離的、可複用的函數。
在接着討論前,讓咱們先統一一下用詞,下面我會把getValueFromEvent
和getValueFromX
這樣的值轉換函數稱做valueAdapter,正如它們的角色(適配器模式中的適配器)同樣。
剛剛的代碼之因此囉嗦,問題出在參數順序和複用度不一致。
_.compose(..., valueAdapter)
其本質是對一類事件進行適配,而咱們把它放在參數最後,這致使適配的工做落在了每一次事件聲明上。隨着項目的發展,狀況會是這樣:
<form> <input value={name} onChange={_.compose(setField('foo'), getValueFromEvent)} /> <input value={name} onChange={_.compose(setField('bar'), getValueFromEvent)} /> <input value={name} onChange={_.compose(setField('baz'), getValueFromEvent)} /> <input value={name} onChange={_.compose(setField('baa'), getValueFromEvent)} /> <input value={name} onChange={_.compose(setField('zzz'), getValueFromEvent)} /> </form>
滿眼的getValueFromEvent
,徹底背離了咱們抽象出valueAdapter
的初衷!
這重申了curry的要點:一般咱們會按照複用程度從高到低地排列參數,好比在同一個組件中,context的複用度最高,而key則次之,event沒有複用度——每一個事件源都是單獨的。至於valueAdapter
們,它們的複用範圍是一類組件。所以,在上例中咱們更但願獲得一個參數順序相似於fn(valueAdapter, context, key, event)
的函數。
下面是封裝一層函數作參數順序轉換而後curry化的簡單實現:
import _ from 'ramda' const getValueFromEvent = function(e){ return e.target.value; }; const getValueFromX = function(x){ return x.value } const setFieldOnContext = _.curry(function(context, key, value){ context.setState({ [key]: value }) }); const getFieldSetter= _.curry(function(valueAdapter, context, name){ //返回真正的event handler return _.compose(setFieldOnContext(context, name), valueAdapter); }); const setFieldForEvent = getFieldSetter(getValueFromEvent); const setFieldForX = getFieldSetter(getValueFromX); React.createClass({ render(){ const {name, x} = this.state; return ( <form> <input value={name} onChange={setFieldForEvent(this, 'name')} /> <X value={x} onChange={setFieldForX(this, 'x')} /> </form> ) } })
數一數,咱們一會兒有了六個函數!或許你會爲此感到不安:是否是弄錯了什麼?
沒必要擔憂,仔細看看,這六個函數都有各自的複用價值,隨着項目的發展和膨脹,響應事件值的需求隨處可見,而重複的代碼和邏輯會慢慢蠶食可維護性。把高度解耦的函數們(好比valueAdapter們)組合起來,會讓咱們更輕鬆的應對挑戰。
還有一點,上面六個函數中有五個都是純函數!除了setFieldOnContext
,每一個函數的輸入輸出都是惟一映射的(雖然有的輸出是函數),沒有依賴外部狀態,也沒有任何的反作用。
追求純函數有時候會比較困難,但它是值得的,若是你的函數依賴了this,或者其它外部狀態,那最好從新審視你的代碼——至少把不安全的依賴剝離到最小範圍。setFieldOnContext
就是個例子,藉助context
變量而不是this
,咱們能夠不用在乎compose出來的event handler的this是誰,如何傳遞。自由組合函數的前提,就是它們無論在哪兒都始終如一。儘管React的setState返回undefined
致使setFieldOnContext
不能成爲純函數,咱們也盡力讓它更加接近這一目標
固然這一版本的實現仍不完美:setFieldOnContext
把值直接設到了state的屬性上,有時候這並非咱們想要的結果。在此我先不給出實現,留給看官思考和動手。
下一篇,我會藉助Promise這個老面孔來介紹Functor和Monad——這兩個你甚至沒有見過,卻無處不在的概念。
本文原載於個人Github,轉載請註明出處