[譯] Reducers VS Transducers

jonjia發表於2018-03-26

Reducers VS Transducers

今天我們為您準備了一份函式正規化甜點。我也不知道為什麼會用「VS」,而且它倆還互相恭維。不管那麼多了,讓我們看點好東西......

Reducers

簡單來說,Reducer 就是個接收上一個計算值和一個當前值並返回新的計算值的方法。

reducers

如果你使用過陣列的 Array.prototype.reduce() 方法,就已經熟悉了 reducer。陣列的 .reduce() 方法本身並不是一個 reducer,這個方法會遍歷一個集合(譯註:累加器初始值和陣列中的元素組成的集合),然後對集合中的每個元素應用傳給這個方法的回撥函式,這個回撥函式才是一個 reducer

假設我們有一個包含五個數字的陣列:[1, 2, 3, 14, 21],我們要找出它們中的最大值。

const numbers = [1, 2, 3, 14, 21];

const biggestNumber = numbers.reduce(
  (accumulator, value) => Math.max(accumulator, value)
);

// 21
複製程式碼

這裡的箭頭函式就是一個 reducer。陣列的 .reduce() 方法只是取這個 reducer 上一次執行的結果(譯註:初始值引數或陣列第一個元素)和陣列中的下一個元素傳給並繼續呼叫這個 reducer。

Reducers 可以處理任何型別的值。唯一條件就是計算方法返回的值型別和傳給計算方法的值型別要保持一致。

在下面的例子中,你可以輕鬆建立一個作用於字串的 reducer:

const folders = ['usr', 'var', 'bin'];

const path = folders.reduce(
  (accumulator, value) => `${accumulator}/${value}`
, ''); // Here I passed empty string as an initial value

// /usr/var/bin
複製程式碼

實際上,不使用 Array.reduce() 方法來說明更好理解。如下:

const stringReducer = (accumulator, value) => `${accumulator} ${value}`

const helloWorld = stringReducer("Hello", "world!")

// Hello world!
複製程式碼

Map 和 Filter 方法作為 Reducers

Reducers 還有一個好處是你可以鏈式地連線它們,來實現對某些資料的一系列操作。這就為功能模組化和 reducer 的複用提供了巨大的可能。

假設有一個有序的數字陣列。你想獲取其中的偶數,然後再乘以 2。

實現上述功能通常的方法是呼叫陣列的 .map.filter 方法:

[1, 2, 3, 4, 5, 6]
  .filter((x) => x % 2 === 0)
  .map((x) => x * 2)
複製程式碼

但如果這個陣列有 1000000 個元素呢?你需要遍歷整個陣列的每個元素,這樣的效率太低了。

我們需要用某種方式去組合傳給 mapfilter 方法的函式。因為它們的介面不同,所以我們無法實現。傳給 filter 方法的函式稱為斷言函式,它接收一個值,依據內部邏輯返回斷言的 True 或者 False。傳給 map 方法的函式稱為轉換函式,它接收一個值,並返回轉換後的值

我們可以通過 reducers 來實現這一點,讓我們建立自己的 reducer 版本的 .map.filter 方法。

const filter = (predicate) => {
  return (accumulator, value) => {
    if(predicate(value)){
      accumulator.push(value);
    }
    return accumulator;
  }
}

const map = (transformer) => {
  return (accumulator, value) => {
    accumulator.push(transformer(value));
    return accumulator;
  }
}
複製程式碼

真棒,我們使用了 裝飾器 來包裝我們的 reducers。現在我們有自己的 mapfilter 方法,它們返回的 reducers 可以傳遞給陣列的 Array.reduce() 方法。

[1, 2, 3, 4, 5, 6]
  .reduce(filter((x) => x % 2 === 0), [])
  .reduce(map((x) => x * 2), [])
複製程式碼

太棒了,現在我們就能鏈式地呼叫一系列的 .reduce 方法,但我們還是沒有組合我們的 reducers!好訊息是我們只差一步了。為了能組合 reducers 我們需要讓它們能互相傳遞。

Transducers, 可以有嗎?

來升級下我們的 filter 方法,讓它能夠接收 reducers 作為引數。我們要分解下它,不是將值新增到 accumulator,而是要傳給傳入的 reducer,並執行這個 reducer。

const filter = (predicate) => (reducer) => {
  return (accumulator, value) => {
    if(predicate(value)){
      return reducer(accumulator, value);
    }
    return accumulator;
  }
}
複製程式碼

我們接收一個 reducer 作為引數,並返回另一個 reducer 的這種模式就叫做 transducer。它是 transformerreducer 的結合(我們接收一個 reducer,並對它進行了轉換)。

const transducer => (reducer) => {
  return (accumulator, value) => {
    // 轉換 reducer 的邏輯
  }
}
複製程式碼

所以最基礎的 transducer 就像 (oneReducer) => anotherReducer 這樣。

現在我們就可以組合使用我們的 mapping reducer 和 filtering transducer,一次呼叫就可以實現我們的計算了。

const evenPredicate = (x) => x % 2 === 0;
const doubleTransformer = (x) = x * 2;

const filterEven = filter(evenPredicate);
const mapDouble = map(doubleTransformer);

[1, 2, 3, 4, 5, 6]
  .reduce(filterEven(mapDouble), []);
複製程式碼

實際上,我們也可以把我們的 map 方法改造為一個 transducer,然後無限地繼續這種改造。

但如果要組合 2 個以上的 reducers 呢?我們要找到更簡便的組合方法。

更好的組合方法

總體來說就是,我們需要一個能接收一定數量的函式並把它們按順序組合的方法。類似下面這樣:

compose(fn1, fn2, fn3)(x) => fn1(fn2(fn3(x)))
複製程式碼

幸運的是,很多庫都提供了這種功能。比如 RamdaJS 這個庫。但為了解釋清楚,來建立我們自己的版本吧。

const compose = (...functions) =>
  functions.reduce((accumulation, fn) =>
    (...args) => accumulation(fn(args)), x => x)
複製程式碼

這個函式的功能非常緊湊,我們來分解下。

如果我們像這樣 compose(fn1, fn2, fn3)(x) 呼叫了這個函式。

首先看 x => x 部分。在 λ 演算中,這被稱為 恆等函式。不管接收什麼引數,它都不會改變。我們就從這裡展開。

所以在第一次遍歷中,我們將使用 fn1 函式作為引數來呼叫 identity function 函式(為了方便,我們稱之為 I):

  // 恆等函式:I
  (...args) => accumulation(fn(args))

  // 第一步
  // 我們把 fn1 傳給 accumulation 方法
  (...args) => accumulation(fn1(args))

  // 第二步
  // 這裡我們用 I 接收 fn1 作為引數替代 accumulation
  (...args) => I(fn1(args))
複製程式碼

耶,我們計算出了第一次遍歷後新的 accumulation 方法。我們再來一次:

  (...args) => I(fn1(args)) // 新的 accumulation 方法

  // 第三步
  // 現在我們把 fn2 傳給 accumulation 方法
  (...args) => accumulation(fn2(args))

  // 第四步
  // 我們來算出 accumulation 的當前值
  (...args) => I(fn1(fn2(args)))
複製程式碼

我認為你應該理解了。現在只需要對 fn3 重複第三步和第四步,就可以把 compose(fn1, fn2, fn3)(x) 轉為 fn1(fn2(fn3(x))) 了。

最後我們就可以像下面這樣組合我們的 mapfilter 了:

[1, 2, 3, 4, 5, 6]
  .reduce(compose(filterEven,
          mapDouble));
複製程式碼

總結

我想你已經掌握了 reducers,如果還沒有 — 你也已經學會了處理集合的抽象方法。Reducers 可以處理不同的資料結構。

你也學會了如何用 transducers 有效地進行計算。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章