函数组合

  • 纯函数和柯里化很容易就写出洋葱代码:形如h(g(f(x)))这样的代码

    • 比如说:获取数组的最后一个元素再转换成大写字母, _.toUpper(_.first(_.reverse(array)))
  • 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

下面这张图表示程序中使用函数处理数据的过程,给 fn 函数输入参数 a,返回结果 b。可以想想 a 数据通过一个管道得到了 b 数据:

当 fn 函数比较复杂的时候,我们可以把函数 fn 拆分成多个小函数,此时多了中间运算过程产生的 m 和 n。下面这张图中可以想象成把 fn 这个管道拆分成了3个管道 f1, f2, f3,数据 a 通过管道 f3 得到结果 m,m再通过管道 f2 得到结果 n,n 通过管道 f1 得到最终结果 b:

// 伪代码
fn = compose(f1, f2, f3)
b = fn(a)
  • 函数组合(compose):如果一个函数要经过多个函数处理之后才能得到返回值,可以将中间过程组合成一个函数来处理

    • 函数就像是数据的传输管道,函数组合就是把这些管道组合到一起,让数据穿过多个管道得到一个最终处理后的值
    • 函数组合的执行方向是从右到左的
// composeExample.js
function compose(f, g) {
  return function (value) {
    return f(g(value));
  };
}

function reverse(array) {
  return array.reverse();
}

function getFirstOne(array) {
  return array[0];
}

//从右到左的实行顺序
// 先取反,再取第一位
console.log(compose(getFirstOne, reverse)(["1", "2", "3"]));

Lodash中的函数组合

  • lodash中的组合函数:flow()和flowRight(),他们都可以组合多个函数
  • flow是从左到右执行
  • flowRight是从右到左执行,使用的更多一点
//_.flowRight()
const _ = require("lodash");
const { toUpper, flowRight } = _;

const reverse = (arr) => arr.reverse();
const getFirst = (arr) => arr[0];

const compose = flowRight(toUpper, getFirst, reverse);

console.log(compose(["a", "b", "c"]));

组合函数的原理

// composeMock
const composeMock =
  (...args) =>
  (value) =>
    args.reverse().reduce((acc, fn) => fn(acc), value);

const composeMockReal = composeMock(toUpper, getFirst, reverse);

console.log(composeMockReal(["a", "b", "c"]));

该函数接收任意数量的函数作为参数,并返回一个新的函数。这个新函数接收一个值作为输入,并将该值作为第一个函数的输入,然后依次执行传入的每个函数,并将上一个函数的输出作为下一个函数的输入,最终返回最后一个函数的输出结果。

具体来说,函数首先使用 ES6 的 rest 参数语法 ...args 来获取所有传入的函数参数,并将它们打包成一个数组 args。接着它返回了一个匿名函数,该函数接收一个参数 value 作为输入。

在这个匿名函数内部,首先调用args.reverse() 方法,将传入的函数数组反转,从而实现了函数的倒序执行。然后使用reduce() 方法依次遍历这些函数,并将上一个函数的输出作为下一个函数的输入,最终返回最后一个函数的输出结果。

需要注意的是,在第一次遍历时,reduce() 方法会将参数 value 作为累加器 acc 的初始值,同时将第一个函数作为初始的回调函数。然后,reduce() 方法依次对每个函数调用回调函数,并将上一个函数的输出结果作为下一个函数的输入值进行处理。最终,函数的结果将会被返回。

总之,composeMock 函数利用了高阶函数和函数式编程思想中的函数组合概念,能够轻松地将多个函数按照特定顺序组合起来,并且方便地将其作为一个新的可复用的函数进行使用。

结合律

函数的组合要满足结合律(associativity)
我们既可以把g和h组合,也可以把f和g组合
比如说

let fn = compose(f,g,h)
let associativity = compose(compose(f,g),h) === compose(f,compose(g,h))
// true

如何调试

以下我将提供一份有错误的代码:

// 函数组合调试
// BOCCHI THE ROCK -> bocchi-the-rock

const _ = require("lodash");
const { flowRight, split: lodashSplit, curry, toLower, join: lodashJoin } = _;

const split = curry((sep, str) => lodashSplit(str, sep));

const join = curry((sep, arr) => lodashJoin(arr, sep));

const log = (v) => {
  console.log(v);
  return v;
};

const fn = flowRight(join("-"), log, toLower, log, split(" "));

console.log(fn("BOCCHI THE ROCK"));

根据代码我们不难知道,我们想得到bocchi-the-rock这个字段,但是,结果是:

原因,我们需要在组合函数中一个个进行查询,所以我在代码中加入了log箭头函数,并将他合并到了组合函数中,在每一步执行之后我可以查看数据变成了什么样子:

很显然,问题出在了toLower上,他返回了一个字符串,而非我们预期的小写字符串的数组,所以我们可以根据map进行更改:

// 函数组合调试
// BOCCHI THE ROCK -> bocchi-the-rock

const _ = require("lodash");
const {
  map: lodashMap,
  flowRight,
  split: lodashSplit,
  curry,
  toLower,
  join: lodashJoin,
} = _;

const split = curry((sep, str) => lodashSplit(str, sep));

const join = curry((sep, arr) => lodashJoin(arr, sep));

const map = curry((fn, arr) => lodashMap(arr, fn));

const log = (v) => {
  console.log(v);
  return v;
};

const fn = flowRight(join("-"), log, map(toLower), log, split(" "));

console.log(fn("BOCCHI THE ROCK"));

lodash/fp

  • lodash中的fp模块提供了实用的对函数式编程友好的方法
  • 提供了不可变自动柯里化,函数优先,数据在后的方法
// lodash 模块
const _ = require("lodash");
_.map(["a", "b", "c"], _.toUpper);
// => ['A', 'B', 'C']
_.map(["a", "b", "c"]);
// => ['a', 'b', 'c']
_.split("Hello World", " ");

// lodash/fp 模块
const fp = require("lodash/fp");
fp.map(fp.toUpper, ["a", "b", "c"]);
fp.map(fp.toUpper)(["a", "b", "c"]);
fp.split(" ", "Hello World");
fp.split(" ")("Hello World");

所以上面我们的函数组合例子可以优化成:

// 函数组合调试: fp
// BOCCHI THE ROCK -> bocchi-the-rock

const fp = require("lodash/fp");

const { flowRight, join, map, split, toLower } = fp;

const fn = flowRight(join("-"), map(toLower), split(" "));

console.log(fn("BOCCHI THE ROCK"));

lodash-map的一些小问题

const _ = require("lodash");

console.log(_.map(["23", "8", "10"], parseInt));

// [23, NaN, 2]

为什么会是这样的结果?
llodash map的函数接受了三个参数

也就是说:

console.log(_.map(["23", "8", "10"], parseInt));
// parseInt("23", 0, arr);
// parseInt("8", 1, arr);
// parseInt("10", 2, arr);

一共进行了以上三步
而我们再看看parseInt的参数

radix:换成几进制的数字
所以,23换成了10进制,1不支持进制,也就是NaN,2进制的10就是2

如何修正?自己定义一个parseInt,或者使用fp模块:

const fp = require("lodash/fp");

console.log(fp.map(parseInt, ["23", "8", "10"]));

fp里的map接受的函数的参数不一样,只有一个参数,所以能够正常处理

lodash中的map是三个参数,元素,索引,数组,而fp的map只有一个,就是当前处理的元素

最后修改:2024 年 03 月 21 日
收款不要了,给孩子补充点点赞数吧