函數式編程——入門筆記與React實踐

前言

最近在看近來很火的函數式編程教程《Mostly Adequate Guide》 (中文版:《JS函數式編程指南》),收穫很大。對於函數式編程的初學者,這本書不只深刻淺出,更讓人感覺到函數式編程的優點和美感,強烈推薦給想要學習函數式編程的朋友。html

這篇文章是我我的的一個學習筆記,在總結知識的同時,也嘗試以React組件的輸入事件響應爲例,用函數式編程去應對實際項目中的場景。git

下文涉及React的代碼出於閱讀考慮有必定刪減,完整代碼在個人Githubgithub

lodash與ramda部分代碼因爲比較簡單,想看運行結果的話能夠直接到lodashramda官網打開console運行。chrome

純函數

純函數引用原書的描述:編程

純函數是這樣一種函數,即相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用。數組

相同的輸入,永遠會獲得相同的輸出,一般意味着對外部狀態解耦。安全

所謂外部狀態,最多見的例子就是this,若是你的函數是:ide

function(){
    return 'hello, ' + this.name;
}

那它不多是純函數——你永遠不知道this.name會被誰改寫,測試用例也不可能覆蓋全部狀況。若是正巧有一個外部函數,它每隔一個月將this.name改寫成'shit',你和測試人員熬了幾個通宵沒有發現一點問題,你也信心滿滿——用函數給客人打招呼實在太簡單。項目上線後,你買好機票正準備出門度假,卻接到老闆的電話讓你滾回公司改bug,而你對事情的情況沒有一點頭緒……函數式編程

提倡函數式編程的人認爲,這種共享狀態致使的混亂是絕大多數bug的萬惡之源函數

其實某種程度上這早已成爲共識:不提倡全局變量其實就是這個道理。也許深入意識到純函數的優點還須要一點時間,也許你以爲純函數不錯,但對於如何在項目中使用它徹底沒有頭緒,不用着急,如今咱們暫時先記住:

作一個純粹的函數,一個脫離了低級趣味的函數

Curry與Compose

curry和compose能夠說是函數式編程的衆妙之門,並且必須相輔相成才見威力。就我我的而言,見過一些講函數式的教程,講了curry,我也知道了什麼是curry,可是curry怎麼用?能帶來什麼好處呢?仍是沒講清楚,而後快馬加鞭地往前講functor講monad,做爲資質不那麼高的函數式菜鳥,很快就雲裏霧中,不明覺厲了。

Curry

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>
        )
    }
}

完整代碼 step_1

藉助高階函數式的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>
        )
    }
}

完整代碼 step_2

部分應用的特性使得咱們能夠把關注點分散到每個參數,在render函數中

const setField = setFieldOnContext(this);

設定了當前上下文,由於你確定不會設置其它component的state,而具體到每個onChange則關注不一樣的目標key。

也許你會想curry是讓代碼變得好看了一點,但也僅此而已,它只是用新的姿式解決問題,並無解決新的問題或產生新的價值。

固然不是,curry真正產生的價值和魅力的地方,是它對組合的友好。

Compose

對邏輯進行組合,這樣的需求其實很常見,當我想要:

將一個數組去重,而後篩選,最後排序

不少時候會寫成這樣:

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>
        )
    }
}

完整代碼 step_3

藉助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>
        )
    }
})

完整代碼 step_4

數一數,咱們一會兒有了六個函數!或許你會爲此感到不安:是否是弄錯了什麼?

沒必要擔憂,仔細看看,這六個函數都有各自的複用價值,隨着項目的發展和膨脹,響應事件值的需求隨處可見,而重複的代碼和邏輯會慢慢蠶食可維護性。把高度解耦的函數們(好比valueAdapter們)組合起來,會讓咱們更輕鬆的應對挑戰。

還有一點,上面六個函數中有五個都是純函數!除了setFieldOnContext,每一個函數的輸入輸出都是惟一映射的(雖然有的輸出是函數),沒有依賴外部狀態,也沒有任何的反作用。

追求純函數有時候會比較困難,但它是值得的,若是你的函數依賴了this,或者其它外部狀態,那最好從新審視你的代碼——至少把不安全的依賴剝離到最小範圍。setFieldOnContext就是個例子,藉助context變量而不是this,咱們能夠不用在乎compose出來的event handler的this是誰,如何傳遞。自由組合函數的前提,就是它們無論在哪兒都始終如一。儘管React的setState返回undefined致使setFieldOnContext不能成爲純函數,咱們也盡力讓它更加接近這一目標

固然這一版本的實現仍不完美:setFieldOnContext把值直接設到了state的屬性上,有時候這並非咱們想要的結果。在此我先不給出實現,留給看官思考和動手。

補充:個人實現見 代碼step_5 , 感謝 Young 的建議與啓發

下一篇,我會藉助Promise這個老面孔來介紹Functor和Monad——這兩個你甚至沒有見過,卻無處不在的概念。

本文原載於個人Github,轉載請註明出處

相關文章
相關標籤/搜索