[译] 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索