[譯] Transducers: JavaScript 中高效的數據處理 Pipeline(第 18 部分)

Transducers:JavaScript 中高效的數據處理 Pipeline

Smoke Art Cubes to Smoke

Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)javascript

注意:這是從頭開始學 JavaScript ES6+ 中的函數式編程和組合軟件技術中 「撰寫軟件」 系列的一部分。敬請關注,咱們會講述大量關於這方面的知識! < 上一篇 | << 從第一篇開始前端

在使用 transducer 以前,你首先要徹底搞懂複合函數(function composition)reducers 是什麼。java

Transduce:源於 17 世紀的科學術語(latin name 通常指學名)「transductionem」,意爲「改變、轉換」。它更早衍生自「transducere/traducere」,意思是「引導或者跨越、轉移」。node

一個 transducer 是一個可組合的高階 reducer。以一個 reducer 做爲輸入,返回另一個 reducer。android

Transducers 是:ios

  • 可組合使用的簡單功能集合
  • 對大型集合或者無限流有效:無論 pipeline 中的操做數量有多少,都只對單一元素進行一次枚舉。
  • 可以轉換任何可枚舉的源(例如,數組、樹、流、圖等...)
  • 無需更換 transducer pipeline,便可用於惰性或熱切求值(譯者注:求值策略)。

Reducer 將多個輸入 摺疊(fold) 成單個輸出,其中「摺疊」能夠用幾乎任何產生單個輸出的二進制操做替換,例如:git

// 求和: (1, 2) = 3 
const add = (a, c) => a + c;

// 求乘積: (2, 4) = 8 
const multiply = (a, c) => a * c;

// 字符串拼接: ('abc', '123') = 'abc123' 
const concatString = (a, c) => a + c;

// 數組拼接: ([1,2], [3,4]) = [1, 2, 3, 4] 
const concatArray = (a, c) => [...a, ...c];
複製代碼

Transducer 作了不少相同的事情,可是和普通的 reducer 不一樣,transducer 可使用正常地組合函數組合。換句話說,你能夠組合任意數量的 tranducer,組成一個將每一個 transducer 組件串聯在一塊兒的新 transducer。github

普通的 reducer 不能這樣(組合)。由於它須要兩個參數,只返回一個輸出值。因此你不能簡單地將輸出鏈接到串聯中下一個 reducer 的輸入。這樣會出現類型不符合的狀況:編程

f: (a, c) => a
g:          (a, c) => a
h: ???
複製代碼

Transducers 有着不一樣的簽名:後端

f: reducer => reducer
g:            reducer => reducer
h: reducer =>         reducer
複製代碼

爲何選擇 Transducer?

一般,處理數據時,將處理分解成多個獨立的可組合階段頗有用。例如,從較大的集合中選擇一些數據而後處理該數據很是常見。你可能會這麼作:

const friends = [
  { id: 1, name: 'Sting', nearMe: true },
  { id: 2, name: 'Radiohead', nearMe: true },
  { id: 3, name: 'NIN', nearMe: false },
  { id: 4, name: 'Echo', nearMe: true },
  { id: 5, name: 'Zeppelin', nearMe: false }
];

const isNearMe = ({ nearMe }) => nearMe;

const getName = ({ name }) => name;

const results = friends
  .filter(isNearMe)
  .map(getName);

console.log(results);
// => ["Sting", "Radiohead", "Echo"]
複製代碼

這對於像這樣的小型列表來講很好,可是存在一些潛在的問題:

  1. 這僅僅只適用於數組。對於那些來自網絡訂閱的潛在無限數據流,或者朋友的朋友的社交圖如何處理呢?

  2. 每次在數組上使用點鏈語法(dot chaining syntax)時,JavaScript 都會構建一個全新的中間數組,而後再轉到鏈中的下一個操做。若是你有一個 2,000,000 名「朋友」的名單,這可能會使數據處理減慢一兩個數量級。使用 transducer,你能夠經過完整的 pipeline 流式傳輸每一個朋友,而無需在它們之間創建中間集合,從而節省大量時間和內存。

  3. 使用點鏈,你必須構建標準操做的不一樣實現。如 .filter().map().reduce().concat() 等。數組方法內置在 JavaScript 中,可是若是你想構建自定義數據類型並支持一堆標準操做並且還不須要重頭進行編寫,改怎麼辦?Transducer 可使用任何傳輸數據類型:編寫一次操做符,在支持 transducer 的任何地方使用它。

讓咱們看看 transducer。這段代碼還不能工做,可是還請繼續,你將可以本身構建這個 transducer pipeline 的每一部分:

const friends = [  
  { id: 1, name: 'Sting', nearMe: true },  
  { id: 2, name: 'Radiohead', nearMe: true },  
  { id: 3, name: 'NIN', nearMe: false },  
  { id: 4, name: 'Echo', nearMe: true },  
  { id: 5, name: 'Zeppelin', nearMe: false }  
];

const isNearMe = ({ nearMe }) => nearMe;

const getName = ({ name }) => name;

const getFriendsNearMe = compose(  
  filter(isNearMe),  
  map(getName)  
);

const results2 = toArray(getFriendsNearMe, friends);
複製代碼

在你告訴他們開始並向他們提供一些數據進行處理以前,transducer 不會作任何事情。這就是咱們爲何須要使用 toArray()。他提供傳導過程並告訴 transducer 將結果轉換成新數組。你能夠告訴它轉換一個流、一個 observable,或者任何你喜歡的東西,而不只僅只是調用 toArray()

Transducer 能夠將數字映射(mapping)成字符串,或者將對象映射到數組,或者將數組映射成更小的數組,或者根本不作任何改變,映射 { x, y, z } -> { x, y, z }。Transducer 能夠過濾流中的部分信號 { x, y, z } -> { x, y },甚至能夠生成新值插入到輸出流中,{ x, y, z } -> { x, xx, y, yy, z, zz }

我將在本節中使用「信號(signal)」和「流(stream)」等詞語。請記住,當我說「流」時,我並非指任何特定的數據類型:只是一個有零個或者多個值的序列,或者隨時間表達的值列表。

背景和詞源

在硬件信號處理系統中,transducer(換能器)是將一種形式的能量轉換成另外一種形式的裝置。例如,麥克風換能器將音頻波轉換爲電能。換句話說,它將一種信號轉換成爲另外一種信號。一樣,代碼中的 transducer 將一個信號轉換成另外一個信號。

軟件找那個使用 「transducer」 一詞和數據轉換的可組合 pipeline 的通用概念至少能夠追溯到 20 世紀 60 年代,可是咱們對於他們應該如何工做的想法已經從一種語言和上下文轉變爲下一種語言。在計算機科學的早期,許多軟件工程師也是電氣工程師。當時對計算機科學的通常研究常常涉及到硬件和軟件設計。所以,將計算過程視爲 「transducer」 並非特別新穎。在早期的計算機科學文獻中可能會遇到這個術語 —— 特別是在數字信號處理(DSP)和數據流編程的背景下。

在 20 世紀 60 年代,麻省理工學院林肯實驗室的圖形計算開始使用 TX-2 計算機系統,這是美國空軍 SAGE 防護系統的前身。Ivan Sutherland 著名的 Sketchpad,於 1961 年至 1962 年開發,是使用光筆進行對象原型委派和圖形編程的早期例子。

Ivan 的兄弟 William Robert 「Bert」 Sutherland 是數據流編程的幾個先驅之一。他在 Sketchpad 上構建了一個數據流編程環境。它將軟件「過程」描述爲操做員節點的有向圖,其輸出鏈接到其餘節點的輸入。他在 1966 年的論文 「The On-Line Graphical Specification of Computer Procedures」 中寫下了這段經歷。在連續運行的交互式程序循環中,全部內容都表示爲值的流,而不是數組和處理中的數組。每一個節點在到達參數輸入時處理每一個值。你如今能夠在虛擬藍圖引擎 Visual Scripting EnvironmentNative Instruments’ Reaktor 找到相似的系統,這是一種音樂家用來構建自定義音頻合成器的可視化編程環境。

Bert Sutherland 撰寫的運營商組成圖

Bert Sutherland 撰寫的運營商組成圖

據我所知,第一本在基於通用軟件的流處理環境中推廣 「transducer」 一詞的書是 1985 年 MIT 計算機科學課程 「Structure and Interpretation of Computer Programs」 的教科書(SICP)。該書由 Harold Abelson、Gerald Jay Sussman、Julie Sussman 和撰寫。然而在數字信號處理中使用術語 「transducer」 早於 SICP。

:從函數式編程的角度來看,SICP 仍然是對計算機科學出色的介紹。它仍然是這個主題中我最喜歡的書。

最近,transducer 已經從新被獨立發掘。而且 Rich Hickey(大約 2014 年)爲 Clojure 開發了一個不一樣的協議,他以精心選擇基於詞源的概念詞而聞名。這時,我就會說他說的太棒了,由於 Clojure 的 transducer 的內在基本和 SICP 中的相同,而且他們也具備了不少共性。可是,他們並不是嚴格相同。

Transducer 做爲通常概念(不是 Hickey 的協議規範)來說,對計算機科學的重要分支產生了至關大的影響,包括數據流編程、科學和媒體應用的信號處理、網絡、人工智能等等。隨着咱們開發更好的工具和技術在咱們打應用代碼中闡釋 transducer,它們開始幫助咱們更好的理解各類軟件組合,包括 Web 和 易用應用程序中的用戶界面行爲,而且在未來,還能夠很好地幫助咱們管理複雜的 AR(augmented reality),自主設備和車輛等。

爲了討論起見,當我說 「transducer」 時,我並非指 SICP transducer,儘管若是你已經熟悉了 SICP transducer,可能聽起來像是在講述它們。我也沒有具體提到 Clojure 的 transducer,或者已經成爲 JavaScript 事實標準的 transducer 協議(由 Ramda、Transducer-JS、RxJS等支持...)。我指的是高階 reducer的通常概念 —— 變幻的轉換。

在我看來,transducer 協議的特定細節比 transducer 的通常原理和基本數學特性重要的多,可是若是你想在生產中使用 transducer,爲了知足互操做性,我目前的建議是使用現有的庫來實現 transducer 協議。

我將在這裏描述的 transducer 應該是用僞代碼來演示概念。它們與 transducer 協議不兼容,不該該在生產中使用。若是你想要學習如何使用特定庫的 transducer,請參閱庫文檔。我這樣寫他們是爲了引你入門,讓你看看它們是如何工做的,而不是強迫你同時學習協議。

當咱們完成後,你應該更好的理解 transducer,以及如何在任意的上下文中、與任意的庫一塊兒、在任何支持閉包和高階函數的語言中使用它。

Transducer 的音樂類比

若是你是衆多既是音樂家又是軟件的開發者的那羣人中的一個,用音樂類比可能會頗有用:你能夠想到信號處理裝置等傳感器(如吉他失真踏板,均衡器,音量旋鈕,回聲,混響和音頻混頻器)。

要使用樂器錄製歌曲,咱們須要某種物理傳感器(即麥克風)來說空氣中的聲波轉換爲電線上的電流。而後咱們須要將該線路鏈接到咱們想要使用的信號處理單元。例如,爲電吉他加失真,或者對音軌進行混響。最終,這些不一樣聲音的集合必須聚合在一塊兒,混合來想成最終記錄的單個信號(或者通道集合)。

換句話說,信號流看起來多是這樣。把箭頭想像成傳感器之間的導線:

[ Source ] -> [ Mic ] -> [ Filter ] -> [ Mixer ] -> [ Recording ]
複製代碼

更通常地說,你能夠這麼表達:

[ Enumerator ]->[ Transducer ]->[ Transducer ]->[ Accumulator ]
複製代碼

若是你曾經使用過音樂製做軟件,這可能會讓您想起一系列的音頻效果。當你考慮 transducer 時,這是一個很好的直覺。但他們還能夠更普遍的應用於數字、對象、動畫幀、3D 模型或者任何你能夠在軟件中表示的其餘內容。

屏幕截圖:Renoise 音頻效果通道。

若是你曾在數組上使用 map 方法,你可能會對某些行爲有點像 transducer 的東西熟悉。例如,要將一系列數字加倍:

const double = x => x * 2;  
const arr = [1, 2, 3];

const result = arr.map(double);
複製代碼

在這個示例中,數組是可枚舉對象。map 方法枚舉原始數組,並將其元素傳遞給處理階段 double,它將每一個元素乘以 2,而後將結果累積到一個新數組中。

你甚至能夠像這樣構成效果:

const double = x => x * 2;  
const isEven = x => x % 2 === 0;

const arr = [1, 2, 3, 4, 5, 6];

const result = arr  
  .filter(isEven)  
  .map(double)  
;

console.log(result);  
// [4, 8, 12]
複製代碼

可是,若是你想過濾和加倍的多是無限數字流,好比無人機的遙測數據呢?

數組不能是無限的,而且數組處理過程當中的每一個階段都要求你在單個值能夠流經 pipeline 的下一個階段以前處理整個數組。一樣的問題意味着使用數組方法的合成會下降性能,由於須要建立一個新數組,而且合成中的每一個階段迭代一個新的集合。

想象一下,你有兩段管道,每段都表明一個應用於數據流的轉換,以及一個表示流的字符串。第一個轉換表示 isEven 過濾器,下一個轉換表示 double 映射。爲了從數組中生成單個徹底變換的值,你必須首先經過第一個管道運行整個字符串,從而產生一個全新的過濾數組,而後才能經過 double 管處理單個值。當你最終將第一個值 double,必須等待整個數組加倍才能讀取單個結果。

因此,上面的代碼至關於:

const double = x => x * 2;  
const isEven = x => x % 2 === 0;

const arr = [1, 2, 3, 4, 5, 6];

const tempResult = arr.filter(isEven);  
const result = tempResult.map(double);

console.log(result);  
// [4, 8, 12]
複製代碼

另外一種方法是將值直接從過濾後的輸出流式傳輸到映射轉換,而無需在其間建立和迭代臨時數組。將值一次一個地流過,無需在轉換過程當中對每一個階段迭代相同的集合,而且 transducer 能夠隨時發出中止信號,這意味着你不須要在集合中更深刻地計算每一個階段。須要產生所需的值。

有兩種方法能夠作到這一點:

  • Pull:惰性求值,或者
  • Push:及早求值

Pull API 等待 consumer 請求下一個值。JavaScript 中一個很好的例子是 Iterable。例如生成器函數生成的對象。在經過它在返回的迭代器對象上調用 .next() 來請求下一個值以前,生成器函數什麼事情都不作。

Push API 枚舉源值並儘量快地將它們推送到管中。對於 array.reduce() 調用是 push API 的一個很好的例子。array.reduce() 從數組中一次獲取一個值並將其推送到 reducer,從而在另外一端產生一個新值。對於像 array reduce 這樣的熱切進程,會當即對數組中的每一個元素重複該過程,直處處理完整個數組。在此期間,阻止進一步的程序執行。

Transducers 不關心你是 pull 仍是 push。Transducers 不瞭解他們所採起的數據結構。他們只需調用你傳遞給它們的 reducer 來積累新值。

Transducers 是高階 reducer: Reducer 函數採用 reducer 返回新的 reducer。Rich Hickey 將 transducer 描述爲過程變換,這意味着 transducer 沒有簡單地改變流經的值,而是改變了做用這些值的過程。

簽名應該是這樣的:

reducer = (accumulator, current) => accumulator

transducer = reducer => reducer
複製代碼

或者,拼出來:

transducer = ((accumulator, current) => accumulator) => ((accumulator, current) => accumulator)
複製代碼

通常來講,大多數 transducer 須要部分應用於某些參數來專門化它們。例如,map transducer 可能以下所示:

map = transform => reducer => reducer
複製代碼

或者更具體地說:

map = (a => b) => step => reducer
複製代碼

換句話說,map transducer 採用映射函數(稱爲變換)和 reducer(稱爲 step 函數 ),返回新的 reducer。Step 函數是一個 reducer,當咱們生成一個新值如下一步中添加到累加器時調用。

讓咱們看一些不成熟的例子:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);

const map = f => step =>  
  (a, c) => step(a, f(c));

const filter = predicate => step =>  
  (a, c) => predicate(c) ? step(a, c) : a;

const isEven = n => n % 2 === 0;  
const double = n => n * 2;

const doubleEvens = compose(  
  filter(isEven),  
  map(double)  
);

const arrayConcat = (a, c) => a.concat([c]);

const xform = doubleEvens(arrayConcat);

const result = [1,2,3,4,5,6].reduce(xform, []); // [4, 8, 12]

console.log(result);
複製代碼

這包含了不少內容。讓咱們分解一下。map 將函數應用於某些上下文的值。在這種狀況下,上下文是 transducer pipeline。看起來大體以下:

const map = f => step =>  
  (a, c) => step(a, f(c));
複製代碼

你能夠像這樣使用它:

const double = x => x * 2;

const doubleMap = map(double);

const step = (a, c) => console.log(c);

doubleMap(step)(0, 4);  // 8doubleMap(step)(0, 21); // 42
複製代碼

函數調用末尾的零表示 reducer 的初始值。請注意,step 函數應該是 reducer,但出於演示目的,咱們能夠劫持它並打開控制檯。若是須要對 step 函數的使用方式進行斷言,則能夠在單元測試中使用相同的技巧。

當咱們將它們組合在一塊兒的時候,transducer 將會變得頗有意思。讓咱們實現一個簡化的 filter transducer:

const filter = predicate => step =>  
  (a, c) => predicate(c) ? step(a, c) : a;
複製代碼

Filter 採用 predicate 函數,只傳遞與 predicate 匹配的值。不然,返回的 reducer 返回累加器,不變。

因爲這兩個函數都使用 reducer 而且返回了 reducer,所以咱們可使用簡單的函數組合來組合它們:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);

const isEven = n => n % 2 === 0;  
const double = n => n * 2;

const doubleEvens = compose(  
  filter(isEven),  
  map(double)  
);
複製代碼

這也將返回一個 transducer,須要咱們必須提供最後一個 step 函數,以告訴 transducer 如何累積結果:

const arrayConcat = (a, c) => a.concat([c]);

const xform = doubleEvens(arrayConcat);
複製代碼

此調用結果是標準的 reducer,咱們能夠直接傳遞給任何兼容的 reduce API。第二個參數表示 reduction 的初始值。這種狀況下是一個空數組:

const result = [1,2,3,4,5,6].reduce(xform, []); // [4, 8, 12]
複製代碼

若是這看起來像是作了不少,請記住,已經有函數編程庫提供常見的 transducer 以及諸如 compose 工具程序,他們處理函數組合,並將值轉換爲給定的空值。例如:

const xform = compose(
  map(inc),
  filter(isEven)
);

into([], xform, [1, 2, 3, 4]); // [2, 4]
複製代碼

因爲工具帶中已經有了大多數所需的工具,所以使用 transducer 進行編程很是直觀。

一些支持 transducer 的流行庫包括 Ramda、RxJS 和 Mori。

由上至下組合 transducers

標準函數組成下的 transducer 從上到下/從左到右而非從下到上/從右到左應用。也就是說,使用正常函數組合,compose(f, g) 表示「在 g 以後複合 f」。Transducer 在組成下糾纏其餘 transducer。換言之,transducer 說「我要作個人事情,而後調用管道中下一個 transducer」,這會將執行堆棧內部轉出。

想象一下,你有一沓紙,頂部的一個標有 f,下一個是 g,再下面是 h。對於每張紙,將紙張從紙沓的頂部取出,而後將其放到相鄰的新的一沓紙的頂部。當你這樣作以後,你將得到一個棧,其內容標記爲 h,而後是 g,而後是 f

Transducer 規則

上面的例子不太成熟,由於他們忽略了 transducer 必須遵循的互操做性(interoperability)規則

和軟件中的大部份內容同樣,transducer 和轉換過程須要遵循一些規則:

  1. 初始化:若是沒有初始的累加器值,transducer 必須調用 step 函數來產生有效的初始值進行操做。該值應該表示空狀態。例如,累積數組的累加器應該在沒有參數的狀況下調用其 step 函數時提供空數組。

  2. 提早終止:使用 transducer 的進程必須在收到 reduce 過的累加器值時檢查並中止。此外,對於嵌套 reduce 的 transducer,使用其 step 函數時必須在遇到時檢查並傳遞 reduce 過的值。

  3. 完成(可選):某些轉換過程永遠不會完成,但那些轉換過程應調用完成函數(completion function)來產生最終值/或刷新(flush)狀態,而且狀態 transducer 應提供完成的操做以清除任何積累的資源和可能產生最終的資源值。

初始化

讓咱們回到 map 操做並確保它遵照初始化(空)法則。固然,咱們不須要作任何特殊的事情,只須要使用 step 函數在 pipeline 中傳遞請求來建立默認值:

const map = f => step => (a = step(), c) => (
  step(a, f(c))
);
複製代碼

咱們關心的部分是函數簽名中的 a = step()。若是 a(累加器)沒有值,咱們將經過鏈中的下一個 reducer 來生成它。最終,它將到達 pipeline 的末端,並(希望)爲咱們建立有效的初始值。

記住這條規則:當沒有參數調用時,reducer 的操做應該老是爲 reducer 返回一個有效的初始(空)值。對於任何 reducer 函數,包括 React 或 Redux 的 Reducer,遵照此規則一般是個好主意。

提早終止

能夠向 pipeline 中的其餘 transducer 發出信號,代表咱們已經完成了 reduce,而且他們不該該指望再處理任何值。在看到 reduced 值時,其餘 transducer 能夠決定中止添加到集合,而且轉換過程(由最終 step() 函數控制)能夠決定中止枚舉值。因爲接收到 reduced 值,轉換過程能夠再調用一次:完成上述調用。咱們能夠經過特殊的 reduce 過的累加器來表示這個意圖。

什麼是 reduced 值?它可能像將累加器值包裝在一個名爲 reduced 的特殊類型中同樣簡單。能夠把它想象包裝盒子並用 "Express" 或 "Fragile" 這樣的消息標記盒子。像這樣的元數據包裝器(metadata wrapper)在計算中很常見。例如:http 消息包含在名爲 「request」 或 「response」 的容器中,這些容器類型提供了狀態碼、預期消息長度、受權參數等信息的表頭...

基本上,它是一種發送多條信息的方式,其中只須要一個值。reduced() 類型提高的最小(非標準)示例可能以下所示:

const reduced = v => ({
  get isReduced () {
    return true;
  },
  valueOf: () => v,
  toString: () => `Reduced(${ JSON.stringify(v) })`
});
複製代碼

惟一嚴格要求的部分是:

  • 類型提高:獲取類型內部值的方法(例如,這種狀況下的 reduced 函數)
  • 類型識別:一種測試值以查看它是否爲 reduced 值的方法(例如,isReduced getter)
  • 值提取:一種從值中取出值的方法(例如,valueOf()

此處包含 toString() 以便於調試。它容許您在 console 中同時內省類型和值。

完成

「在完成步驟中,具備刷新狀態(flush state)的 transducer 應該在調用嵌套 transducer 的完成函數以前刷新狀態,除非以前已經看到嵌套步驟中的 reduced 值,在這種狀況下應該丟棄 pending 狀態。」 ~ Clojure transducer 文檔

換句話說,若是在前一個函數表示已完成 reduce 後,有更多狀態須要刷新,則完成函數是處理它的時間。在此階段,你能夠選擇:

  • 再發送一個值(刷新待處理狀態)
  • 丟棄 pending 狀態
  • 執行任何所需的狀態清理

Transducing

能夠轉換大量不一樣類型的數據,可是這個過程能夠推廣:

// 導入標準 curry,或者使用這個魔術:
const curry = (
  f, arr = []
) => (...args) => (
  a => a.length === f.length ?
    f(...a) :
    curry(f, a)
)([...arr, ...args]);

const transduce = curry((step, initial, xform, foldable) =>
  foldable.reduce(xform(step), initial)
);
複製代碼

transduce() 函數採用 step 函數(transducer pipeline 的最後一步),累加器的初始值,transducer 而且可摺疊。可摺疊是提供 .reduce() 方法的任何對象。

經過定義 transduce(),咱們能夠輕鬆建立一個轉換爲數組的函數。首先,咱們須要一個 reduce 數組的 reducer:

const concatArray = (a, c) => a.concat([c]);
複製代碼

如今咱們可使用柯里化過的 transduce() 建立一個轉換爲數組的部分應用程序:

const toArray = transduce(concatArray, []);
複製代碼

使用 toArray() 咱們能夠用一行替代兩行代碼,並在不少其餘狀況下複用它,除此以外:

// 手動 transduce:
const xform = doubleEvens(arrayConcat);
const result = [1,2,3,4,5,6].reduce(xform, []);
// => [4, 8, 12]

// 自動 transduce:
const result2 = toArray(doubleEvens, [1,2,3,4,5,6]);
console.log(result2); // [4, 8, 12]
複製代碼

Transducer 協議

到目前爲止,咱們一直在隱藏幕後一些細節,但如今是時候看看它們了。Transducer 並不是真正的單一函數。他們由 3 種不一樣的函數組成。Clojure 使用函數的 arity 上的模式匹配並在它們之間切換。

在計算機科學中,函數的 arity 是函數所採用參數的數量。在 transducer 的狀況下,reducer 函數有兩個參數,累加器和當前值。在 Clojure 中,二者都是可選的,而且函數的行爲會根據參數是否經過而更改。若是沒有傳遞參數,則函數中該參數的類型是 undefined

JavaScript transducer 協議處理的方式略有不一樣。JavaScript transducer 不是使用函數 arity,而是採用 transducer 並返回 transducer 的函數。Transducer 是一個有三種方法的對象:

  • init 返回累加器的有效初始值(一般,只須要調用下一步 step())。
  • step 應用變換,例如,對於 map(f)step(accumulator, f(current))
  • result 若是在沒有新值的狀況下調用 transducer,它應該處理其完成步驟(一般是 step(a),除非 transducer 是有狀態的)。

注意: JavaScript 中的 transducer 協議分別使用 @@transducer/init@@transducer/step@@transducer/result

有些庫提供一個 tranducer() 工具程序,能夠自動爲你包裝 transducer。

這是一個不那麼不成熟的 transducer 實現:

const map = f => next => transducer({
  init: () => next.init(),
  result: a => next.result(a),
  step: (a, c) => next.step(a, f(c))
});
複製代碼

默認狀況下,大多數 transducer 應該將 init() 調用傳遞給 pipeline 中的下一個 transducer,由於咱們不知道傳輸數據類型,所以咱們沒法爲它生成有效的初始值。

此外,特殊的 reduced 對象使用這些屬性(在 transducer 協議中也命名爲 @@transducer/<name>):

  • reduced 一個布爾值,對於 reduced 的值,該值始終爲 true
  • value reduced 的值。

結論

Transducers 是可組合的高階 reducer,能夠 reduce 任何基礎數據類型。

Transducers 產生的代碼比使用數組進行點連接的效率高几個數量級,而且能夠處理潛在的無需數據集而無需建立中間聚合。

注意:Transducers 並非老是比內置數組方法更快。當數據集很是大(數十萬個項目)或 pipeline 很是大(顯著增長使用方法鏈所需的迭代次數)時,性能優點每每會有所提高。若是你追求性能優點,請記住簡介。

再看看介紹中的例子。你應該能使用示例代碼做爲參考構建 filter()map()toArray(),並使此代碼工做:

const friends = [  
  { id: 1, name: 'Sting', nearMe: true },  
  { id: 2, name: 'Radiohead', nearMe: true },  
  { id: 3, name: 'NIN', nearMe: false },  
  { id: 4, name: 'Echo', nearMe: true },  
  { id: 5, name: 'Zeppelin', nearMe: false }  
];

const isNearMe = ({ nearMe }) => nearMe;

const getName = ({ name }) => name;

const getFriendsNearMe = compose(  
  filter(isNearMe),  
  map(getName)  
);

const results2 = toArray(getFriendsNearMe, friends);
複製代碼

在生產中,你可使用 RamdaRxJStransducers-js 或者 Mori

全部上面的這些都與這裏的示例代碼略有不一樣,但遵循全部相同的基本原則。

一下是 Ramda 的一個例子:

import {  
  compose,  
  filter,  
  map,  
  into  
} from 'ramda';

const isEven = n => n % 2 === 0;  
const double = n => n * 2;

const doubleEvens = compose(  
  filter(isEven),  
  map(double)  
);

const arr = [1, 2, 3, 4, 5, 6];

// into = (structure, transducer, data) => result 
// into transduces the data using the supplied 
// transducer into the structure passed as the 
// first argument. 
const result = into([], doubleEvens, arr);

console.log(result); // [4, 8, 12]
複製代碼

每當咱們須要組個一些操做時,例如 mapfilterchunktake 等,我會深刻 transducer 以優化處理過程並保持代碼的可讀性和清爽。來試試吧。

EricElliottJS.com 上能夠了解到更多

視頻課程和函數式編程已經爲 EricElliottJS.com 的網站成員準備好了。若是你還不是當中的一員,如今就註冊吧


Eric Elliott「編寫 JavaScript 應用」(O’Reilly)以及「跟着 Eric Elliott 學 Javascript」 兩書的做者。他爲許多公司和組織做過貢獻,例如 Adobe SystemsZumba FitnessThe Wall Street JournalESPNBBC 等,也是不少機構的頂級藝術家,包括但不限於 UsherFrank Ocean 以及 Metallica

大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一塊兒。

感謝 JS_Cheerleader

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


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

相關文章
相關標籤/搜索