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

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

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

JavaScript 輕量級函數式編程

附錄 A:Transducing(下)

組合柯里化

這一步是最棘手的。因此請慢慢的用心的閱讀。java

讓咱們看看沒有將 listCombination(..) 傳遞給柯里化函數的樣子:git

var x = curriedMapReducer( strUppercase );
var y = curriedFilterReducer( isLongEnough );
var z = curriedFilterReducer( isShortEnough );

看看這三個中間函數 x(..)y(..)z(..)。每一個函數都指望獲得一個單一的組合函數併產生一個 reducer 函數。github

記住,若是咱們想要全部這些的獨立的 reducer,咱們能夠這樣作:編程

var upperReducer = x( listCombination );
var longEnoughReducer = y( listCombination );
var shortEnoughReducer = z( listCombination );

可是,若是你調用 y(z),會獲得什麼呢?當把 z 傳遞給 y(..) 調用,而不是 combinationFn(..) 時會發生什麼呢?這個返回的 reducer 函數內部看起來像這樣:小程序

function reducer(list,val) {
    if (isLongEnough( val )) return z( list, val );
    return list;
}

看到 z(..) 裏面的調用了嗎? 這看起來應該是錯誤的,由於 z(..) 函數應該只接收一個參數(combinationFn(..)),而不是兩個參數(list 和 val)。這和要求不匹配。不行。微信小程序

咱們來看看組合 y(z(listCombination))。咱們將把它分紅兩個不一樣的步驟:數組

var shortEnoughReducer = z( listCombination );
var longAndShortEnoughReducer = y( shortEnoughReducer );

咱們建立 shortEnoughReducer(..),而後將它做爲 combinationFn(..) 傳遞給 y(..),生成 longAndShortEnoughReducer(..)。多讀幾遍,直到理解。微信

如今想一想: shortEnoughReducer(..)longAndShortEnoughReducer(..) 的內部構造是什麼樣的呢?你能想獲得嗎?

// shortEnoughReducer, from z(..):
function reducer(list,val) {
    if (isShortEnough( val )) return listCombination( list, val );
    return list;
}

// longAndShortEnoughReducer, from y(..):
function reducer(list,val) {
    if (isLongEnough( val )) return shortEnoughReducer( list, val );
    return list;
}

你看到 shortEnoughReducer(..) 替代了 longAndShortEnoughReducer(..) 裏面 listCombination(..) 的位置了嗎? 爲何這樣也能運行?

由於 reducer(..) 的「形狀」和 listCombination(..) 的形狀是同樣的。 換句話說,reducer 能夠用做另外一個 reducer 的組合函數; 它們就是這樣組合起來的! listCombination(..) 函數做爲第一個 reducer 的組合函數,這個 reducer 又能夠做爲組合函數給下一個 reducer,以此類推。

咱們用幾個不一樣的值來測試咱們的 longAndShortEnoughReducer(..)

longAndShortEnoughReducer( [], "nope" );
// []

longAndShortEnoughReducer( [], "hello" );
// ["hello"]

longAndShortEnoughReducer( [], "hello world" );
// []

longAndShortEnoughReducer(..) 會過濾出不夠長且不夠短的值,它在同一步驟中執行這兩個過濾。這是一個組合 reducer!

再花點時間消化下。

如今,把 x(..) (生成大寫 reducer 的產生器)加入組合:

var longAndShortEnoughReducer = y( z( listCombination) );
var upperLongAndShortEnoughReducer = x( longAndShortEnoughReducer );

正如 upperLongAndShortEnoughReducer(..) 名字所示,它同時執行全部三個步驟 - 一個映射和兩個過濾器!它內部看起來是這樣的:

// upperLongAndShortEnoughReducer:
function reducer(list,val) {
    return longAndShortEnoughReducer( list, strUppercase( val ) );
}

一個字符串類型的 val 被傳入,由 strUppercase(..) 轉換成大寫,而後傳遞給 longAndShortEnoughReducer(..)。該函數只有在 val 知足足夠長且足夠短的條件時纔將它添加到數組中。不然數組保持不變。

我花了幾個星期來思考分析這種雜耍似的操做。因此彆着急,若是你須要在這好好研究下,從新閱讀個幾(十幾個)次。慢慢來。

如今來驗證一下:

upperLongAndShortEnoughReducer( [], "nope" );
// []

upperLongAndShortEnoughReducer( [], "hello" );
// ["HELLO"]

upperLongAndShortEnoughReducer( [], "hello world" );
// []

這個 reducer 成功的組合了和 map 和兩個 filter,太棒了!

讓咱們回顧一下咱們到目前爲止所作的事情:

var x = curriedMapReducer( strUppercase );
var y = curriedFilterReducer( isLongEnough );
var z = curriedFilterReducer( isShortEnough );

var upperLongAndShortEnoughReducer = x( y( z( listCombination ) ) );

words.reduce( upperLongAndShortEnoughReducer, [] );
// ["WRITTEN","SOMETHING"]

這已經很酷了,可是咱們可讓它更好。

x(y(z( .. ))) 是一個組合。咱們能夠直接跳過中間的 x / y / z 變量名,直接這麼表示該組合:

var composition = compose(
    curriedMapReducer( strUppercase ),
    curriedFilterReducer( isLongEnough ),
    curriedFilterReducer( isShortEnough )
);

var upperLongAndShortEnoughReducer = composition( listCombination );

words.reduce( upperLongAndShortEnoughReducer, [] );
// ["WRITTEN","SOMETHING"]

咱們來考慮下該組合函數中「數據」的流動:

  1. listCombination(..) 做爲組合函數傳入,構造 isShortEnough(..) 過濾器的 reducer。

  2. 而後,所獲得的 reducer 函數做爲組合函數傳入,繼續構造 isShortEnough(..) 過濾器的 reducer。

  3. 最後,所獲得的 reducer 函數做爲組合函數傳入,構造 strUppercase(..) 映射的 reducer。

在前面的片斷中,composition(..) 是一個組合函數,指望組合函數來造成一個 reducer;而這個 composition(..) 有一個特殊的標籤:transducer。給 transducer 提供組合函數產生組合的 reducer:

// TODO:檢查 transducer 是產生 reducer 仍是它自己就是 reducer

var transducer = compose(
    curriedMapReducer( strUppercase ),
    curriedFilterReducer( isLongEnough ),
    curriedFilterReducer( isShortEnough )
);

words
.reduce( transducer( listCombination ), [] );
// ["WRITTEN","SOMETHING"]

注意:咱們應該好好觀察下前面兩個片斷中的 compose(..) 順序,這地方有點難理解。回想一下,在咱們的原始示例中,咱們先 map(strUppercase) 而後 filter(isLongEnough) ,最後 filter(isShortEnough);這些操做實際上也確實按照這個順序執行的。但在第 4 章中,咱們瞭解到,compose(..) 一般是以相反的順序運行。那麼爲何咱們不須要反轉這裏的順序來得到一樣的指望結果呢?來自每一個 reducer 的 combinationFn(..) 的抽象反轉了操做順序。因此和直覺相反,當組合一個 tranducer 時,你只須要按照實際的順序組合就好!

列表組合:純與不純

咱們再來看一下咱們的 listCombination(..) 組合函數的實現:

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

雖然這種方法是純的,但它對性能有負面影響。首先,它建立臨時數組來包裹 val。而後,concat(..) 方法建立一個全新的數組來鏈接這個臨時數組。每一步都會建立和銷燬的不少數組,這不只對 CPU 不利,也會形成 GC 內存的流失。

下面是性能更好可是不純的版本:

function listCombination(list,val) {
    list.push( val );
    return list;
}

單獨的考慮下 listCombination(..) ,毫無疑問,這是不純的,這一般是咱們想要避免的。可是,咱們應該考慮一個更大的背景。

listCombination(..) 不是咱們徹底有交互的函數。咱們不直接在程序中的任何地方使用它,而只是在 transducing 的過程當中使用它。

回到第 5 章,咱們定義純函數來減小反作用的目標只是限制在應用的 API 層級。對於底層實現,只要沒有違反對外部是純函數,就能夠在函數內爲了性能而變得不純。

listCombination(..) 更多的是轉換的內部實現細節。實際上,它一般由 transducing 庫提供!而不是你的程序中進行交互的頂層方法。

底線:我認爲甚至使用 listCombination(..) 的性能最優可是不純的版本也是徹底能夠接受的。只要確保你用代碼註釋記錄下它不純便可!

可選的組合

到目前爲止,這是咱們用轉換所獲得的:

words
.reduce( transducer( listCombination ), [] )
.reduce( strConcat, "" );
// 寫點什麼

這已經很是棒了,可是咱們還藏着最後一個的技巧。坦白來講,我認爲這部分可以讓你迄今爲止付出的全部努力變得值得。

咱們能夠用某種方式實現只用一個 reduce(..) 來「組合」這兩個 reduce(..) 嗎? 不幸的是,咱們並不能將 strConcat(..) 添加到 compose(..) 調用中; 它的「形狀」不適用於那個組合。

可是讓咱們來看下這兩個功能:

function strConcat(str1,str2) { return str1 + str2; }

function listCombination(list,val) { list.push( val ); return list; }

若是你用心觀察,能夠看出這兩個功能是如何互換的。它們以不一樣的數據類型運行,但在概念上它們也是同樣的:將兩個值組合成一個。

換句話說, strConcat(..) 是一個組合函數!

這意味着若是咱們的最終目標是得到字符串鏈接而不是數組,咱們就能夠用它代替 listCombination(..)

words.reduce( transducer( strConcat ), "" );
// 寫點什麼

Boom! 這就是 transducing。

最後

深吸一口氣,確實有不少要消化。

放空咱們的大腦,讓咱們把注意力轉移到如何在咱們的程序中使用轉換,而不是關心它的工做原理。

回想起咱們以前定義的輔助函數,爲清楚起見,咱們從新命名一下:

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

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

還記得咱們這樣使用它們:

var transducer = compose(
    transduceMap( strUppercase ),
    transduceFilter( isLongEnough ),
    transduceFilter( isShortEnough )
);

transducer(..) 仍然須要一個組合函數(如 listCombination(..)strConcat(..))來產生一個傳遞給 reduce(..) (連同初始值)的 transduce-reducer 函數。

可是爲了更好的表達全部這些轉換步驟,咱們來作一個 transduce(..) 工具來爲咱們作這些步驟:

function transduce(transducer,combinationFn,initialValue,list) {
    var reducer = transducer( combinationFn );
    return list.reduce( reducer, initialValue );
}

這是咱們的運行示例,梳理以下:

var transducer = compose(
    transduceMap( strUppercase ),
    transduceFilter( isLongEnough ),
    transduceFilter( isShortEnough )
);

transduce( transducer, listCombination, [], words );
// ["WRITTEN","SOMETHING"]

transduce( transducer, strConcat, "", words );
// 寫點什麼

不錯,嗯! 看到 listCombination(..)strConcat(..) 函數能夠互換使用組合函數了嗎?

Transducers.js

最後,咱們來講明咱們運行的例子,使用sensors-js庫(https://github.com/cognitect-labs/transducers-js ):

var transformer = transducers.comp(
    transducers.map( strUppercase ),
    transducers.filter( isLongEnough ),
    transducers.filter( isShortEnough )
);

transducers.transduce( transformer, listCombination, [], words );
// ["WRITTEN","SOMETHING"]

transducers.transduce( transformer, strConcat, "", words );
// WRITTENSOMETHING

看起來幾乎與上述相同。

注意: 上面的代碼段使用 transformers.comp(..) ,由於這個庫提供這個 API,但在這種狀況下,咱們從第 4 章的 compose(..) 也將產生相同的結果。換句話說,組合自己不是 transducing 敏感的操做。

該片斷中的組合函數被稱爲 transformer ,而不是 transducer。那是由於若是咱們直接調用 transformer(listCombination)(或 transformer(strConcat)),那麼咱們不會像之前那樣獲得一個直觀的 transduce-reducer 函數。

transducers.map(..)transducers.filter(..) 是特殊的輔助函數,能夠將常規的斷言函數或映射函數轉換成適用於產生特殊變換對象的函數(裏面包含了 reducer 函數);這個庫使用這些變換對象進行轉換。此轉換對象抽象的額外功能超出了咱們將要探索的內容,請參閱該庫的文檔以獲取更多信息。

因爲 transformer(..) 產生一個變換對象,而不是一個典型的二元 transduce-reducer 函數,該庫還提供 toFn(..) 來使變換對象適應本地數組的 reduce(..) 方法:

words.reduce(
    transducers.toFn( transformer, strConcat ),
    ""
);
// WRITTENSOMETHING

into(..) 是另外一個提供的輔助函數,它根據指定的空/初始值的類型自動選擇默認的組合函數:

transducers.into( [], transformer, words );
// ["WRITTEN","SOMETHING"]

transducers.into( "", transformer, words );
// WRITTENSOMETHING

當指定一個空數組 [] 時,內部的 transduce(..) 使用一個默認的函數實現,這個函數就像咱們的 listCombination(..)。可是當指定一個空字符串 「」 時,會使用像咱們的 strConcat(..) 這樣的方法。這很酷!

如你所見,transducers-js 庫使轉換很是簡單。咱們能夠很是有效地利用這種技術的力量,而不至於陷入定義全部這些中間轉換器生產工具的繁瑣過程當中去。

總結

Transduce 就是經過減小來轉換。更具體點,transduer 是可組合的 reducer。

咱們使用轉換來組合相鄰的map(..)filter(..)reduce(..) 操做。咱們首先將 map(..)filter(..) 表示爲 reduce(..),而後抽象出經常使用的組合操做來建立一個容易組合的一致的 reducer 生成函數。

transducing 主要提升性能,若是在延遲序列(異步 observables)中使用,則這一點尤其明顯。

可是更普遍地說,transducing 是咱們針對那些不能被直接組合的函數,使用的一種更具聲明式風格的方法。不然這些函數將不能直接組合。若是使用這個技術能像使用本書中的全部其餘技術同樣用的恰到好處,代碼就會顯得更清晰,更易讀! 使用 transducer 進行單次 reduce(..) 調用比追蹤多個 reduce(..) 調用更容易理解。

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

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

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

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