学习函数式编程,必须掌握很多术语,否则根本看不懂文档。
本文介绍两个基本术语: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]
八、参考链接
- Transducers.js: A JavaScript Library for Transformation of Data
- Transducers Explained: Part 1
- Streaming Logs with Transducers and Ramda
(完)
JerryHong 说:
為什麼能用 reduce 組成 map 功能,就代表 map 是 reduce 的特例?
這麼說來 filter 也是 reduce 的特例囉?
我也能用 forEach 組成 map 的功能,那 map 也是 forEach 的特例嗎?
2017年3月18日 19:20 | # | 引用
jone 说:
阮老师科普系列,mark
2017年3月19日 11:23 | # | 引用
业余草 说:
阮老师科普的东西都很高深,能否普及一下在实际项目中如何使用这些知识点?介绍一些在项目中使用的案例?
2017年3月19日 16:45 | # | 引用
Lutz 说:
无法认同在reduce里面做transform的行为。reduce里面把transform和combine偶合在一起,复用性当然会差咯。本身reduce的目的就是化简。
transduce的例子可以这么写:
const addOne = x => x+1;
const square = x => x*x;
var newArr = arr.map(_.flow(addOne, square));
// 如果想顺手求个和啥的,还可以这么来
var sum = newArr.reduce(_.add);
以上示例中使用的 _ 为lodash
2017年3月19日 20:29 | # | 引用
Tang3 说:
参照 How To Design Programs, 2E 第三部分:
对列表的遍历,用递归来实现。
reduce 是对递归的抽象。
其他如 map、filter 都只是递归的特例。
2017年3月20日 07:50 | # | 引用
毛瑞彬 说:
阮老师你好,我在深圳平安科技听过你的演讲。我想问下这R.pipe()的问题。将多个函数合并成一个函数,从左到右执行。
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.pipe(R.map(plusOne), R.map(square)),
append,
[],
arr
); // [2, 5, 10, 17]
这个却是从右到左,难道这个也要遵循四则运算,先乘除,再加减?不过我交换了plusOne和square的位置,发现却是先加一,再平方。这是什么导致的呢?
2017年3月20日 09:23 | # | 引用
自由风 说:
阮老师好,我运行了修改后的示例:
var arr = [1, 2, 3, 4];
var sum = (a, b) => a + b;
arr.reduce(sum) // 10
结果仍然是10,是不是表示没有init value时,初始值设置为数组的第1个值,并且从第2个值开始递归?
是不是MDN上说了前半部分,而MSDN上说了后半部分?
2017年3月20日 10:15 | # | 引用
阮一峰 说:
@自由风:
是的。
@毛瑞彬:
谢谢指出,我疏忽了。
R.transduce 的实现很奇怪,两个变形都是 R.map 时,总是从右到左执行。我把例子改一下。
2017年3月20日 13:57 | # | 引用
毛瑞彬 说:
@阮一峰:
好的,谢谢。
2017年3月20日 17:22 | # | 引用
madaoNo 说:
如果省略该参数,那么初始值默认是数组的第一个成员。
这句应该是默认是0吧,不然那个示例的结果就应该是11了
2017年3月20日 18:04 | # | 引用
cshenger 说:
一脸懵逼的进来,又一脸懵逼的出去
2017年3月24日 18:21 | # | 引用
eeeqxxtg 说:
阮老师,你主页home.html 有一张大图片,有阳光,有很高大的树。刷新了一天页面了,再也找不到了。怎么办?
2017年3月28日 21:05 | # | 引用
shinefine 说:
var plusOne = x => x + 1;
var square = x => x * x;
var res =_.transduce(
//_.map(_.compose(plusOne, square)), // ===> [2,5,10,17]
//_.map(_.compose(square,plusOne)), // ===> [4,9,16,25]
//_.map(_.pipe(square,plusOne)), // ===> [2,5,10,17]
//_.map(_.pipe(plusOne,square)), // ===> [4,9,16,25]
//_.pipe(_.map(plusOne), _.map(square)), // ===> [2,5,10,17]
//_.compose(_.map(square),_.map(plusOne)),// ===> [2,5,10,17]
_.compose(_.map(plusOne),_.map(square)),// ===> [4,9,16,25]
append,
[],
arr
);
2017年5月26日 09:53 | # | 引用
搬运工 说:
这个例子感觉不是很好, 把原本只需要循环一次的操作分为多次, 如果只是为了复用,可能还要考虑好场景, 到最后不得不混合使用, 那么更混乱的感觉
2017年5月29日 09:58 | # | 引用
loc 说:
阮老师,R.map(plusOne)返回的是个函数吧
2017年6月25日 17:08 | # | 引用
love.feng 说:
个人认为 map() 和 reduce() 的概念,最初是 Google 用于解决大数据搜索而提出来的。Hadoop 提供了具体的开源实现。其核心就是分布式计算(Map)+ 对计算的结果进行合并(reduce)。
试想:在亿万的网页中搜索一个字符,google只用 0.15 秒左右。如果不是分布式计算,这是无法达到的。举个例子:课堂上,老师说考了 91 分的举手,1 秒钟不到,老师就会知道答案。如果让老师自己去检查每个学生的分数,然后统计,可能需要20分钟才能完成。这就是分布式计算 —— 把工作交给每个学生去完成 -- Map 的过程,然后老师自己统计 -- Reduce 结果。离开了分布式,Map 和 Reduce 是没有实际意义的。里面设计到并发和同步的东西,已经超出单纯的
单线程编程问题了。
2017年7月 8日 03:17 | # | 引用
llllll 说:
累积器会有变化吗?怎么感觉transduce用来用去也只是实现了map而已。
2017年8月 9日 10:16 | # | 引用
球场铁王 说:
如果省略该参数,那么初始值默认是数组的第一个成员。这句话是不是不对?
2017年12月19日 16:59 | # | 引用
limux 说:
reduce对数组arr的每个成员执行sum函数。sum的参数a是累积变量,参数b是当前的数组成员。每次执行时,b会加到a,最后输出a。
这样的解释对吗,好像不通啊。
reduce对数组arr的每个成员执行sum函数。第一个参数arr是一个数组,第二个参数a是累积变量。每次执行时,取出参数数组的下一个成员与累积变量做sum运算,结果保存在累积变量,然后再执行下一次运算,直到arr为空。
2018年6月30日 21:33 | # | 引用