「前端進階」完全弄懂函數組合

引言

函數組合在函數式編程中被稱爲組合(composition),咱們將瞭解組合的概念並學習大量的例子。而後建立本身的compose函數。javascript

組合的概念是很是直觀的,並非函數式編程獨有的,在咱們生活中或者前端開發中到處可見。前端

好比咱們如今流行的 SPA (單頁面應用),都會有組件的概念,爲何要有組件的概念呢,由於它的目的就是想讓你把一些通用的功能或者元素組合抽象成可重用的組件,就算不通用,你在構建一個複雜頁面的時候也能夠拆分紅一個個具備簡單功能的組件,而後再組合成你知足各類需求的頁面。java

其實函數組合也是相似,函數組合就是一種將已被分解的簡單任務組合成複雜任務的過程。node

什麼是組合

先看一個在 Linux 系統中經常使用的命令 ps -ef | grep nodegit

這個命令的用處是將系統中與 node 有關的進程顯示出來,其中ps -ef是顯示全部進程的全格式,grep node是過濾與node有關的內容,|是將左側的函數的輸出做爲輸入發送給右側的函數。github

這個例子可能微不足道,但它傳達了這樣一個理念:編程

每個程序的輸出能夠是另外一個還沒有可知的程序的輸入設計模式

按照咱們對組合的理解,現假定有compose函數能夠實現以下功能:數組

function compose(...fns){
    //忽略
}
// compose(f,g)(x) === f(g(x))
// compose(f,g,m)(x) === f(g(m(x)))
// compose(f,g,m)(x) === f(g(m(x)))
// compose(f,g,m,n)(x) === f(g(m(n(x))))
//···
複製代碼

咱們能夠看到compose函數,會接收若干個函數做爲參數,每一個函數執行後的輸出做爲下一個函數的輸出,直至最後一個函數的輸出做爲最終的結果。微信

應用 compose 函數

在建立並完善咱們本身的compose函數前,咱們先來學習一下如何應用compose函數。

假定有這樣一個需求:對一個給定的數字四捨五入求值,數字爲字符型。

常規實現:

let n = '3.56';
let data = parseFloat(n);
let result = Math.round(data); // =>4 最終結果
複製代碼

在這段代碼中,能夠看到parseFloat函數的輸出做爲輸入傳遞給Math.round函數以得到最終結果,這是compose函數可以解決的典型問題。

compose函數改寫:

let n = '3.56';
let number = compose(Math.round,parseFloat);
let result = number(n); // =>4 最終結果
複製代碼

這段代碼的核心是經過composeparseFloatMath.round組合到一塊兒,返回一個新函數number

這個組合的過程就是函數式組合!咱們將兩個函數組合在一塊兒以便能及時的構造出一個新函數!

再舉一個例子,假設咱們有兩個函數:

let splitIntoSpaces = str => str.split(' ');
let count = array => array.length;
複製代碼

現但願構建一個新函數以便計算一個字符串中單詞的數量,能夠很容易的實現:

let countWords = compose(count,splitIntoSpaces);
複製代碼

調用一下:

let result = countWords('hello your reading about composition'); // => 5
複製代碼

開發中組合的用處

假設咱們有這樣一個需求:給你一個字符串,將這個字符串轉化成大寫,而後逆序。

咱們的常規思路以下:

let str = 'jspool'

//先轉成大寫,而後逆序
function fn(str) {
    let upperStr = str.toUpperCase()
    return upperStr.split('').reverse().join('')
}

fn(str) // => "LOOPSJ"
複製代碼

這段代碼實現起來沒什麼問題,但如今更改了需求,須要在將字符串大寫以後,將每一個字符拆開並封裝成一個數組:

"jspool" => ["J","S","P","O","O","L"]

爲了實現這個目標,咱們須要更改咱們以前封裝的函數,這其實就破壞了設計模式中的開閉原則。

開閉原則:軟件中的對象(類,模塊,函數等等)應該對於擴展是開放的,可是對於修改是封閉的。

那麼在需求未變動,依然是字符串大寫並逆序,應用組合的思想來怎麼寫呢?

原需求,咱們能夠這樣實現:

let str = 'jspool'

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

let toUpperAndReverse = compose(stringReverse, stringToUpper)
let result = toUpperAndReverse(str) // "LOOPSJ"
複製代碼

那麼當咱們需求變化爲字符串大寫並拆分爲數組時,咱們根本不須要修改以前封裝過的函數:

let str = 'jspool'

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

function stringToArray(str) {
    return str.split('')
}

let toUpperAndArray = compose(stringToArray, stringToUpper)
let result = toUpperAndArray(str) // => ["J","S","P","O","O","L"]

複製代碼

能夠看到當變動需求的時候,咱們沒有打破之前封裝的代碼,只是新增了函數功能,而後把函數進行從新組合。

可能有人會有疑問,應用組合的方式書寫代碼,當需求變動時,依然也修改了代碼,不是也算破壞了開閉原則麼?其實咱們修改的是調用的邏輯代碼,並無修改封裝、抽象出來的代碼,而這種書寫方式也正是開閉原則所提倡的。

咱們假設,如今又修改了需求,如今的需求是,將字符串轉換爲大寫以後,截取前3個字符,而後轉換爲數組,那麼咱們能夠這樣實現:

let str = 'jspool'

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

function getThreeCharacters(str){
    return str.substring(0,3)
}

function stringToArray(str) {
    return str.split('')
}

let toUpperAndGetThreeAndArray = compose(stringToArray, getThreeCharacters,stringToUpper)
let result = toUpperAndGetThreeAndArray(str) // => ["J","S","P"]
複製代碼

從這個例子,咱們能夠知道,組合的方式是真的就是抽象單一功能的函數,而後再組成複雜功能,不只代碼邏輯更加清晰,也給維護帶來巨大的方便。

實現組合

先回看compose函數到底作了什麼事:

// compose(f,g)(x) === f(g(x))
// compose(f,g,m)(x) === f(g(m(x)))
// compose(f,g,m)(x) === f(g(m(x)))
// compose(f,g,m,n)(x) === f(g(m(n(x))))
//···
複製代碼

歸納來講,就是接收若干個函數做爲參數,返回一個新函數。新函數執行時,按照由右向左的順序依次執行傳入compose中的函數,每一個函數的執行結果做爲爲下一個函數的輸入,直至最後一個函數的輸出做爲最終的輸出結果。

若是compose函數接收的函數數量是固定的,那麼實現起來很簡單也很好理解。

只接收兩個參數:

function compose(f,g){
    return function(x){
        return f(g(x));
    }
}
複製代碼

只接收三個參數:

function compose(f,g,m){
    return function(x){
        return f(g(m(x)));
    }
}
複製代碼

上面的代碼,沒什麼問題,可是咱們要考慮的是compose接收的參數個數是不肯定的,咱們考慮用rest參數來接收:

function compose(...fns){
    return function(x){
        //···
    }
}
複製代碼

如今compose接收的參數fns是一個數組,那麼如今思考的問題變成了,如何將數組中的函數從右至左依次執行。

咱們選擇數組的reduceRight函數來實現:

function compose(...fns){
    return function(x){
        return fns.reduceRight(function(arg,fn){
            return fn(arg);
        },x)
    }
}
複製代碼

這樣咱們就實現了compose函數~

實現管道

compose的數據流是從右至左的,由於最右側的函數首先執行,最左側的函數最後執行!

但有些人喜歡從左至右的執行方式,即最左側的函數首先執行,最右側的函數最後執行!

從左至右處理數據流的過程稱之爲管道(pipeline)!

管道(pipeline)的實現同compose的實現方式很相似,由於兩者的區別僅僅是數據流的方向不一樣而已。

對比compose函數的實現,僅需將reduceRight替換爲reduce便可:

function pipe(...fns){
    return function(x){
        return fns.reduce(function(arg,fn){
            return fn(arg);
        },x)
    }
}
複製代碼

組合相比,有些人更喜歡管道。這只是我的偏好,與底層實現無關。重點是pipecompose作一樣的是事情,只是數據流放行不一樣而已!咱們能夠在代碼中使用pipecompose,但不要同時使用,由於這會在團隊成員中引發混淆。若是要使用,請堅持只用一種組合的風格。

系列文章推薦

參考

寫在最後

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊關注
  • 本文同步首發與github,可在github中找到更多精品文章,歡迎Watch & Star ★
  • 後續文章參見:計劃

歡迎關注微信公衆號【前端小黑屋】,每週1-3篇精品優質文章推送,助你走上進階之旅

相關文章
相關標籤/搜索