接上篇 JS 函数式编程的理解(1)
命令式与声明式数据流
假设有三行代码,我需要逐行阅读、理解计算中间态和主流程之间的逻辑关系,才能推导出程序的意图。这样的代码是命令式的。
jsconst filteredArr = arr.filter(biggerThan2); const multipledArr = filteredArr.map(multi2); const sum = multipledArr.reduce(add, 0);
以上三行代码并非严格绑定的,filteredArr和multipledArr是计算sum的两个关键的计算中间态,他们作为引用类型,完全有可能在运行过程中被修改。
与其寄望于禁止打断、绝不篡改的 flag,不如一开始就不把计算中间态暴露出去。
链式调用
借助链式调用可以实现声明式的数据流。
jsconst sum = arr.filter(biggerThan2).map(multi2).reduce(add, 0);
此时,我们只需要观察一个函数调用链。即使不清楚数据如何在传送带上流转,也可以通过函数名去理解程序的意图。
这样的代码,是声明式的。基于此构建出的数据流,就是声明式的数据流。
链式调用的限制
链式调用的本质,是通过在方法中返回对象实例本身的this / 与实例this相同类型的对象,达到多次调用其原型(链)上方法的目的。
要对函数执行链式调用,前提是函数挂载在一个靠谱的宿主 Object 上。
所以对于那些没有挂载在对象上的函数(下称独立函数)而言,需要通过组合来实现声明式数据流。
函数组合
函数组合即反复嵌套各种回调函数。形如:
jsconst add = (num) => num + 1; const divide = (num) => num / 2; const plus = (num) => num * 3; const sum = add(divide(plus(num)));
然而如果函数嵌套较多,将形成回调地狱。
为解决这个问题,我们可以利用reduce。只要想办法让reduce工作流里的计算单元从一个函数转变为 N 个函数,就可以达到函数组合的目的。
具体而言,我们可以将待组合的函数放入一个函数里,然后调用这个函数数组的reduce方法,就可以创建一个由多个函数组成的工作流。
而这就是市面上主流的函数式库实现compose/pipe函数的思路。
使用reduce实现组合
以下实现的是一元入参函数的组合。具体如何让不同入参个数的函数组合起来,需要依靠后面介绍的偏函数和柯里化。
- pipe 正序组合
jsfunction pipe(...funcs) { return (params) => funcs.reduce((input, func) => func(input), param); } const compute = pipe(add, plus, divide); console.log(compute(10));
- compose 倒序组合
只需将reduce替换成reduceRight
jsfunction compose(...funcs) { return (params) => funcs.reduceRight((input, func) => func(input), param); } const compute = compose(add, plus, divide); console.log(compute(10));
偏函数与柯里化
偏函数和柯里化解决的最核心的问题有两个,分别是:
- 函数组合链中的多元参数问题
- 函数逻辑复用的问题
函数组合链中的多元参数对齐问题
函数参数的元数,指的是函数参数的数量。比如,单个入参的函数为一元函数。
之前介绍的函数组合只实现了一元函数的组合,但如果调用链中的函数元数并不相同,则需要进行参数对齐。
任何时候,只要想对函数的入参数量进行改造,必须想到偏函数和柯里化。
对于柯里化来说,不仅函数的元发生了变化,函数的数量也发生了变化(1 个变成 n 个)。
对于偏函数来说,仅有函数的元发生了变化(减少了),函数的数量是不变的。
偏函数
偏函数是将1个n元函数变成一个1个m元函数(m<n)的过程,实现思路是固定一部分函数参数。
以下是一个偏函数的例子。它通过固定multiply函数的第一个入参x,得到了一个一元函数multiply3,使函数入参个数从n变成m(m<n)。
js// 定义一个包装函数,专门用来处理偏函数逻辑 function wrapFunc(func, fixedValue) { // 包装函数的目标输出是一个新的函数 function wrappedFunc(input) { // 这个函数会固定 fixedValue,然后把 input 作为动态参数读取 const newFunc = func(input, fixedValue); return newFunc; } return wrappedFunc; } const multiply3 = wrapFunc(multiply, 3); // 输出6 multiply3(2);
偏函数这种固定参数得到新函数的思路,在缩减函数元数的同时,也可以减少函数调用时的重复传参。
实际上,通用函数为确保其自身的灵活性,往往都具备多元参数的特征。但在一些特定的业务场景下,真正需要动态变化的只是其中的一部分的参数。这时候函数的一部分灵活性对我们来说是多余的,我们反而希望他的功能具体一点。
假设有代码如下,不难看出type和area两个参数是固定的,我们显然可以在generateOrderData的基础上做一个偏函数,固定type和area字段,用于帮助我们避免大量的重复代码。
js// 文件 a const res = generateOrderData("food", "hunan", settelment); // 文件 b const UIData = generateOrderData("food", "hunan", settelment); // 文件 c const result = generateOrderData("food", "hunan", settelment);
柯里化
柯里化是把1个n元函数改造成n个相互嵌套的一元函数的过程。它的特征在于它是嵌套定义的多个函数,也就是套娃。
以下是柯里化的一个例子。
js// 定义高阶函数 curry function curry(addThreeNum) { // 返回一个嵌套了三层的函数 return function addA(a) { // 第一层“记住”参数a return function addB(b) { // 第二层“记住”参数b return function addC(c) { // 第三层直接调用现有函数 addThreeNum return addThreeNum(a, b, c); }; }; }; } // 借助 curry 函数将 add const curriedAddThreeNum = curry(addThreeNum); // 输出6,输出结果符合预期 curriedAddThreeNum(1)(2)(3);
柯里化的实现思路,就是套娃之路。套娃的层数有多深,取决于原函数的参数个数。比如以上代码中,它是三元函数,就相应地需要套三层函数。
如果想实现一个通用的curry,它应该能分析出参数的数量,并动态地根据入参数量自动进行函数嵌套。因此,它需要做如下工作:
- 获取函数入参数量
- 自动分层嵌套函数。有多少参数,就有多少层嵌套
- 在嵌套的最后一层调用回调函数,传入所有入参
获取函数的入参数量,可以通过访问函数的length属性。
而对于函数自动嵌套,基本逻辑就是利用递归,先判断当前层级是否已经到达了嵌套的上限。若达到,则执行回调函数;否则继续嵌套。而如何认定递归边界,可以借助柯里化过程中传入的参数数量。因为柯里化的过程,是层层记忆每个参数的过程。每一层嵌套函数都有它需要记住的参数。若递归到某一层时,发现此时没有需要记忆的参数了,就可以认为已经到达了递归边界。
柯里化具体编码实现如下:
jsconst curry = (func, arity = func.length) => { const generateCurried = (prevArgs) => (nextArg) => { // 统计当前`已记忆`+`未记忆`的参数 const args = [...prevArgs, nextArg]; // 若总参数数量 >= 回调函数元素个数,则认为已经记忆了所有参数 if (args.length >= arity) { // 触碰递归边界,传入所有参数,调用回调函数 return func(...args); } // 未触碰递归边界,则继续递归调用generateCurried本身,创建一层新的嵌套 return generateCurried(args); }; // 起始传参未空数组,表示`目前还未记住任何参数` return generateCurried([]); };
柯里化解决组合链的元数问题
若需要将不同元数的函数组合调用,可以先将所有函数柯里化,重构其传参方式,再逐个传参,传至每个函数只剩下一个待传参数为止,这样就将所有函数变成了一元函数,可供组合函数逐个调用。
jsconst add = (a, b) => a + b; const plus = (a, b, c) => a * b * c; const curriedAdd = curry(add); const curriedPlus = curry(plus); const compute = pipe(curriedAdd(1), curriedPlus(1)(2)); console.log(compute(3));
参考