JS輕量級函數式編程實現XDM三

前言

這是【JS如何函數式編程】系列文章第三篇。點贊👍關註👀,持續追蹤😄

前兩篇傳送門:

《XDM,JS如何函數式編程?看這就夠瞭!(一)》

《XDM,JS如何函數式編程?看這就夠瞭!(二)》

在第二篇,我們談瞭基礎之基礎,重要之重要——“偏函數”,偏函數通過函數封裝,實現瞭減少傳參數量的目的,解決瞭手動指定實參的麻煩。

更具重要意義的是:

當函數隻有一個形參時,我們能夠比較容易地組合它們。這種單元函數,便於進行後續的組合函數;

沒錯,本篇就是談關於 “組合函數”。它是函數編程的重中之重之重之重重重!

組合函數

含義

函數編程就像拼樂高!

樂高有各式各樣的零部件,我們將它們組裝拼接,拼成一個更大的組件或模型。

函數編程也有各種功能的函數,我們將它們組裝拼接,用於實現某個特定的功能。

下面來看一個例子,比如我們要使用這兩個函數來分析文本字符串:

function words(str) {
    return String( str )
        .toLowerCase()
        .split( /\s|\b/ )
        .filter( function alpha(v){
            return /^[\w]+$/.test( v );
        } );
}
function unique(list) {
    var uniqList = [];
    for (let i = 0; i < list.length; i++) {
        if (uniqList.indexOf( list[i] ) === -1 ) {
            uniqList.push( list[i] );
        }
    }
    return uniqList;
}
var text = "To compose two functions together";
var wordsFound = words( text );
var wordsUsed = unique( wordsFound );
wordsUsed;
//  ["to", "compose", "two", "functions", "together"]

不用細看,隻用知道:我們先用 words 函數處理瞭 text,然後用 unique 函數處理瞭上一處理的結果 wordsFound;

這樣的過程就好比生產線上加工商品,流水線加工。

想象一下,如果你是工廠老板,還會怎樣優化流程、節約成本?

這裡作者給瞭一種解決方式:去掉傳送帶!

即減少中間變量,我們可以這樣調用:

var wordsUsed = unique( words( text ) );
wordsUsed

確實,少瞭中間變量,更加清晰,還能再優化嗎?

我們還可以進一步把整個處理流程封裝到一個函數內:

function uniqueWords(str) {
    return unique( words( str ) );
}
uniqueWords(text)

這樣就像是一個黑盒,無需管裡面的流程,隻用知道這個盒子輸入是什麼!輸出是什麼!輸入輸出清晰,功能清晰,非常“幹凈”!如圖:

與此同時,它還能被搬來搬去,或再繼續組裝。

我們回到 uniqueWords() 函數的內部,它的數據流也是清晰的:

uniqueWords <-- unique <-- words <-- text

封裝盒子

上面的封裝 uniqueWords 盒子很 nice ,如果要不斷的封裝像 uniqueWords 的盒子,我們要一個一個的去寫嗎?

function uniqueWords(str) {
    return unique( words( str ) );
}
function uniqueWords_A(str) {
    return unique_A( words_A( str ) );
}
function uniqueWords_B(str) {
    return unique_B( words_B( str ) );
}
...

所以,一切為瞭偷懶,我們可以寫一個功能更加強大的函數來實現自動封裝盒子:

function compose2(fn2,fn1) {
    return function composed(origValue){
        return fn2( fn1( origValue ) );
    };
}
// ES6 箭頭函數形式寫法
var compose2 =
    (fn2,fn1) =>
        origValue =>
            fn2( fn1( origValue ) );

接著,調用就變成瞭這樣:

var uniqueWords = compose2( unique, words );
var uniqueWords_A = compose2( unique_A, words_A );
var uniqueWords_B = compose2( unique_B, words_B );

太清晰瞭!

任意組合

上面,我們組合瞭兩個函數,實際上我們也可以組合 N 個函數;

finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- origValue

比如用一個 compose 函數來實現(敲重點):

function compose(...fns) {
    return function composed(result){
        // 拷貝一份保存函數的數組
        var list = fns.slice();
        while (list.length > 0) {
            // 將最後一個函數從列表尾部拿出
            // 並執行它
            result = list.pop()( result );
        }
        return result;
    };
}
// ES6 箭頭函數形式寫法
var compose =
    (...fns) =>
        result => {
            var list = fns.slice();
            while (list.length > 0) {
                // 將最後一個函數從列表尾部拿出
                // 並執行它
                result = list.pop()( result );
            }
            return result;
        };

基於前面 uniqueWords(..) 的例子,我們進一步再增加一個函數來處理(過濾掉長度小於等於4的字符串):

function skipShortWords(list) {
    var filteredList = [];
    for (let i = 0; i < list.length; i++) {
        if (list[i].length > 4) {
            filteredList.push( list[i] );
        }
    }
    return filteredList;
}
var text = "To compose two functions together";
var biggerWords = compose( skipShortWords, unique, words );
var wordsUsed = biggerWords( text );
wordsUsed;
// ["compose", "functions", "together"]

這樣 compose 函數就有三個入參且都是函數瞭。我們還可以利用偏函數的特性實現更多:

function skipLongWords(list) { /* .. */ }
var filterWords = partialRight( compose, unique, words ); // 固定 unique 函數 和 words 函數
var biggerWords = filterWords( skipShortWords );
var shorterWords = filterWords( skipLongWords );
biggerWords( text );
shorterWords( text );

filterWords 函數是一個更具有特定功能的變體(根據第一個函數的功能來過濾字符串)。

compose 變體

compose(..)函數非常重要,但我們可能不會在生產中使用自己寫的 compose(..),而更傾向於使用某個庫所提供的方案。瞭解其底層工作的原理,對我們強化理解函數式編程也非常有用。

我們理解下 compose(..) 的另一種變體 —— 遞歸的方式實現:

function compose(...fns) {
    // 拿出最後兩個參數
    var [ fn1, fn2, ...rest ] = fns.reverse();
    var composedFn = function composed(...args){
        return fn2( fn1( ...args ) );
    };
    if (rest.length == 0) return composedFn;
    return compose( ...rest.reverse(), composedFn );
}
// ES6 箭頭函數形式寫法
var compose =
    (...fns) => {
        // 拿出最後兩個參數
        var [ fn1, fn2, ...rest ] = fns.reverse();
        var composedFn =
            (...args) =>
                fn2( fn1( ...args ) );
        if (rest.length == 0) return composedFn;
        return compose( ...rest.reverse(), composedFn );
    };

通過遞歸進行重復的動作比在循環中跟蹤運行結果更易懂,這可能需要更多時間去體會;

基於之前的例子,如果我們想讓參數反轉:

var biggerWords = compose( skipShortWords, unique, words );
// 變成
var biggerWords = pipe( words, unique, skipShortWords );

隻需要更改 compose(..) 內部實現這一句就行:

...
        while (list.length > 0) {
            // 從列表中取第一個函數並執行
            result = list.shift()( result );
        }
...

雖然隻是顛倒參數順序,這二者沒有本質上的區別。

抽象能力

你是否會疑問:什麼情況下可以封裝成上述的“盒子”呢?

這就很考驗 —— 抽象的能力瞭!

實際上,有兩個或多個任務存在公共部分,我們就可以進行封裝瞭。

比如:

function saveComment(txt) {
    if (txt != "") {
        comments[comments.length] = txt;
    }
}
function trackEvent(evt) {
    if (evt.name !== undefined) {
        events[evt.name] = evt;
    }
}

就可以抽象封裝為:

function storeData(store,location,value) {
    store[location] = value;
}
function saveComment(txt) {
    if (txt != "") {
        storeData( comments, comments.length, txt );
    }
}
function trackEvent(evt) {
    if (evt.name !== undefined) {
        storeData( events, evt.name, evt );
    }
}

在做這類抽象時,有一個原則是,通常被稱作 DRY(don't repeat yourself),即便我們要花時間做這些非必要的工作。

抽象能讓你的代碼走得更遠! 比如上例,還能進一步升級:

function conditionallyStoreData(store,location,value,checkFn) {
    if (checkFn( value, store, location )) {
        store[location] = value;
    }
}
function notEmpty(val) { return val != ""; }
function isUndefined(val) { return val === undefined; }
function isPropUndefined(val,obj,prop) {
    return isUndefined( obj[prop] );
}
function saveComment(txt) {
    conditionallyStoreData( comments, comments.length, txt, notEmpty );
}
function trackEvent(evt) {
    conditionallyStoreData( events, evt.name, evt, isPropUndefined );
}

這樣 if 語句也被抽象封裝瞭。

抽象是一個過程,程序員將一個名字與潛在的復雜程序片段關聯起來,這樣該名字就能夠被認為代表函數的目的,而不是代表函數如何實現的。通過隱藏無關的細節,抽象降低瞭概念復雜度,讓程序員在任意時間都可以集中註意力在程序內容中的可維護子集上。—— 《程序設計語言》

我們在本系列初始提到:“一切為瞭創造更可讀、更易理解的代碼。”

從另一個角度,抽象就是將命令式代碼變成聲命式代碼的過程。從“怎麼做”轉化成“是什麼”。

命令式代碼主要關心的是描述怎麼做來準確完成一項任務。聲明式代碼則是描述輸出應該是什麼,並將具體實現交給其它部分。

比如 ES6 增加的結構語法:

function getData() {
    return [1,2,3,4,5];
}
// 命令式
var tmp = getData();
var a = tmp[0];
var b = tmp[3];
// 聲明式
var [ a ,,, b ] = getData();

開發者需要對他們程序中每個部分使用恰當的抽象級別保持謹慎,不能太過,也不能不夠。

階段小結

函數組合是為瞭符合“聲明式編程風格”,即關註“是什麼”,而非具體“做什麼”。

它能將一個函數調用的輸出路由跳轉到另一個函數的調用上,然後一直進行下去,它借助 compose(..) 或它的變體實現。。

我們期望組合中的函數是一元的(輸入輸出盡量是一個),這個也是前篇有提到的很重要的一個點。

組合 ———— 聲明式數據流 ———— 是支撐函數式編程其他特性的最重要的工具之一!

以上就是JS輕量級函數式編程實現XDM三的詳細內容,更多關於JS輕量級函數式編程XDM的資料請關註WalkonNet其它相關文章!

推薦閱讀: