266
技術社區[雲棲]
輕量函數式 JavaScript:附錄 A、Transducing
與我們在本書中所講解的內容相比,Transducing 是一種更高級的技術。它擴展了第八章中的列表操作的許多概念。
我不認為這個話題是嚴格的 “輕量函數式”,它更像是在此之上的額外獎勵。我將它留作附錄是因為你很可能需要暫且跳過關於它的討論,而在你對本書正文中的概念感到相當適應 —— 並且確實經過實踐! —— 之後再回到這裏。
老實說,即便是教授了 transducing 許多次,而且編寫了這一章之後,我依然在努力地嚐試使用這種技術來武裝自己的頭腦。所以,如果它讓你感到很繞也不要灰心。給這一章夾上一個書簽,當你準備好了之後再回來。
Transducing 意味著帶有變形(transforming)的遞減(reduction)。
我知道,它聽起來就像一個雜亂的詞匯 —— 它使人煳塗的地方要比它澄清的東西多。但還是讓我們看看它能有多麼強大。實際上,我認為一旦你掌握了輕量函數式編程的原理,它就是你能力的最佳展示。
正如本書的其他部分一樣,我的方式是首先解釋 為什麼,然後在講解 如何做,最後歸結為一種簡化的,可重用的 什麼。這通常與其他許多人的教授方法相反,但我認為這種方式可以使你更深入地學習。
首先,為什麼
讓我們從擴展第三章中的一個場景開始,測試一個單詞,看它是否足夠短並/或足夠長:
function isLongEnough(str) {
return str.length >= 5;
}
function isShortEnough(str) {
return str.length <= 10;
}
在第三章中,我們使用了這些判定函數來測試一個單詞。然後在第八章中,我們學習了如何使用 filter(..)
之類的列表操作重複這樣的測試。例如:
var words = [ "You", "have", "written", "something", "very", "interesting" ];
words
.filter( isLongEnough )
.filter( isShortEnough );
// ["written","something"]
這可能不太明顯,不過這種分離且相鄰的列表操作模式有些不盡人意的性質。當我們處理僅擁有為數不多的值的單一數組是,一切都很正常。但如果數組中有許多值的時候,分離地處理每個 filter(..)
可能會出人意料地降低程序運行的速度。
當我們的數組是異步/懶惰(也稱為 observable)的、在對事件作出相應而跨時段處理值(見第十章)的時候也會出現同樣的性能問題。在這種場景下,事件流中每次隻會有一個值被傳遞出來,所以使用兩個分離的 filter(..)
函數處理這些離散的值也不是什麼大問題。
但微妙的是,每個 filter(..)
方法都生成一個分離的 observable。將一個值從一個 observable 中傳遞到另一個 observable 的開銷可能累積起來。特別是在這些情況下成千或上百萬的值需要被處理並非不尋常的事;即便是如此之小的開銷也會很快地累積起來。
另一個缺陷是可讀性,特別是當我們需要對多個列表(或者 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 );
}
但這不是 FP 的方式!
在第八章中,我們談到了熔合 —— 組合相鄰的映射函數。回想一下:
words
.map(
pipe( removeInvalidChars, upper, elide )
);
不幸的是,組合相鄰的判定函數不像組合相鄰的映射函數那麼簡單。究其原因,考慮一下判定函數的 “外形(shape)” —— 某種描述輸入和輸出簽名的學術化方式。它接收一個單獨的值,並返回一個 true
或 false
。
如果你試著使用 isShortEnough(isLongEnough(str))
,它是不會正常工作的。isLongEnough(..)
將會返回 true
/ false
,而不是 isShortEnough(..)
所期待的字符串值。煩人。
在組合相鄰的遞減函數時也存在相似的惱人之處。遞減函數的 “外形” 是一個接收兩個輸入值的函數,並返回一個組合好的值。遞減函數的單值輸出不適於作為另一個期待兩個值的遞減函數的輸入。
另外,reduce(..)
幫助函數接收一個可選的輸入 initialValue
。有時它可以被忽略,但有時不得不被傳入。這使組合變得更複雜,因為一個遞減操作可能需要一個 initialValue
而另一個遞減操作可能需要一個不同的 initialValue
。我們如何才能使用某種組合好的遞減函數來發起一個 reduce(..)
調用呢?
考慮一個這樣鏈條:
words
.map( strUppercase )
.filter( isLongEnough )
.filter( isShortEnough )
.reduce( strConcat, "" );
// "WRITTENSOMETHING"
你能想想一個包含所有 map(strUppercase)
、filter(isLongEnough)
、filter(isShortEnough)
、reduce(strConcat)
這些步驟的組合嗎?每一個操作函數的外形都是不同的,所以它們不能直接組合在一起。我們需要調整一下它們的外形來使它們彼此吻合。
希望這些觀察展示了為什麼單純的熔合式組合不能完成這個任務。我們需要更強大的技術,而 transducing 就是工具。
接下來,如何做
讓我們來談談如何才能衍生出一種映射函數、判定函數和/或遞減函數的組合。
不要被衝昏了頭腦:你不必在你自己的程序中把我們將要探索的所有這些思維步驟都走一遍。一旦你理解並能夠認出 trasnducing 解決的問題,你就可以直接跳到使用一個 FP 庫的 transduce(..)
工具,並繼續處理你程序的其餘部分!
讓我們開始吧。
將映射/過濾表達為遞減
我們要施展的第一個技巧是將 filter(..)
和 map(..)
調用表達為 reduce(..)
調用。回憶一下我們在第八章中是如何做的:
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(..)
這四個遞減函數,因為它們接收兩個參數而不是一個。
在第八章中,我們作弊並使用了 list.push(..)
作為一種副作用進行改變,而不是調用 list.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(..)
在這裏是否必要。
將遞減函數參數化
兩個過濾遞減函數除了使用一個不同的判定函數以外幾乎是相同的。讓我們將此參數化,這樣我們就得到一個可以定義任意過濾遞-減函數的工具:
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(..)
進行相同的參數化:
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(..)
函數在做什麼,它接收兩個值(一個數組和另一個值)並通過將值連接到數組末尾來將它們 “組合”,再返回一個新數組。非常沒有創意,但我們可以將它命名為 listCombination(..)
:
function listCombination(list,val) {
return list.concat( [val] );
}
現在讓我們重新定義遞減函數的幫助函數,來使用 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;
};
}
我們的鏈條依然沒變(所以我們不再囉嗦這一點)。
將組合參數化
我們簡單的 listCombination(..)
工具隻是我們結合兩個值的一種可能的方式。讓我們將使用它的過程參數化,來時我們的遞減函數更加一般化:
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(..)
這些遞減函數,我們就快成功了。
組合柯裏化後的函數
這一步是所有思考中最刁鑽的一步。所以這裏要慢慢讀並集中注意力。
讓我們考慮一下上麵柯裏化後的函數,但不帶 listCombination(..)
函數被傳入的部分:
var x = curriedMapReducer( strUppercase );
var y = curriedFilterReducer( isLongEnough );
var z = curriedFilterReducer( isShortEnough );
考慮所有這三個中間函數的外形,x(..)
、y(..)
、和 z(..)
。每一個都期待一個單獨的組合函數,並為它生成一個遞減函數。
記住,如果我們想要得到所有這些函數的獨立的遞減函數,我們可以這樣做:
var upperReducer = x( listCombination );
var longEnoughReducer = y( listCombination );
var shortEnoughReducer = z( listCombination );
但如果調用了 y(z)
你會得到什麼?基本上是說,當將 z
作為 y(..)
的 combinationFn(..)
傳入時發生了什麼?這會返回一個內部看起來像這樣的遞減函數:
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,來自 z(..):
function reducer(list,val) {
if (isShortEnough( val )) return listCombination( list, val );
return list;
}
// longAndShortEnoughReducer,來自 y(..):
function reducer(list,val) {
if (isLongEnough( val )) return shortEnoughReducer( list, val );
return list;
}
你看到 shortEnoughReducer(..)
是如何在 longAndShortEnoughReducer(..)
內部取代了 listCombination(..)
嗎?為什麼這個好用?
因為 一個 reducer(..)
的外形和 listCombination(..)
的外形是相同的。 換言之,一個遞減函數可以被用作另一個遞減函數的組合函數;這就是它們如何組合的!listCombination(..)
函數製造了第一個遞減函數,然後這個遞減函數可以作為組合函數來製造下一個遞減函數,以此類推。
讓我們使用幾個不同的值來測試一下我們的 longAndShortEnoughReducer(..)
:
longAndShortEnoughReducer( [], "nope" );
// []
longAndShortEnoughReducer( [], "hello" );
// ["hello"]
longAndShortEnoughReducer( [], "hello world" );
// []
longAndShortEnoughReducer(..)
工具濾除了既不夠長也不夠短的值,而且它是在同一個步驟中做了這兩個過濾的。它是一個組合的遞減函數!
再花點兒時間讓它消化吸收。它還是有些讓我混亂。
現在,把 x(..)
(大寫遞減函數生成器)代入組合之中:
var longAndShortEnoughReducer = y( z( listCombination) );
var upperLongAndShortEnoughReducer = x( longAndShortEnoughReducer );
正如 upperLongAndShortEnoughReducer(..)
這個名字所暗示的,它一次完成所有三個步驟 —— 一個映射和兩個過濾!它內部看起來就像這樣:
// upperLongAndShortEnoughReducer:
function reducer(list,val) {
return longAndShortEnoughReducer( list, strUppercase( val ) );
}
一個字符串 val
被傳入,由 strUppercase(..)
改為大寫,然後被傳遞給 longAndShortEnoughReducer(..)
。這個函數僅條件性地 —— 如果這個字符串長短合適 —— 將這個大寫字符串添加到 list
,否則 list
保持不變。
我的大腦花了好幾周才完全理解了這套雜耍的含義。所以如果你需要在這裏停下並重讀幾遍(幾十遍!)來搞明白它也不要擔心。慢慢來。
現在我們驗證一下:
upperLongAndShortEnoughReducer( [], "nope" );
// []
upperLongAndShortEnoughReducer( [], "hello" );
// ["HELLO"]
upperLongAndShortEnoughReducer( [], "hello world" );
// []
這個遞減函數是一個映射函數和兩個過濾函數的組合!這真令人吃驚!
概括一下我們目前身在何處:
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"]
考慮一下這個組合函數的 “數據” 流:
-
listCombination(..)
作為組合函數流入isShortEnough(..)
,為它製造了過濾-遞減函數。 - 然後這個結果遞減函數作為組合函數流入
isLongEnough(..)
,為它製造了過濾-遞減函數。 - 最後,這個結果遞減函數作為組合函數流入
strUppercase(..)
,為它製造了映射-遞減函數。
在前一個代碼段中,composition(..)
是一個組合好的函數,它期待一個組合函數來製造一個遞減函數;composition(..)
有一個特殊的標簽:transducer。向一個 transducer 提供組合函數就生成了組合好的遞減函數:
// 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"]
注意: 我們應當關注一下前兩個代碼段中的 compose(..)
順序,它可能有些令人煳塗。回憶一下我們原來例子中的鏈條,我們 map(strUppercase)
然後 filter(isLongEnough)
最後 filter(isShortEnough)
;這些操作確實是按照這樣的順序發生的。但是在第四章中,我們學習了 compose(..)
通常會以函數被羅列的相反方向運行它們。所以,為什麼我們在 這裏 不需要反轉順序來得到我們期望的相同結果呢?來自於每個遞減函數的 combinationFn(..)
的抽象在底層反轉了操作實際的實施順序。所以與直覺相悖地,當你組合一個 transducer 時,你實際上要以你所期望的函數執行的順序來羅列它們!
列表組合:純粹 vs 不純粹
一個快速的旁注,讓我們重溫一下 listCombination(..)
組合函數的實現:
function listCombination(list,val) {
return list.concat( [val] );
}
雖然這種方式是純粹的,但是它對性能產生了負麵的影響。首先,它創建 [..]
臨時數組包裝了 val
。然後,concat(..)
創建了一個全新的數組,將這個臨時數組鏈接在它後麵。在我們組合好的遞減函數的每一步中,有許多數組被創建又被扔掉,這對不僅對 CPU 很不好而且還會引發內存的垃圾回收。
性能好一些的,不純粹版本:
function listCombination(list,val) {
list.push( val );
return list;
}
孤立地考慮一下 listCombination(..)
,無疑它是不純粹的,而這是我們通常想要避免的。但是,我們考慮的角度應當更高一些。
listCombination(..)
根本不是我們要與之交互的函數。我們沒有在程序的任何部分直接使用它,而是讓 transducer 處理使用它。
回顧第五章,我們聲稱降低副作用與定義純函數的目標僅僅是向我們將要在程序中通篇使用的 API 級別的函數暴露純函數。我們在一個純函數內部觀察了它的底層,隻要它不違反外部純粹性的約定,就可以為了性能而使用任何作弊的方法。
listCombination(..)
更像是一個 transducing 的內部實現細節 —— 事實上,它經常由一個 transducing 庫提供給你! —— 而非一個你平常在程序中與之交互的頂層方法。
底線:我認為使用性能優化後的非純粹版本的 listCombination(..)
是完全可以接受的,甚至是明智的。但要確保你用了一段代碼注釋將它的非純粹性記錄下來!
替換組合函數
至此,這就是我們從 transducing 中衍生出的東西:
words
.reduce( transducer( listCombination ), [] )
.reduce( strConcat, "" );
// WRITTENSOMETHING
這相當好,但關於 transducing 我們手中還有最後一個技巧。而且老實說,我認為這部分才是使你至此做出的所有思維上的努力得到回報的東西。
我們能否 “組合” 這兩個 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 ), "" );
// WRITTENSOMETHING
轟!這就是你的 transducing。我不會真的在這裏摔麥克,而是輕輕地將它放下……
最後,什麼
深唿吸。這真是有太多東西要消化了。
用幾分鍾清理一下大腦,擺脫所有那些推導它如何工作的思維圈子,讓我們將注意力返回到在我們的應用程序中如何使用 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(..)
)來聲稱一個 transduce-遞減函數,然後這個函數才能在 reduce(..)
中使用(與一個初始值一起)。
但是為了更具聲明性地表達所有這些 transducing 步驟,讓我們製造一個實施所有這些步驟的 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 );
// WRITTENSOMETHING
不賴吧!?看到 listCombination(..)
和 strConcat(..)
函數作為組合函數被互換地使用了嗎?
Transducers.js
最後,讓我們使用 transducers-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(..)
是因為庫提供了它,但在這種情況下我們第四章的 compose(..)
將生成相同的結果。換言之,組合本身不是一個 transducing 敏感的操作。
在這個代碼段中,組合好的函數被命名為 transformer
而不是 transducer
。這是因為如果我們調用 transformer(listCombination)
(或者 transformer(strConcat)
),我們不會像之前那樣直接得到 transduce-遞減函數。
transducers.map(..)
和 transducers.filter(..)
是特殊的幫助函數,它們將普通的判定或映射函數適配為生成一個特殊(在底層包裝了一個 transducer 函數的)變形對象的函數;這個庫將這些變形對象用於 transducing。這個變形函數抽象的額外能力超出了我們要探索的範圍,更多的信息請參閱庫的文檔。
因為調用 transformer(..)
會生成一個變形對象,而且不是一個典型的二元 transduce-遞減函數,所以庫還提供了 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
庫使得 transducing 變得相當直接了當。我們可以非常高效地利用這種技術的力量,而不必親自深入所有這些定義中間 transducer 生成工具的過程。
總結
Transduce 意味著使用遞減來變形。更具體點兒說,一個 transducer 是一個可以進行組合的遞減函數。
我們使用 transducing 將相鄰的 map(..)
、filter(..)
、以及 reduce(..)
組合在一起。我們是這樣做到的:首先將 map(..)
和 filter(..)
表達為 reduce(..)
,然後將共通的組合操作抽象出來,創建一個很容易組合的一元遞減函數生成函數。
Transducing 主要改善了新能,這在用於一個懶惰序列(異步 observable)時尤其明顯。
但更廣泛地說,transducing 是我們如何將不能直接組合的函數表達為聲明性更強的函數組合的方式。如果與本書中的其他技術一起恰當地使用,它就能產生更幹淨,可讀性更強的代碼!推理一個使用 transducer 的單獨 reduce(..)
調用,要比跟蹤多個 reduce(..)
調用容易許多。
最後更新:2017-09-02 16:02:36