1. 程式人生 > >Reduce 和 Transduce 的含義

Reduce 和 Transduce 的含義

span 開發 opera clas 當前 form 基本功 通過 handle

一、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方法相當於默認提供appendtransduce函數。


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 的含義