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

iKcamp發表於2017-11-29

關於譯者:這是一個流淌著滬江血液的純粹工程:認真,是 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庫(github.com/cognitect-l… ):

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》姊妹篇 **

翻譯連載 | 附錄 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開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章