Reduce 和 Transduce 的含義
一、reduce 的用法
reduce
是一種數組運算,通常用於將數組的所有成員"累積"為一個值。
var arr = [1, 2, 3, 4]; var sum = (a, b) => a + b; arr.reduce(sum, 0) // 10
上面代碼中,reduce
對數組arr
的每個成員執行sum
函數。sum
的參數a
是累積變量,參數b
是當前的數組成員。每次執行時,b
會加到a
,最後輸出a
。
累積變量必須有一個初始值,上例是reduce
函數的第二個參數0
。如果省略該參數,那麽初始值默認是數組的第一個成員。
var arr = [1, 2, 3, 4]; var sum = function (a, b) { console.log(a, b); return a + b; }; arr.reduce(sum) // => 10 // 1 2 // 3 3 // 6 4
上面代碼中,reduce
方法省略了初始值。通過sum
函數裏面的打印語句,可以看到累積變量每一次的變化。
總之,reduce
方法提供了一種遍歷手段,對數組所有成員進行"累積"處理。
二、map 是 reduce 的特例
累積變量的初始值也可以是一個數組。
var arr = [1, 2, 3, 4]; var handler = function (newArr, x) { newArr.push(x + 1); return newArr; }; arr.reduce(handler, []) // [2, 3, 4, 5]
上面代碼中,累積變量的初始值是一個空數組,結果reduce
map
方法,對原數組進行一次"變形"。下面是使用map
改寫上面的例子。
var arr = [1, 2, 3, 4]; var plusOne = x => x + 1; arr.map(plusOne) // [2, 3, 4, 5]
事實上,所有的map
方法都可以基於reduce
實現。
function map(f, arr) { return arr.reduce(function(result, x) { result.push(f(x)); return result; }, []); }
因此,map
只是reduce
三、reduce
的本質
本質上,reduce
是三種運算的合成。
- 遍歷
- 變形
- 累積
還是來看上面的例子。
var arr = [1, 2, 3, 4]; var handler = function (newArr, x) { newArr.push(x + 1); return newArr; }; arr.reduce(handler, []) // [2, 3, 4, 5]
上面代碼中,首先,reduce
遍歷了原數組,這是它能夠取代map
方法的根本原因;其次,reduce
對原數組的每個成員進行了"變形"(上例是加1
);最後,才是把它們累積起來(上例是push
方法)。
四、 transduce 的含義
reduce
包含了三種運算,因此非常有用。但也帶來了一個問題:代碼的復用性不高。在reduce
裏面,變形和累積是耦合的,不太容易拆分。
每次使用reduce
,開發者往往都要從頭寫代碼,重復實現很多基本功能,很難復用別人的代碼。
var handler = function (newArr, x) { newArr.push(x + 1); return newArr; };
上面的這個處理函數,就很難用在其他場合。
有沒有解決方法呢?回答是有的,就是把"變形"和"累積"這兩種運算分開。如果reduce
允許變形運算和累積運算分開,那麽代碼的復用性就會大大增加。這就是transduce
方法的由來。
transduce
這個名字來自 transform(變形)和 reduce 這兩個單詞的合成。它其實就是reduce
方法的一種不那麽耦合的寫法。
// 變形運算 var plusOne = x => x + 1; // 累積運算 var append = function (newArr, x) { newArr.push(x); return newArr; }; R.transduce(R.map(plusOne), append, [], arr); // [2, 3, 4, 5]
上面代碼中,plusOne
是變形操作,append
是累積操作。我使用了 Ramda 函數庫的transduce
實現。可以看到,transduce
就是將變形和累積從reduce
拆分出來,其他並無不同。
五、transduce 的用法
transduce
最大的好處,就是代碼復用更容易。
var arr = [1, 2, 3, 4]; var append = function (newArr, x) { newArr.push(x); return newArr; }; // 示例一 var plusOne = x => x + 1; var square = x => x * x; R.transduce( R.map(R.pipe(plusOne, square)), append, [], arr ); // [4, 9, 16, 25] // 示例二 var isOdd = x => x % 2 === 1; R.transduce( R.pipe(R.filter(isOdd), R.map(square)), append, [], arr ); // [1, 9]
上面代碼中,示例一是兩個變形操作的合成,示例二是過濾操作與變形操作的合成。這兩個例子都使用了 Pointfree 風格。
可以看到,transduce
非常有利於代碼的復用,可以將一系列簡單的、可復用的函數合成為復雜操作。作為練習,有興趣的讀者可以試試,使用reduce
方法完成上面兩個示例。你會發現,代碼的復雜度和行數大大增加。
六、Transformer 對象
transduce
函數的第一個參數是一個對象,稱為 Transformer 對象(變形器)。前面例子中,R.map(plusOne)
返回的就是一個 Transformer 對象。
事實上,任何一個對象只要遵守 Transformer 協議,就是 Transformer 對象。
var Map = function(f, xf) { return { "@@transducer/init": function() { return xf["@@transducer/init"](); }, "@@transducer/result": function(result) { return xf["@@transducer/result"](result); }, "@@transducer/step": function(result, input) { return xf["@@transducer/step"](result, f(input)); } }; };
上面代碼中,Map
函數返回的就是一個 Transformer 對象。它必須具有以下三個屬性。
- @@transducer/step:執行變形操作
- @@transducer/init:返回初始值
- @@transducer/result:返回變形後的最終值
所有符合這個協議的對象,都可以與其他 Transformer 對象合成,充當transduce
函數的第一個參數。
因此,transduce
函數的參數類型如下。
transduce( 變形器 : Object, 累積器 : Function, 初始值 : Any, 原始數組 : Array )
七、into 方法
最後,你也許發現了,前面所有示例使用的都是同一個累積器。
var append = function (newArr, x) { newArr.push(x); return newArr; };
上面代碼的append
函數是一個常見累積器。因此, Ramda 函數庫提供了into
方法,將它內置了。也就是說,into
方法相當於默認提供append
的transduce
函數。
R.transduce(R.map(R.add(1)), append, [], [1,2,3,4]); // 等同於 R.into([], R.map(R.add(1)), [1,2,3,4]);
上面代碼中,into
方法的第一個參數是初始值,第二個參數是變形器,第三個參數是原始數組,不需要提供累積器。
下面是另外一個例子。
R.into( [5, 6], R.pipe(R.take(2), R.map(R.add(1))), [1, 2, 3, 4] ) // [5, 6, 2, 3]
Reduce 和 Transduce 的含義