翻譯連載 | 附錄 A:Transducing(上)-《JavaScript輕量級函數式編程》 |《你不知道的JS》姊妹篇

關於譯者:這是一個流淌着滬江血液的純粹工程:認真,是 HTML 最堅實的樑柱;分享,是 CSS 裏最閃耀的一瞥;總結,是 JavaScript 中最嚴謹的邏輯。通過捶打磨練,成就了本書的中文版。本書包含了函數式編程之精髓,但願能夠幫助你們在學習函數式編程的道路上走的更順暢。比心。github

譯者團隊(排名不分前後):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry蘿蔔vavd317vivaxy萌萌zhouyao編程

JavaScript 輕量級函數式編程

附錄 A:Transducing

Transducing 是咱們這本書要講到的更爲高級的技術。它繼承了第 8 章數組操做的許多思想。小程序

我不會把 Transducing 嚴格的稱爲「輕量級函數式編程」,它更像是一個頂級的技巧。我把這個技術留到附錄來說意味着你如今極可能並不須要關心它,當你確保你已經很是熟悉整本書的主要內容,你能夠再回頭看看這一章節。微信小程序

說實話,即便我已經教過 transducing 不少次了,在寫這一章的時候,我仍然須要花不少腦力去理清楚這個技術。因此,若是你看這一章看的很疑惑也不必感到沮喪。把這一章加個書籤,等你以爲你差很少能理解時再回頭看看。數組

Transducing 就是經過減小來轉換。微信

我知道這聽起來很使人費解。可是讓咱們來看看它有多強大。實際上,我認爲這是你掌握了輕量級函數式編程後能夠作的最好的例證之一。app

和這本書的其餘部分同樣,個人方法是先解釋爲何使用這個技術,而後如何使用,最後歸結爲簡單的這個技術究竟是什麼樣的。這一般會有多學不少東西,可是我以爲用這種方式你會更深刻的理解它。框架

首先,爲何

讓咱們從擴展咱們在第 3 章中介紹的例子開始,測試單詞是否足夠短和/或足夠長:

function isLongEnough(str) {
    return str.length >= 5;
}

function isShortEnough(str) {
    return str.length <= 10;
}

在第 3 章中,咱們使用這些斷言函數來測試一個單詞。而後在第 8 章中,咱們學習瞭如何使用像 filter(..) 這樣的數組操做來重複這些測試。例如:

var words = [ "You", "have", "written", "something", "very", "interesting" ];

words
.filter( isLongEnough )
.filter( isShortEnough );
// ["written","something"]

這個例子可能並不明顯,可是這種分開操做相同數組的方式具備一些不理想的地方。當咱們處理一個值比較少的數組時一切都還好。可是若是數組中有不少值,每一個 filter(..) 分別處理數組的每一個值會比咱們預期的慢一點。

當咱們的數組是異步/懶惰(也稱爲 observables)的,隨着時間的推移響應事件處理(見第 10 章),會出現相似的性能問題。在這種狀況下,一次事件只有一個值,所以使用兩個單獨的 filter(..) 函數處理這些值並非什麼大不了的事情。

可是,不太明顯的是每一個 filter(..) 方法都會產生一個單獨的 observable 值。從一個 observable 值中抽出一個值的開銷真的能夠加起來(譯者注:詳情請看第 10 章的「積極的 vs 惰性的」這一節)。這是真實存在的,由於在這些狀況下,處理數千或數百萬的值並不罕見; 因此,即便是這麼小的成本也會很快累加起來。

另外一個缺點是可讀性,特別是當咱們須要對多個數組(或 observable)重複相同的操做時。例如:

zip(
    list1.filter( isLongEnough ).filter( isShortEnough ),
    list2.filter( isLongEnough ).filter( isShortEnough ),
    list3.filter( isLongEnough ).filter( isShortEnough )
)

顯得很重複,對不對?

若是咱們能夠將 isLongEnough(..) 斷言與 isShortEnough(..) 斷言組合在一塊兒是否是會更好一點呢(可讀性和性能)?你能夠手動執行:

function isCorrectLength(str) {
    return isLongEnough( str ) && isShortEnough( str );
}

但這不是函數式編程的方式!

在第 8 章中,咱們討論了融合 —— 組合相鄰映射函數。回憶一下:

words
.map(
    pipe( removeInvalidChars, upper, elide )
);

不幸的是,組合相鄰斷言函數並不像組合相鄰映射函數那樣容易。爲何呢?想一想斷言函數長什麼「樣子」 —— 一種描述輸入和輸出的學術方式。它接收一個單一的參數,返回一個 true 或 false。

若是你試着用 isshortenough(islongenough(str)),這是行不通的。由於 islongenough(..) 會返回 true 或者 false ,而不是返回 isshortenough(..) 所要的字符串類型的值。這可真倒黴。

試圖組合兩個相鄰的 reducer 函數一樣是行不通的。reducer 函數接收兩個值做爲輸入,並返回單個組合值。reducer 函數的單一返回值也不能做爲參數傳到另外一個須要兩個輸入的 reducer 函數中。

此外,reduce(..) 輔助函數能夠接收一個可選的 initialValue 輸入。有時能夠省略,但有時候它又必須被傳入。這就讓組合更復雜了,由於一個 reduce(..) 可能須要一個 initialValue,而另外一個 reduce(..) 可能須要另外一個 initialValue。因此咱們怎麼可能只用某種組合的 reducer 來實現 reduce(..) 呢。

考慮像這樣的鏈:

words
.map( strUppercase )
.filter( isLongEnough )
.filter( isShortEnough )
.reduce( strConcat, "" );
// "WRITTENSOMETHING"

你能想出一個組合可以包含 map(strUppercase)filter(isLongEnough)filter(isShortEnough)reduce(strConcat) 全部這些操做嗎?每種操做的行爲是不一樣的,因此不能直接組合在一塊兒。咱們須要把它們修改下讓它們組合在一塊兒。

但願這些例子說明了爲何簡單的組合不能勝任這項任務。咱們須要一個更強大的技術,而 transducing 就是這個技術。

如何,下一步

讓咱們談談咱們該如何獲得一個能組合映射,斷言和/或 reducers 的框架。

別太緊張:你沒必要經歷編程過程當中全部的探索步驟。一旦你理解了 transducing 能解決的問題,你就能夠直接使用函數式編程庫中的 transduce(..) 工具繼續你應用程序的剩餘部分!

讓咱們開始探索吧。

把 Map/Filter 表示爲 Reduce

咱們要作的第一件事情就是將咱們的 filter(..)map(..)調用變爲 reduce(..) 調用。回想一下咱們在第 8 章是怎麼作的:

function strUppercase(str) { return str.toUpperCase(); }
function strConcat(str1,str2) { return str1 + str2; }

function strUppercaseReducer(list,str) {
    list.push( strUppercase( str ) );
    return list;
}

function isLongEnoughReducer(list,str) {
    if (isLongEnough( str )) list.push( str );
    return list;
}

function isShortEnoughReducer(list,str) {
    if (isShortEnough( str )) list.push( str );
    return list;
}

words
.reduce( strUppercaseReducer, [] )
.reduce( isLongEnoughReducer, [] )
.reduce( isShortEnough, [] )
.reduce( strConcat, "" );
// "WRITTENSOMETHING"

這是一個不錯的改進。咱們如今有四個相鄰的 reduce(..) 調用,而不是三種不一樣方法的混合。然而,咱們仍然不能 compose(..) 這四個 reducer,由於它們接受兩個參數而不是一個參數。

在 8 章,咱們偷了點懶使用了數組的 push 方法而不是 concat(..) 方法返回一個新數組,致使有反作用。如今讓咱們更正式一點:

function strUppercaseReducer(list,str) {
    return list.concat( [strUppercase( str )] );
}

function isLongEnoughReducer(list,str) {
    if (isLongEnough( str )) return list.concat( [str] );
    return list;
}

function isShortEnoughReducer(list,str) {
    if (isShortEnough( str )) return list.concat( [str] );
    return list;
}

在後面咱們會來頭看看這裏是否須要 concat(..)

參數化 Reducers

除了使用不一樣的斷言函數以外,兩個 filter reducers 幾乎相同。讓咱們把這些 reducers 參數化獲得一個能夠定義任何 filter-reducer 的工具函數:

function filterReducer(predicateFn) {
    return function reducer(list,val){
        if (predicateFn( val )) return list.concat( [val] );
        return list;
    };
}

var isLongEnoughReducer = filterReducer( isLongEnough );
var isShortEnoughReducer = filterReducer( isShortEnough );

一樣的,咱們把 mapperFn(..) 也參數化來生成 map-reducer 函數:

function mapReducer(mapperFn) {
    return function reducer(list,val){
        return list.concat( [mapperFn( val )] );
    };
}

var strToUppercaseReducer = mapReducer( strUppercase );

咱們的調用鏈看起來是同樣的:

words
.reduce( strUppercaseReducer, [] )
.reduce( isLongEnoughReducer, [] )
.reduce( isShortEnough, [] )
.reduce( strConcat, "" );

提取共用組合邏輯

仔細觀察上面的 mapReducer(..)filterReducer(..) 函數。你發現共享功能了嗎?

這部分:

return list.concat( .. );

// 或者
return list;

讓咱們爲這個通用邏輯定義一個輔助函數。可是咱們叫它什麼呢?

function WHATSITCALLED(list,val) {
    return list.concat( [val] );
}

WHATSITCALLED(..) 函數作了些什麼呢,它接收兩個參數(一個數組和另外一個值),將值 concat 到數組的末尾返回一個新的數組。因此這個 WHATSITCALLED(..) 名字不合適,咱們能夠叫它 listCombination(..)

function listCombination(list,val) {
    return list.concat( [val] );
}

咱們如今用 listCombination(..) 來從新定義咱們的 reducer 輔助函數:

function mapReducer(mapperFn) {
    return function reducer(list,val){
        return listCombination( list, mapperFn( val ) );
    };
}

function filterReducer(predicateFn) {
    return function reducer(list,val){
        if (predicateFn( val )) return listCombination( list, val );
        return list;
    };
}

咱們的調用鏈看起來仍是同樣的(這裏就不重複寫了)。

參數化組合

咱們的 listCombination(..) 小工具只是組合兩個值的一種方式。讓咱們將它的用途參數化,以使咱們的 reducers 更加通用:

function mapReducer(mapperFn,combinationFn) {
    return function reducer(list,val){
        return combinationFn( list, mapperFn( val ) );
    };
}

function filterReducer(predicateFn,combinationFn) {
    return function reducer(list,val){
        if (predicateFn( val )) return combinationFn( list, val );
        return list;
    };
}

使用這種形式的輔助函數:

var strToUppercaseReducer = mapReducer( strUppercase, listCombination );
var isLongEnoughReducer = filterReducer( isLongEnough, listCombination );
var isShortEnoughReducer = filterReducer( isShortEnough, listCombination );

將這些實用函數定義爲接收兩個參數而不是一個參數不太方便組合,所以咱們使用咱們的 curry(..) (柯里化)方法:

var curriedMapReducer = curry( function mapReducer(mapperFn,combinationFn){
    return function reducer(list,val){
        return combinationFn( list, mapperFn( val ) );
    };
} );

var curriedFilterReducer = curry( function filterReducer(predicateFn,combinationFn){
    return function reducer(list,val){
        if (predicateFn( val )) return combinationFn( list, val );
        return list;
    };
} );

var strToUppercaseReducer =
    curriedMapReducer( strUppercase )( listCombination );
var isLongEnoughReducer =
    curriedFilterReducer( isLongEnough )( listCombination );
var isShortEnoughReducer =
    curriedFilterReducer( isShortEnough )( listCombination );

這看起來有點冗長並且可能不是頗有用。

但這其實是咱們進行下一步推導的必要條件。請記住,咱們的最終目標是可以 compose(..) 這些 reducers。咱們快要完成了。

 附錄 A:Transducing(下)---- 四天後更新

** 【上一章】[翻譯連載 | 第 11 章:融會貫通 -《JavaScript輕量級函數式編程》 |《你不知道的JS》姊妹篇
](https://juejin.im/post/5a0cf1... **

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

iKcamp官網:https://www.ikcamp.com訪問官網更快閱讀所有免費分享課程:《iKcamp出品|全網最新|微信小程序|基於最新版1.0開發者工具之初中級培訓教程分享》《iKcamp出品|基於Koa2搭建Node.js實戰項目教程》包含:文章、視頻、源代碼

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息