书籍文库  |  文档资料  |  最近更新  |  MAP  |  TAG  | 
注册
手机版
就爱阅读网
当前位置:首页 > 时尚爱美 > 穿衣搭配 > 13翻譯連載 | 附錄 A:Transducing(下)-《JavaScript輕量級函數式編程》 |《你不知道的JS》姊妹篇

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

分享人:玉蝴蝶 来源:互联网 时间:2017-12-06 阅读:0

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

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

JavaScript 輕量級函數式編程

附錄 A:Transducing(下)

組合柯裡化

這一步是最棘手的。所以請慢慢的用心的閱讀。

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

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

看看這三個中間函數 x(..)y(..)z(..)。每個函數都期望得到一個單一的組合函數並產生一個 reducer 函數。

記住,如果我們想要所有這些的獨立的 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-... ):

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實戰項目教程》
包含:文章、視頻、源代碼


百度搜索“就爱阅读”,专业资料,生活学习,尽在就爱阅读网92to.com,您的在线图书馆!

热点阅读

网友最爱