輕量函數(shù)式 JavaScript 附錄 A:Transducing

感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券劫谅,享受所有官網(wǎng)優(yōu)惠祖凫,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取

與我們?cè)诒緯?shū)中所講解的內(nèi)容相比寝杖,Transducing 是一種更高級(jí)的技術(shù)型豁。它擴(kuò)展了第八章中的列表操作的許多概念僵蛛。

我不認(rèn)為這個(gè)話題是嚴(yán)格的 “輕量函數(shù)式”尚蝌,它更像是在此之上的額外獎(jiǎng)勵(lì)。我將它留作附錄是因?yàn)槟愫芸赡苄枰獣呵姨^(guò)關(guān)于它的討論充尉,而在你對(duì)本書(shū)正文中的概念感到相當(dāng)適應(yīng) —— 并且確實(shí)經(jīng)過(guò)實(shí)踐飘言! —— 之后再回到這里。

老實(shí)說(shuō)驼侠,即便是教授了 transducing 許多次姿鸿,而且編寫(xiě)了這一章之后,我依然在努力地嘗試使用這種技術(shù)來(lái)武裝自己的頭腦泪电。所以般妙,如果它讓你感到很繞也不要灰心纪铺。給這一章夾上一個(gè)書(shū)簽相速,當(dāng)你準(zhǔn)備好了之后再回來(lái)。

Transducing 意味著帶有變形(transforming)的遞減(reduction)鲜锚。

我知道突诬,它聽(tīng)起來(lái)就像一個(gè)雜亂的詞匯 —— 它使人糊涂的地方要比它澄清的東西多。但還是讓我們看看它能有多么強(qiáng)大芜繁。實(shí)際上旺隙,我認(rèn)為一旦你掌握了輕量函數(shù)式編程的原理,它就是你能力的最佳展示骏令。

正如本書(shū)的其他部分一樣蔬捷,我的方式是首先解釋 為什么,然后在講解 如何做榔袋,最后歸結(jié)為一種簡(jiǎn)化的周拐,可重用的 什么。這通常與其他許多人的教授方法相反凰兑,但我認(rèn)為這種方式可以使你更深入地學(xué)習(xí)妥粟。

首先,為什么

讓我們從擴(kuò)展第三章中的一個(gè)場(chǎng)景開(kāi)始吏够,測(cè)試一個(gè)單詞勾给,看它是否足夠短并/或足夠長(zhǎng):

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

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

在第三章中,我們使用了這些判定函數(shù)來(lái)測(cè)試一個(gè)單詞锅知。然后在第八章中播急,我們學(xué)習(xí)了如何使用 filter(..) 之類的列表操作重復(fù)這樣的測(cè)試。例如:

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

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

這可能不太明顯售睹,不過(guò)這種分離且相鄰的列表操作模式有些不盡人意的性質(zhì)旅择。當(dāng)我們處理僅擁有為數(shù)不多的值的單一數(shù)組是,一切都很正常侣姆。但如果數(shù)組中有許多值的時(shí)候生真,分離地處理每個(gè) filter(..) 可能會(huì)出人意料地降低程序運(yùn)行的速度沉噩。

當(dāng)我們的數(shù)組是異步/懶惰(也稱為 observable)的、在對(duì)事件作出相應(yīng)而跨時(shí)段處理值(見(jiàn)第十章)的時(shí)候也會(huì)出現(xiàn)同樣的性能問(wèn)題柱蟀。在這種場(chǎng)景下川蒙,事件流中每次只會(huì)有一個(gè)值被傳遞出來(lái),所以使用兩個(gè)分離的 filter(..) 函數(shù)處理這些離散的值也不是什么大問(wèn)題长已。

但微妙的是畜眨,每個(gè) filter(..) 方法都生成一個(gè)分離的 observable。將一個(gè)值從一個(gè) observable 中傳遞到另一個(gè) observable 的開(kāi)銷(xiāo)可能累積起來(lái)术瓮。特別是在這些情況下成千或上百萬(wàn)的值需要被處理并非不尋常的事康聂;即便是如此之小的開(kāi)銷(xiāo)也會(huì)很快地累積起來(lái)。

另一個(gè)缺陷是可讀性胞四,特別是當(dāng)我們需要對(duì)多個(gè)列表(或者 observable)重復(fù)這一系列操作的時(shí)候恬汁。例如:

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

很啰嗦,對(duì)吧辜伟?

如果我們能夠?qū)?isLongEnough(..)isShortEnough(..) 判定函數(shù)結(jié)合起來(lái)不是更好嗎(對(duì)于可讀性和性能來(lái)說(shuō))氓侧?你可以手動(dòng)這樣做:

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

但這不是 FP 的方式!

在第八章中导狡,我們談到了熔合 —— 組合相鄰的映射函數(shù)约巷。回想一下:

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

不幸的是旱捧,組合相鄰的判定函數(shù)不像組合相鄰的映射函數(shù)那么簡(jiǎn)單独郎。究其原因,考慮一下判定函數(shù)的 “外形(shape)” —— 某種描述輸入和輸出簽名的學(xué)術(shù)化方式枚赡。它接收一個(gè)單獨(dú)的值氓癌,并返回一個(gè) truefalse

如果你試著使用 isShortEnough(isLongEnough(str))标锄,它是不會(huì)正常工作的顽铸。isLongEnough(..) 將會(huì)返回 true / false,而不是 isShortEnough(..) 所期待的字符串值料皇。煩人谓松。

在組合相鄰的遞減函數(shù)時(shí)也存在相似的惱人之處。遞減函數(shù)的 “外形” 是一個(gè)接收兩個(gè)輸入值的函數(shù)践剂,并返回一個(gè)組合好的值鬼譬。遞減函數(shù)的單值輸出不適于作為另一個(gè)期待兩個(gè)值的遞減函數(shù)的輸入。

另外逊脯,reduce(..) 幫助函數(shù)接收一個(gè)可選的輸入 initialValue优质。有時(shí)它可以被忽略,但有時(shí)不得不被傳入。這使組合變得更復(fù)雜巩螃,因?yàn)橐粋€(gè)遞減操作可能需要一個(gè) initialValue 而另一個(gè)遞減操作可能需要一個(gè)不同的 initialValue演怎。我們?nèi)绾尾拍苁褂媚撤N組合好的遞減函數(shù)來(lái)發(fā)起一個(gè) reduce(..) 調(diào)用呢?

考慮一個(gè)這樣鏈條:

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

你能想想一個(gè)包含所有 map(strUppercase)避乏、filter(isLongEnough)爷耀、filter(isShortEnough)reduce(strConcat) 這些步驟的組合嗎拍皮?每一個(gè)操作函數(shù)的外形都是不同的歹叮,所以它們不能直接組合在一起。我們需要調(diào)整一下它們的外形來(lái)使它們彼此吻合铆帽。

希望這些觀察展示了為什么單純的熔合式組合不能完成這個(gè)任務(wù)咆耿。我們需要更強(qiáng)大的技術(shù),而 transducing 就是工具爹橱。

接下來(lái)萨螺,如何做

讓我們來(lái)談?wù)勅绾尾拍苎苌鲆环N映射函數(shù)、判定函數(shù)和/或遞減函數(shù)的組合宅荤。

不要被沖昏了頭腦:你不必在你自己的程序中把我們將要探索的所有這些思維步驟都走一遍屑迂。一旦你理解并能夠認(rèn)出 trasnducing 解決的問(wèn)題浸策,你就可以直接跳到使用一個(gè) FP 庫(kù)的 transduce(..) 工具冯键,并繼續(xù)處理你程序的其余部分!

讓我們開(kāi)始吧庸汗。

將映射/過(guò)濾表達(dá)為遞減

我們要施展的第一個(gè)技巧是將 filter(..)map(..) 調(diào)用表達(dá)為 reduce(..) 調(diào)用惫确。回憶一下我們?cè)诘诎苏轮惺侨绾巫龅模?/p>

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"

這是個(gè)相當(dāng)好的改進(jìn)蚯舱。我們現(xiàn)在有了四個(gè)相鄰的 reduce(..) 調(diào)用而不是擁有不同外形的三種不同方法的混合改化。但是,我們依然不能簡(jiǎn)單地 compose(..) 這四個(gè)遞減函數(shù)枉昏,因?yàn)樗鼈兘邮諆蓚€(gè)參數(shù)而不是一個(gè)陈肛。

在第八章中,我們作弊并使用了 list.push(..) 作為一種副作用進(jìn)行改變兄裂,而不是調(diào)用 list.concat(..) 來(lái)返回一個(gè)全新的數(shù)組【浜担現(xiàn)在讓我們更正式一些:

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(..) 在這里是否必要晰奖。

將遞減函數(shù)參數(shù)化

兩個(gè)過(guò)濾遞減函數(shù)除了使用一個(gè)不同的判定函數(shù)以外幾乎是相同的谈撒。讓我們將此參數(shù)化,這樣我們就得到一個(gè)可以定義任意過(guò)濾遞-減函數(shù)的工具:

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 );

為了得到一個(gè)能夠生成任意映射-遞減函數(shù)的工具匾南,讓我們對(duì) mapperFn(..) 進(jìn)行相同的參數(shù)化:

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

var strToUppercaseReducer = mapReducer( strUppercase );

我們鏈條看起來(lái)沒(méi)變:

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

抽取共通的組合邏輯

極其仔細(xì)地觀察上面的 mapReducer(..)filterReducer(..) 函數(shù)啃匿。你發(fā)現(xiàn)它們共享的共通功能了嗎?

這一部分:

return list.concat( .. );

// 或者
return list;

讓我們?yōu)檫@個(gè)共同邏輯定義一個(gè)幫助函數(shù)。但我們?nèi)绾畏Q呼它溯乒?

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

檢視一下 WHATSITCALLED(..) 函數(shù)在做什么夹厌,它接收兩個(gè)值(一個(gè)數(shù)組和另一個(gè)值)并通過(guò)將值連接到數(shù)組末尾來(lái)將它們 “組合”,再返回一個(gè)新數(shù)組裆悄。非常沒(méi)有創(chuàng)意尊流,但我們可以將它命名為 listCombination(..)

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

現(xiàn)在讓我們重新定義遞減函數(shù)的幫助函數(shù),來(lái)使用 listCombination(..)

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;
    };
}

我們的鏈條依然沒(méi)變(所以我們不再啰嗦這一點(diǎn))灯帮。

將組合參數(shù)化

我們簡(jiǎn)單的 listCombination(..) 工具只是我們結(jié)合兩個(gè)值的一種可能的方式崖技。讓我們將使用它的過(guò)程參數(shù)化,來(lái)時(shí)我們的遞減函數(shù)更加一般化:

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;
    };
}

要使用這種形式的幫助函數(shù):

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

將這些工具定義為接收兩個(gè)參數(shù)而非一個(gè)對(duì)于組合來(lái)說(shuō)不太方便钟哥,所以讓我們使用我們的 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 );

這看起來(lái)煩冗了一些迎献,而且看起來(lái)可能不是非常有用。

但是為了進(jìn)行到我們衍生物的下一步來(lái)說(shuō)這實(shí)際上是必要的腻贰。記住吁恍,我們這里的終極目標(biāo)是能夠 compose(..) 這些遞減函數(shù),我們就快成功了播演。

組合柯里化后的函數(shù)

這一步是所有思考中最刁鉆的一步冀瓦。所以這里要慢慢讀并集中注意力。

讓我們考慮一下上面柯里化后的函數(shù)写烤,但不帶 listCombination(..) 函數(shù)被傳入的部分:

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

考慮所有這三個(gè)中間函數(shù)的外形翼闽,x(..)y(..)洲炊、和 z(..)感局。每一個(gè)都期待一個(gè)單獨(dú)的組合函數(shù),并為它生成一個(gè)遞減函數(shù)暂衡。

記住询微,如果我們想要得到所有這些函數(shù)的獨(dú)立的遞減函數(shù),我們可以這樣做:

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

但如果調(diào)用了 y(z) 你會(huì)得到什么狂巢?基本上是說(shuō)撑毛,當(dāng)將 z 作為 y(..)combinationFn(..)傳入時(shí)發(fā)生了什么?這會(huì)返回一個(gè)內(nèi)部看起來(lái)像這樣的遞減函數(shù):

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

看到內(nèi)部的 z(..) 調(diào)用了嗎唧领?這在你看來(lái)應(yīng)當(dāng)是錯(cuò)的藻雌,因?yàn)?z(..) 函數(shù)本應(yīng)只接收一個(gè)參數(shù)(一個(gè) combinationFn(..)),不是兩個(gè)(listval)疹吃。外形不匹配蹦疑。這不能工作。

相反讓我們看看組合 y(z(listCombination))萨驶。我們將它分解為兩個(gè)分離的步驟:

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

我們創(chuàng)建了 shortEnoughReducer(..)歉摧,然后我們將它作為 combinationFn(..) 傳遞給 y(..),生成了 longAndShortEnoughReducer(..)。將這一句重讀即便叁温,直到你領(lǐng)悟?yàn)橹埂?/p>

現(xiàn)在考慮一下:shortEnoughReducer(..)longAndShortEnoughReducer(..)內(nèi)部看起來(lái)什么樣再悼?你能在思維中看到它們嗎?

// shortEnoughReducer膝但,來(lái)自 z(..):
function reducer(list,val) {
    if (isShortEnough( val )) return listCombination( list, val );
    return list;
}

// longAndShortEnoughReducer冲九,來(lái)自 y(..):
function reducer(list,val) {
    if (isLongEnough( val )) return shortEnoughReducer( list, val );
    return list;
}

你看到 shortEnoughReducer(..) 是如何在 longAndShortEnoughReducer(..) 內(nèi)部取代了 listCombination(..) 嗎?為什么這個(gè)好用跟束?

因?yàn)?一個(gè) reducer(..) 的外形和 listCombination(..) 的外形是相同的莺奸。 換言之,一個(gè)遞減函數(shù)可以被用作另一個(gè)遞減函數(shù)的組合函數(shù)冀宴;這就是它們?nèi)绾谓M合的灭贷!listCombination(..) 函數(shù)制造了第一個(gè)遞減函數(shù),然后這個(gè)遞減函數(shù)可以作為組合函數(shù)來(lái)制造下一個(gè)遞減函數(shù)略贮,以此類推甚疟。

讓我們使用幾個(gè)不同的值來(lái)測(cè)試一下我們的 longAndShortEnoughReducer(..)

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

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

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

longAndShortEnoughReducer(..) 工具濾除了既不夠長(zhǎng)也不夠短的值,而且它是在同一個(gè)步驟中做了這兩個(gè)過(guò)濾的逃延。它是一個(gè)組合的遞減函數(shù)览妖!

再花點(diǎn)兒時(shí)間讓它消化吸收。它還是有些讓我混亂揽祥。

現(xiàn)在讽膏,把 x(..) (大寫(xiě)遞減函數(shù)生成器)代入組合之中:

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

正如 upperLongAndShortEnoughReducer(..) 這個(gè)名字所暗示的,它一次完成所有三個(gè)步驟 —— 一個(gè)映射和兩個(gè)過(guò)濾盔然!它內(nèi)部看起來(lái)就像這樣:

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

一個(gè)字符串 val 被傳入桅打,由 strUppercase(..) 改為大寫(xiě)是嗜,然后被傳遞給 longAndShortEnoughReducer(..)愈案。這個(gè)函數(shù)僅條件性地 —— 如果這個(gè)字符串長(zhǎng)短合適 —— 將這個(gè)大寫(xiě)字符串添加到 list,否則 list 保持不變鹅搪。

我的大腦花了好幾周才完全理解了這套雜耍的含義站绪。所以如果你需要在這里停下并重讀幾遍(幾十遍!)來(lái)搞明白它也不要擔(dān)心丽柿。慢慢來(lái)恢准。

現(xiàn)在我們驗(yàn)證一下:

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

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

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

這個(gè)遞減函數(shù)是一個(gè)映射函數(shù)和兩個(gè)過(guò)濾函數(shù)的組合!這真令人吃驚甫题!

概括一下我們目前身在何處:

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( .. ))) 是一個(gè)組合坠非。讓我們跳過(guò)中間的變量名 x / y / z敏沉,直接表達(dá)這個(gè)組合:

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

var upperLongAndShortEnoughReducer = composition( listCombination );

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

考慮一下這個(gè)組合函數(shù)的 “數(shù)據(jù)” 流:

  1. listCombination(..) 作為組合函數(shù)流入 isShortEnough(..),為它制造了過(guò)濾-遞減函數(shù)。
  2. 然后這個(gè)結(jié)果遞減函數(shù)作為組合函數(shù)流入 isLongEnough(..)盟迟,為它制造了過(guò)濾-遞減函數(shù)秋泳。
  3. 最后,這個(gè)結(jié)果遞減函數(shù)作為組合函數(shù)流入 strUppercase(..)攒菠,為它制造了映射-遞減函數(shù)迫皱。

在前一個(gè)代碼段中,composition(..) 是一個(gè)組合好的函數(shù)辖众,它期待一個(gè)組合函數(shù)來(lái)制造一個(gè)遞減函數(shù)卓起;composition(..) 有一個(gè)特殊的標(biāo)簽:transducer。向一個(gè) transducer 提供組合函數(shù)就生成了組合好的遞減函數(shù):

// TODO: fact-check if the transducer produces the reducer or is the reducer

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

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

注意: 我們應(yīng)當(dāng)關(guān)注一下前兩個(gè)代碼段中的 compose(..) 順序凹炸,它可能有些令人糊涂既绩。回憶一下我們?cè)瓉?lái)例子中的鏈條还惠,我們 map(strUppercase) 然后 filter(isLongEnough) 最后 filter(isShortEnough)饲握;這些操作確實(shí)是按照這樣的順序發(fā)生的。但是在第四章中蚕键,我們學(xué)習(xí)了 compose(..) 通常會(huì)以函數(shù)被羅列的相反方向運(yùn)行它們救欧。所以,為什么我們?cè)?這里 不需要反轉(zhuǎn)順序來(lái)得到我們期望的相同結(jié)果呢锣光?來(lái)自于每個(gè)遞減函數(shù)的 combinationFn(..) 的抽象在底層反轉(zhuǎn)了操作實(shí)際的實(shí)施順序笆怠。所以與直覺(jué)相悖地,當(dāng)你組合一個(gè) transducer 時(shí)誊爹,你實(shí)際上要以你所期望的函數(shù)執(zhí)行的順序來(lái)羅列它們蹬刷!

列表組合:純粹 vs 不純粹

一個(gè)快速的旁注,讓我們重溫一下 listCombination(..)組合函數(shù)的實(shí)現(xiàn):

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

雖然這種方式是純粹的频丘,但是它對(duì)性能產(chǎn)生了負(fù)面的影響办成。首先,它創(chuàng)建 [..] 臨時(shí)數(shù)組包裝了 val搂漠。然后迂卢,concat(..) 創(chuàng)建了一個(gè)全新的數(shù)組,將這個(gè)臨時(shí)數(shù)組鏈接在它后面桐汤。在我們組合好的遞減函數(shù)的每一步中而克,有許多數(shù)組被創(chuàng)建又被扔掉,這對(duì)不僅對(duì) CPU 很不好而且還會(huì)引發(fā)內(nèi)存的垃圾回收怔毛。

性能好一些的员萍,不純粹版本:

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

孤立地考慮一下 listCombination(..),無(wú)疑它是不純粹的拣度,而這是我們通常想要避免的碎绎。但是蜂莉,我們考慮的角度應(yīng)當(dāng)更高一些。

listCombination(..) 根本不是我們要與之交互的函數(shù)混卵。我們沒(méi)有在程序的任何部分直接使用它映穗,而是讓 transducer 處理使用它。

回顧第五章幕随,我們聲稱降低副作用與定義純函數(shù)的目標(biāo)僅僅是向我們將要在程序中通篇使用的 API 級(jí)別的函數(shù)暴露純函數(shù)蚁滋。我們?cè)谝粋€(gè)純函數(shù)內(nèi)部觀察了它的底層,只要它不違反外部純粹性的約定赘淮,就可以為了性能而使用任何作弊的方法辕录。

listCombination(..) 更像是一個(gè) transducing 的內(nèi)部實(shí)現(xiàn)細(xì)節(jié) —— 事實(shí)上,它經(jīng)常由一個(gè) transducing 庫(kù)提供給你梢卸! —— 而非一個(gè)你平常在程序中與之交互的頂層方法走诞。

底線:我認(rèn)為使用性能優(yōu)化后的非純粹版本的 listCombination(..) 是完全可以接受的,甚至是明智的蛤高。但要確保你用了一段代碼注釋將它的非純粹性記錄下來(lái)蚣旱!

替換組合函數(shù)

至此,這就是我們從 transducing 中衍生出的東西:

words
.reduce( transducer( listCombination ), [] )
.reduce( strConcat, "" );
// WRITTENSOMETHING

這相當(dāng)好戴陡,但關(guān)于 transducing 我們手中還有最后一個(gè)技巧塞绿。而且老實(shí)說(shuō),我認(rèn)為這部分才是使你至此做出的所有思維上的努力得到回報(bào)的東西恤批。

我們能否 “組合” 這兩個(gè) reduce(..) 調(diào)用使它們成為一個(gè) reduce(..)异吻?不幸的是,我們不能僅僅將 strConcat(..) 加入 compose(..) 調(diào)用喜庞;它的外形對(duì)于這種組合來(lái)說(shuō)不正確诀浪。

但讓我肩并肩地看看這兩個(gè)函數(shù):

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

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

如果你瞇起眼,你就能看到這兩個(gè)函數(shù)幾乎是可以互換的延都。它們操作不同的數(shù)據(jù)類型雷猪,但是在概念上它們做的是相同的事情:將兩個(gè)值結(jié)合為一個(gè)。

換句話說(shuō)窄潭,strConcat(..) 是一個(gè)組合函數(shù)春宣!

這意味著如果我們的最終目標(biāo)是得到一個(gè)字符串鏈接而非一個(gè)列表的話,我們就可以使用它替換 listCombination(..)

words.reduce( transducer( strConcat ), "" );
// WRITTENSOMETHING

轟嫉你!這就是你的 transducing。我不會(huì)真的在這里摔麥克躏惋,而是輕輕地將它放下……

最后幽污,什么

深呼吸。這真是有太多東西要消化了簿姨。

用幾分鐘清理一下大腦距误,擺脫所有那些推導(dǎo)它如何工作的思維圈子簸搞,讓我們將注意力返回到在我們的應(yīng)用程序中如何使用 transducing。

回憶一下我們?cè)缦榷x的幫助函數(shù)准潭;為了清晰讓我們重命名它們:

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(..) 任然需要被傳入一個(gè)組合函數(shù)(比如 listCombination(..)strConcat(..))來(lái)聲稱一個(gè) transduce-遞減函數(shù)趁俊,然后這個(gè)函數(shù)才能在 reduce(..) 中使用(與一個(gè)初始值一起)。

但是為了更具聲明性地表達(dá)所有這些 transducing 步驟刑然,讓我們制造一個(gè)實(shí)施所有這些步驟的 transduce(..) 工具:

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

這是我們清理過(guò)后的例子:

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

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

transduce( transducer, strConcat, "", words );
// WRITTENSOMETHING

不賴吧K吕蕖?看到 listCombination(..)strConcat(..) 函數(shù)作為組合函數(shù)被互換地使用了嗎泼掠?

Transducers.js

最后怔软,讓我們使用 transducers-js 庫(kù) (https://github.com/cognitect-labs/transducers-js) 來(lái)展示我們的例子:

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

這看起來(lái)幾乎和上面一模一樣。

注意: 上面的代碼段使用 transformers.comp(..) 是因?yàn)閹?kù)提供了它择镇,但在這種情況下我們第四章的 compose(..) 將生成相同的結(jié)果挡逼。換言之,組合本身不是一個(gè) transducing 敏感的操作腻豌。

在這個(gè)代碼段中家坎,組合好的函數(shù)被命名為 transformer 而不是 transducer。這是因?yàn)槿绻覀冋{(diào)用 transformer(listCombination)(或者 transformer(strConcat))吝梅,我們不會(huì)像之前那樣直接得到 transduce-遞減函數(shù)乘盖。

transducers.map(..)transducers.filter(..) 是特殊的幫助函數(shù),它們將普通的判定或映射函數(shù)適配為生成一個(gè)特殊(在底層包裝了一個(gè) transducer 函數(shù)的)變形對(duì)象的函數(shù)憔涉;這個(gè)庫(kù)將這些變形對(duì)象用于 transducing订框。這個(gè)變形函數(shù)抽象的額外能力超出了我們要探索的范圍,更多的信息請(qǐng)參閱庫(kù)的文檔兜叨。

因?yàn)檎{(diào)用 transformer(..) 會(huì)生成一個(gè)變形對(duì)象穿扳,而且不是一個(gè)典型的二元 transduce-遞減函數(shù),所以庫(kù)還提供了 toFn(..) 來(lái)將這個(gè)變形對(duì)象適配為可以被原生數(shù)組 reduce(..) 使用的函數(shù):

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

into(..) 是庫(kù)提供的另一個(gè)幫助函數(shù)国旷,它根據(jù)被指定的空/初始值類型自動(dòng)地選擇一個(gè)默認(rèn)組合函數(shù):

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

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

當(dāng)指定一個(gè)空數(shù)組 [] 時(shí)矛物,在底層被調(diào)用的 transduce(..) 使用一個(gè)默認(rèn)的函數(shù)實(shí)現(xiàn),它就像我們的 listCombination(..) 幫助函數(shù)跪但。但當(dāng)指定一個(gè)空字符串 "" 時(shí)履羞,一個(gè)如我們 strConcat(..) 的函數(shù)就會(huì)被使用÷啪茫酷忆首!

如你所見(jiàn),transducers-js 庫(kù)使得 transducing 變得相當(dāng)直接了當(dāng)被环。我們可以非常高效地利用這種技術(shù)的力量糙及,而不必親自深入所有這些定義中間 transducer 生成工具的過(guò)程。

總結(jié)

Transduce 意味著使用遞減來(lái)變形筛欢。更具體點(diǎn)兒說(shuō)浸锨,一個(gè) transducer 是一個(gè)可以進(jìn)行組合的遞減函數(shù)唇聘。

我們使用 transducing 將相鄰的 map(..)filter(..)柱搜、以及 reduce(..) 組合在一起迟郎。我們是這樣做到的:首先將 map(..)filter(..) 表達(dá)為 reduce(..),然后將共通的組合操作抽象出來(lái)聪蘸,創(chuàng)建一個(gè)很容易組合的一元遞減函數(shù)生成函數(shù)宪肖。

Transducing 主要改善了新能,這在用于一個(gè)懶惰序列(異步 observable)時(shí)尤其明顯宇姚。

但更廣泛地說(shuō)匈庭,transducing 是我們?nèi)绾螌⒉荒苤苯咏M合的函數(shù)表達(dá)為聲明性更強(qiáng)的函數(shù)組合的方式。如果與本書(shū)中的其他技術(shù)一起恰當(dāng)?shù)厥褂没肜停湍墚a(chǎn)生更干凈阱持,可讀性更強(qiáng)的代碼!推理一個(gè)使用 transducer 的單獨(dú) reduce(..) 調(diào)用魔熏,要比跟蹤多個(gè) reduce(..) 調(diào)用容易許多衷咽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蒜绽,隨后出現(xiàn)的幾起案子镶骗,更是在濱河造成了極大的恐慌,老刑警劉巖躲雅,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鼎姊,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡相赁,警方通過(guò)查閱死者的電腦和手機(jī)相寇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)钮科,“玉大人唤衫,你說(shuō)我怎么就攤上這事∶喔” “怎么了佳励?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蛆挫。 經(jīng)常有香客問(wèn)我赃承,道長(zhǎng),這世上最難降的妖魔是什么璃吧? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任楣导,我火速辦了婚禮,結(jié)果婚禮上畜挨,老公的妹妹穿的比我還像新娘筒繁。我一直安慰自己,他們只是感情好巴元,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布毡咏。 她就那樣靜靜地躺著,像睡著了一般逮刨。 火紅的嫁衣襯著肌膚如雪呕缭。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天修己,我揣著相機(jī)與錄音恢总,去河邊找鬼。 笑死睬愤,一個(gè)胖子當(dāng)著我的面吹牛片仿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播尤辱,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼砂豌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了光督?” 一聲冷哼從身側(cè)響起阳距,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎结借,沒(méi)想到半個(gè)月后筐摘,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡船老,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年咖熟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片努隙。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡球恤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出荸镊,到底是詐尸還是另有隱情咽斧,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布躬存,位于F島的核電站张惹,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏岭洲。R本人自食惡果不足惜宛逗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盾剩。 院中可真熱鬧雷激,春花似錦替蔬、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至根悼,卻和暖如春凶异,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背挤巡。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工剩彬, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人矿卑。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓喉恋,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親粪摘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瀑晒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容