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

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

產生 斷言 需要 pipe [] 個數 word 視頻 block

  • 原文地址:Functional-Light-JS
  • 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者

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

譯者團隊(排名不分先後):阿希、blueken、brucecham、cfanlife、dail、kyoko-df、l3ve、lilins、LittlePineapple、MatildaJin、冬青、pobusama、Cherry、蘿蔔、vavd317、vivaxy、萌萌、zhouyao

JavaScript 輕量級函數式編程

附錄 A:Transducing

Transducing 是我們這本書要講到的更為高級的技術。它繼承了第 8 章數組操作的許多思想。

我不會把 Transducing 嚴格的稱為“輕量級函數式編程”,它更像是一個頂級的技巧。我把這個技術留到附錄來講意味著你現在很可能並不需要關心它,當你確保你已經非常熟悉整本書的主要內容,你可以再回頭看看這一章節。

說實話,即使我已經教過 transducing 很多次了,在寫這一章的時候,我仍然需要花很多腦力去理清楚這個技術。所以,如果你看這一章看的很疑惑也沒必要感到沮喪。把這一章加個書簽,等你覺得你差不多能理解時再回頭看看。

Transducing 就是通過減少來轉換。

我知道這聽起來很令人費解。但是讓我們來看看它有多強大。實際上,我認為這是你掌握了輕量級函數式編程後可以做的最好的例證之一。

和這本書的其他部分一樣,我的方法是先解釋為什麽使用這個技術,然後如何使用,最後歸結為簡單的這個技術到底是什麽樣的。這通常會有多學很多東西,但是我覺得用這種方式你會更深入的理解它。

首先,為什麽

讓我們從擴展我們在第 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》姊妹篇 **

技術分享圖片

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

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

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