JavaScript 进阶实战课
石川
JavaScript Patterns and Anti-Patterns 等开源项目创建者,O'Reilly 技术评审
15066 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 47 讲
开篇词 (1讲)
JavaScript 进阶实战课
15
15
1.0x
00:00/00:00
登录|注册

04 | 如何通过组合、管道和reducer让函数抽象化?

你好,我是石川。
上节课我们讲到,通过部分应用和柯里化,我们做到了从抽象到具象化。那么,今天我们要讲的组合和管道,就是反过来帮助我们把函数从具象化变到抽象化的过程。它相当于是系统化地把不同的组件函数,封装在了只有一个入口和出口的函数当中。
其实,我们在上节课讲处理函数输入问题的时候,在介绍 unary 的相关例子中,已经看到了组合的雏形。在函数式编程里,组合(Composition)的概念就是把组件函数组合起来,形成一个新的函数。
我们可以先来看个简单的组合函数例子,比如要创建一个“判断一个数是否为奇数”的 isOdd 函数,可以先写一个“计算目标数值除以 2 的余数”的函数,然后再写一个“看结果是不是等于 1”的函数。这样,isOdd 函数就是建立在两个组件函数的基础上。
var isOdd = compose(equalsToOne, remainderOfTwo);
不过,你会看到这个组合的顺序是反直觉的,因为如果按照正常的顺序,应该是先把 remainderByTwo 放在前面来计算余数,然后再执行后面的 equalsToOne, 看结果是不是等于 1。
那么,这里为什么会有一个反直觉的设计呢?今天这节课,我们就通过回答这个问题,来看看组合和管道要如何做到抽象化,而 reducer 又是如何在一系列的操作中,提高针对值的处理性能的。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入浅出地介绍了函数抽象化的概念和实现方式,通过组合、管道和reducer让函数抽象化。作者首先介绍了组合的概念,即将组件函数组合起来形成一个新的函数,并通过示例展示了如何创建一个“判断一个数是否为奇数”的isOdd函数。接着,介绍了Point-Free和函数组件的概念,以及独立的组合函数的实现方式。然后,讨论了管道的概念,即一个函数的输出作为下一个函数的输入,并举例说明了在Unix/Linux和JavaScript中的应用。最后,提出了用reverseArgs函数来实现管道,并展示了如何通过管道来实现isOdd函数。此外,文章还介绍了转导(transducing)的概念,以及reducer的作用和原理。通过使用transducer和reducer,可以优化一系列map、filter、reduce操作,使得输入数组只被处理一次并直接产生输出结果,而不需要创建任何中间数组。总而言之,本文通过实际例子和概念解释,为想深入了解函数式编程的读者提供了很高的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《JavaScript 进阶实战课》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(16)

  • 最新
  • 精选
  • 靜下心來重看一次, 好像看懂了, 以下是我對於 composeReducer 的實作: ``` const { filterTR, mapTR, composeReducer } = (() => { function applyTypeForFunction(fn, type) { fn.type = type; return fn; } function filterTR(fn) { return applyTypeForFunction(fn, "filter"); } function mapTR(fn) { return applyTypeForFunction(fn, "map"); } function composeReducer(inputArray, fnArray) { return inputArray.reduce((sum, element) => { let tmpVal = element; let tmpFn; for (let i = 0; i < fnArray.length; i++) { tmpFn = fnArray[i]; if (tmpFn.type === "filter" && tmpFn(tmpVal) === false) { console.log(`failed to pass filter: ${element} `); return sum; } if (tmpFn.type === "map") { tmpVal = tmpFn(tmpVal); } } console.log(`${element} pass, result = ${tmpVal}`); sum.push(tmpVal); return sum; }, []); } return { filterTR, mapTR, composeReducer }; })(); const isEven = (v) => v % 2 === 0; const passSixty = (v) => v > 60; const double = (v) => 2 * v; const addFive = (v) => v + 5; var oldArray = [36, 29, 18, 7, 46, 53]; var newArray = composeReducer(oldArray, [ filterTR(isEven), mapTR(double), filterTR(passSixty), mapTR(addFive) ]); console.log(newArray); ```

    作者回复: 帥!!

    2022-09-27归属地:北京
    2
    9
  • 卡卡
    我的理解是:reduce可以对原集合的每个元素使用map回调函数进行映射或者使用filter回调函数进行过滤,然后将新值放入新的集合 mapReduce的实现: Array.prototype.mapReduce = function (cb, initValue) { return this.reduce(function (mappedArray, curValue, curIndex, array) { mappedArray[curIndex] = cb.call(initValue, curValue, curIndex, array); return mappedArray; }, []); }; filterReduce的实现: Array.prototype.filterReduce = function (cb, initValue) { return this.reduce(function (mappedArray, curValue, curIndex, array) { if (cb.call(initValue, curValue, curIndex, array)) { mappedArray.push(curValue); } return mappedArray; }, []); };

    作者回复: 是的,这里利用了reduce的第二个参数的初始值可以是一个“空数组”,映射或过滤后,放入“新数组”。

    2022-09-27归属地:北京
    5
  • 雨中送陈萍萍
    看了下阮老师对PointFree风格的描述(https://www.ruanyifeng.com/blog/2017/03/pointfree.html),可以直接简单理解成对多个运算过程的合成,不涉及到具体值的处理,所以compose和pipeline就是这种风格.

    作者回复: 对,compose就是天然的pointfree。

    2022-11-09归属地:北京
    2
    2
  • I keep my ideals💤
    想请教一下老师compose组合的新函数里面如果有某一个是异步函数,或者没有返回值的情况下该怎么处理呢。还有多条件分支的情况下又该如何处理呢

    作者回复: 1. 异步可以考虑结合CPS的promise/then,或 async/await来解决。 2. 没有返回值,可以考虑用Just和Nothing组成Maybe monad。 3. 多条件分支的情况下可以考虑在Maybe monad中创建orElse的方法。

    2022-09-28归属地:北京
    2
  • 程序员一土
    业务上函数拆这么细会被打吧

    作者回复: 在采用某种风格的时候,还是要掌握一个度

    2022-12-02归属地:北京
    1
  • 深山何处钟
    请问老师,compose那个函数,直接fns后不接reverse,是不是就是pipe的效果呢?

    作者回复: 如果不用reverseArgs,pipe是可以简单理解成这样的: var pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);

    2022-09-30归属地:北京
    1
  • 天择
    最近两篇文章的知识常在框架和库的代码里面见到,也会给我们阅读源码提供帮助。 具体和抽象都是为使用目标服务的,不管是柯里化还是函数组件,都是给使用者提供某种场景下的便利性,只不过有的需要具体的手段,有的需要抽象的手段。

    作者回复: 嗯嗯,是这样的,无论具象还是抽象,目的都是学以致用

    2022-09-27归属地:北京
    1
  • 天择
    point free的理解:把参数去掉,是指参数的含义已经体现在函数声明(名字)里面了,比如equalsToOne,那就是说传入的值是否等于1,如果是equalsToA,那么这个A就得传为参数,加上要比较的x就是两个参数了。这就是所谓“暴露给使用者的就是功能本身”。

    作者回复: 是这样的

    2022-09-27归属地:北京
    1
  • WGH丶
    function compose(...fns) { return fns.reverse().reduce( function reducer(fn1,fn2){ return function composed(...args){ return fn2( fn1( ...args ) ); }; } ); } 老师好,请教下:这里如果不用reverse,且交换下fn1,fn2的执行顺序能达到同样的效果。之所以使用reverse,是为了保证fn1先于fn2执行吗,还是别的原因?

    作者回复: 这样做的目的就是从右往左reduce哈,另外一种方式就是用reduceRight,这样就不需要reverse了。

    2022-12-18归属地:海南
  • 23568
    var oldArray = [36, 29, 18, 7, 46, 53]; var newArray = composeReducer(oldArray, [ filterTR(isEven), mapTR(double), filterTR(passSixty), mapTR(addfive), ]); console.log (newArray); // 返回:[77,97] “在这个例子里,我们对一组数组进行了一系列的操作,先是筛选出奇数,再乘以二,之后筛出大于六十的值,最后加上五。在这个过程中,会不断生成中间数组。” 看返回结果是 [77, 97] ,这里好像筛选出来的是奇数吧老师

    作者回复: 一开始筛的是偶数: 第1次筛出来的偶数是 [36, 18, 46]; 第2次乘以2的数值是 [72, 36, 92]; 第3次筛大于60的是 [72, 92]; 第4次加上5的数值是 [77, 97]。

    2022-11-09归属地:海南
收起评论
显示
设置
留言
16
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部