组合和管道
写在前面
本文的目的在于使用函数式编程解决常见问题,本文仅代表个人对函数式编程的一些粗浅认识,仅供参考,如有错漏,欢迎指出。
函数式编程理念
函数式编程的两个原则:
每个程序只做好一件事,与其在旧程序中添加新属性,不如重新构建程序。
每个程序的输出应该是另一个程序的输入
组合函数
组合函数的优势在于:利用基础函数,不用重新创建新的函数就能解决问题。
如何把两个或多个函数组合起来?我们先从简单的入手
组合两个函数
const compose = (a, b) => (c) => a(b(c));
// 对输入值转float然后四舍五入
const va = parseFloat("3.56");
const vb = Math.round(va);
const composeFn = compose(Math.round, parseFloat);
console.log(va); // 3.56
console.log(vb); // 4
console.log(composeFn("3.56"));
console.log(compose(Math.round, parseFloat)("3.56")); // 4
说明一下:parseFloat, Math.round分别是两个函数,且恰好是两个单参数函数,可以通过组合的方式调用
需要注意的两点:
1.传入的函数的执行顺序是从右往左的
2.在调用composeFn前,parseFloat, Math.round是不会被调用的
引入curry和partial
上面只能应用于单参数函数,如何应用多参数函数?有什么应对措施吗?有,我们可以引入curry函数来把多参数函数转为单参数函数
举个栗子
// 将字符串转成数字,再乘以20
const fn = (x, y) => x * y;
const va = parseInt("5.6", 10); // 5
const vb = fn(5, 20); // 100
const partialFn = _.partial(parseInt, _, 10);
const curryFn = _.curry(fn)(20);
const composeFn = compose(curryFn, partialFn);
console.log(composeFn("5.6")); // 100
这里引入了lodash的curry/partial函数
partialFn是偏函数,单参数参数
curryFn是固定一个参数的函数,单参数函数
这样一来可以扩展到组合两个任意纯函数
组合多个函数
上面的函数只能组合两个函数,下面来重写组合函数
// const compose =
// (...fns) =>
// (value) =>
// _.reduce(fns.reverse(), (acc, fn) => fn(acc), value);
// 为了方便理解写成以下形式
const compose = (...fns) => {
return (value) => {
const func = (acc, fn) => fn(acc);
return _.reduce(fns.reverse(), func, value);
};
};
简单说明一下:
fns存储所有要组合的函数,fns之所以执行reverse()反转,是因为组合函数是从右往左执行
func表示接受参数并执行方法
value是组合函数的参数,表示初始值
reduce遍历每个函数,把初始值传入执行后的值作为下一个函数的传入的值
下面来运用组合函数解决问题
// 计算文字的单词数量
const split = (str) => str.split(" ");
const count = (word) => word.length;
const wordCount = compose(count, _.map, split);
console.log(wordCount("Thank you for reading my blog")); // 6
这里组合了三个函数,分别是split,_.map,count,先分割字符串为数组,遍历数组,计算每个数组项的长度,每个函数都极其简单,都组合起来就能实现复杂的方法。
管道
管道与组合实际上是一样的,区别在于数据流的不同,组合是从右往左,管道是从左往右,管道函数的定义如下:
const pipe = (...fns) => {
return (value) => {
const func = (acc, fn) => fn(acc);
return _.reduce(fns, func, value);
};
};
这里没有fns.reverse(),顺序是从左往右的,比较符合实际开发的应用
// 计算文字的单词数量
const split = (str) => str.split(" ");
const count = (word) => word.length;
const wordCount = pipe(split, _.map, count);
console.log(wordCount("Thank you for reading my blog")); // 6
组合的优势
组合函数其实是符合结合律的,如:
x * (y * z) = (x * y)* z
所以可以对其中的几个函数进行组合,得到的结果一致
const mapCount = pipe(_.map, count);
const wordCoun2 = pipe(split, mapCount);
console.log(wordCoun2("Thank you for reading my blog")); // 6
const splitMap = pipe(split, _.map);
const wordCoun3 = pipe(splitMap, count);
console.log(wordCoun3("Thank you for reading my blog")); // 6
identity函数
最后再讲一下组合函数的调试技巧,有这样的函数:
const identity = (it) => {
console.log(it);
return it;
};
这个函数接受参数,把这个参数打印后原样返回,因此,把这个函数插入到组合函数的任意两个函数中间,能检查函数是否正确执行,对调试很有帮助,就像这样:
const words = "Thank you for reading my blog";
pipe(identity, split, _.map, count)(words); // Thank you for reading my blog
pipe(split, identity, _.map, count)(words); // [ 'Thank', 'you', 'for', 'reading', 'my', 'blog' ]
pipe(split, _.map, identity, count)(words); // [ 'Thank', 'you', 'for', 'reading', 'my', 'blog' ]
pipe(split, _.map, count, identity)(words); // 6
总结
组合是函数式编程的一种特性,通过定义良好的小函数组合出复杂的函数,要比直接对原输入编写复杂函数省力许多。组合与管道的不同在于数据流方向不同。identity函数的原样返回可以用来对函数组合进行调试。