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

iKcamp發表於2017-11-23

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

譯者團隊(排名不分先後):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry蘿蔔vavd317vivaxy萌萌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》姊妹篇 **

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

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

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


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

2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章