文章摘自互联网
学习函数式编程,必须掌握很多术语,否则根本看不懂文档。
本文介绍两个基本术语:reduce和transduce。它们非常重要,也非常有用。
一、reduce 的用法
reduce是一种数组运算,通常用于将数组的所有成员”累积”为一个值。
1.2. var arr = [1, 2, 3, 4];3.4. var sum = (a, b) => a + b;5.6. arr.reduce(sum, 0) // 107.
上面代码中,reduce对数组arr的每个成员执行sum函数。sum的参数a是累积变量,参数b是当前的数组成员。每次执行时,b会加到a,最后输出a。
累积变量必须有一个初始值,上例是reduce函数的第二个参数0。如果省略该参数,那么初始值默认是数组的第一个成员。
1.2. var arr = [1, 2, 3, 4];3.4. var sum = function (a, b) {5. console.log(a, b);6. return a + b;7. };8.9. arr.reduce(sum) // => 1010. // 1 211. // 3 312. // 6 413.
上面代码中,reduce方法省略了初始值。通过sum函数里面的打印语句,可以看到累积变量每一次的变化。
总之,reduce方法提供了一种遍历手段,对数组所有成员进行”累积”处理。
二、map 是 reduce 的特例
累积变量的初始值也可以是一个数组。
1.2. var arr = [1, 2, 3, 4];3.4. var handler = function (newArr, x) {5. newArr.push(x + 1);6. return newArr;7. };8.9. arr.reduce(handler, [])10. // [2, 3, 4, 5]11.
上面代码中,累积变量的初始值是一个空数组,结果reduce就返回了一个新数组,等同于执行map方法,对原数组进行一次”变形”。下面是使用map改写上面的例子。
1.2. var arr = [1, 2, 3, 4];3. var plusOne = x => x + 1;4. arr.map(plusOne) // [2, 3, 4, 5]5.
事实上,所有的map方法都可以基于reduce实现。
1.2. function map(f, arr) {3. return arr.reduce(function(result, x) {4. result.push(f(x));5. return result;6. }, []);7. }8.
三、reduce的本质
本质上,reduce是三种运算的合成。
- 遍历
- 变形
- 累积
还是来看上面的例子。
1.2. var arr = [1, 2, 3, 4];3. var handler = function (newArr, x) {4. newArr.push(x + 1);5. return newArr;6. };7.8. arr.reduce(handler, [])9. // [2, 3, 4, 5]10.
上面代码中,首先,reduce遍历了原数组,这是它能够取代map方法的根本原因;其次,reduce对原数组的每个成员进行了”变形”(上例是加1);最后,才是把它们累积起来(上例是push方法)。
四、 transduce 的含义
reduce包含了三种运算,因此非常有用。但也带来了一个问题:代码的复用性不高。在reduce里面,变形和累积是耦合的,不太容易拆分。
每次使用reduce,开发者往往都要从头写代码,重复实现很多基本功能,很难复用别人的代码。
1.2. var handler = function (newArr, x) {3. newArr.push(x + 1);4. return newArr;5. };6.
上面的这个处理函数,就很难用在其他场合。
有没有解决方法呢?回答是有的,就是把”变形”和”累积”这两种运算分开。如果reduce允许变形运算和累积运算分开,那么代码的复用性就会大大增加。这就是transduce方法的由来。transduce这个名字来自 transform(变形)和 reduce 这两个单词的合成。它其实就是reduce方法的一种不那么耦合的写法。
1.2. // 变形运算3. var plusOne = x => x + 1;4.5. // 累积运算6. var append = function (newArr, x) {7. newArr.push(x);8. return newArr;9. };10.11. R.transduce(R.map(plusOne), append, [], arr);12. // [2, 3, 4, 5]13.
上面代码中,plusOne是变形操作,append是累积操作。我使用了 Ramda 函数库的transduce实现。可以看到,transduce就是将变形和累积从reduce拆分出来,其他并无不同。
五、transduce 的用法
transduce最大的好处,就是代码复用更容易。
1.2. var arr = [1, 2, 3, 4];3. var append = function (newArr, x) {4. newArr.push(x);5. return newArr;6. };7.8. // 示例一9. var plusOne = x => x + 1;10. var square = x => x * x;11.12. R.transduce(13. R.map(R.pipe(plusOne, square)),14. append,15. [],16. arr17. ); // [4, 9, 16, 25]18.19. // 示例二20. var isOdd = x => x % 2 === 1;21.22. R.transduce(23. R.pipe(R.filter(isOdd), R.map(square)),24. append,25. [],26. arr27. ); // [1, 9]28.
上面代码中,示例一是两个变形操作的合成,示例二是过滤操作与变形操作的合成。这两个例子都使用了 Pointfree 风格。
可以看到,transduce非常有利于代码的复用,可以将一系列简单的、可复用的函数合成为复杂操作。作为练习,有兴趣的读者可以试试,使用reduce方法完成上面两个示例。你会发现,代码的复杂度和行数大大增加。
六、Transformer 对象
transduce函数的第一个参数是一个对象,称为 Transformer 对象(变形器)。前面例子中,R.map(plusOne)返回的就是一个 Transformer 对象。
事实上,任何一个对象只要遵守 Transformer 协议,就是 Transformer 对象。
1.2. var Map = function(f, xf) {3. return {4. "@@transducer/init": function() {5. return xf["@@transducer/init"]();6. },7. "@@transducer/result": function(result) {8. return xf["@@transducer/result"](result);9. },10. "@@transducer/step": function(result, input) {11. return xf["@@transducer/step"](result, f(input));12. }13. };14. };15.
上面代码中,Map函数返回的就是一个 Transformer 对象。它必须具有以下三个属性。
- @@transducer/step:执行变形操作
- @@transducer/init:返回初始值
- @@transducer/result:返回变形后的最终值
所有符合这个协议的对象,都可以与其他 Transformer 对象合成,充当transduce函数的第一个参数。
因此,transduce函数的参数类型如下。
1.2. transduce(3. 变形器 : Object,4. 累积器 : Function,5. 初始值 : Any,6. 原始数组 : Array7. )8.
