函数式编程之组合与管道
组合 compose
compose()
会把你需要函数结合在一起,像一根管道一样,函数就是这跟管道的节点。你只需要从管道的开始端注入数据,管道会把你的数据处理成你想要的数据然后返回给你。
compose()
函数的作用就是组合函数的,将函数串联起来执行,将多个函数组合起来,一个函数的输出结果是另一个函数的输入参数,一旦第一个函数开始执行,就会像多米诺骨牌一样推导执行了。
类似于,数学中的复合函数。函数 f 和 g 的组合可以被定义为 f(g(x)),从内到外(从右到左)求值。
1. 特点
compose()
组合函数的参数是函数,返回的也是一个函数;- 因为除了第一个函数的接受参数,其他函数的接受参数都是上一个函数的返回值,所以初始函数的参数是多元的,而其他函数的接受值是一元的;
compose()
函数可以接受任意的参数,所有的参数都是函数,且执行方向是自右向左的,初始函数一定放到参数的最右面;
2. 举例说明
比如有这样的需求,要输入一个名字,这个名字有由 firstName,lastName
组合而成。如输入 jack,smith 我们就要打印出来,‘HELLO,JACK SMITH’ 。
我们考虑用函数组合的方法来解决这个问题,需要两个函数 greeting()
, toUpper()
.
const greeting = (x, y) => {
return `HELLO, ${x} ${y}`;
}
const toUpperCase = (x) => {
return x.toUpperCase();
}
let fn = compose(toUpperCase, greeting);
fn('qqq', 'lll'); // "HELLO, QQQ LLL"
fn('jack','smith'); // "HELLO, JACK SMITH"
使用组合函数,其执行过程是:初始函数为 greeting()
,执行结果作为参数传递给toUpper()
,再执行 toUpper()
,得出最后的结果。
使用组合函数有一个好处就是,如果我需要对这个结果再加一个处理函数,trim
()去除这个字符串中的空格。不需要修改 fn, 只需要再调用一次 compose() 函数即可。(拿上次运行的结果来作为本次处理函数的参数,所以只需要将上次的结果函数作为初始函数即可)
const trimR = (x) => {
return x.replace(/\s/g, '');
}
let newFn = compose(trimR, fn);
newFn('qqq', 'lll'); // "HELLO,QQQLLL"
这里compose(trimR, fn)
相当于 compose(trimR, compose(toUpperCase, greeting))
。
利用 compose() 将两个函数组合成一个函数,让代码从右向左运行,而不是由内而外运行,可读性大大提升。这便是函数组合。
但是现在的 compose 函数也只是能支持两个参数,如果有更多的步骤呢?我们岂不是要这样做:
compose(d, compose(c, compose(b, a)));
为什么我们不写一个帅气的 compose 函数支持传入多个函数呢?这样就变成了:
compose(d, c, b, a);
3. 好处
- 专注于编写基本函数。将多个单一功能的纯函数进行组合。
先定义做什么,然后在传入数据,就可以得到想要的结果。
4. 实现
function compose() {
let args = arguments;
let start = args.length - 1;
let result;
let i;
return function () {
result = args[start].apply(this, arguments);
i = start;
while(i--) {
result = args[i].call(this, result); // result =
}
return result;
}
}
compose(greeting)('aa','bb'); // "HELLO, aa bb"
思路: 先把传入的函数都缓存起来,然后在传入数据的时候,再挨个的使用apply执行函数, 上一个函数的输出数据,作为下一个函数的输入数据。
compose遵循的是从右向左运行,而不是由内而外运行。也就是说compose是从最后一个函数开始执行。
5. 结合柯里化和组合 Curry + Compose
我们知道,compose()
组合函数除了初始函数,仅当函数接收一个参数时,才能将函数组合,那多参的函数该如何组合呢?咦...柯里化和偏函数不就是用来分割参数的嘛?首先看个概念:
- pointfree
pointfree 指的是函数无须提及将要操作的数据是什么样的。
// 需求:输入 'kevin',返回 'HELLO, KEVIN'。
// 非 pointfree,因为提到了数据:name
var greet = function(name) {
return ('hello ' + name).toUpperCase();
}
// pointfree
// 先定义基本运算,这些可以封装起来复用
var toUpperCase = function(x) { return x.toUpperCase(); };
var hello = function(x) { return 'HELLO, ' + x; };
var greet = compose(hello, toUpperCase);
greet('kevin');
再看一个复杂的需求:
// 需求:输入 'kevin daisy kelly',返回 'K.D.K'
// 非 pointfree,因为提到了数据:name
var initials = function (name) {
return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};
// pointfree
// 先定义基本运算 并把要操作的数据放在最后
var split = curry(function(separator, str) { return str.split(separator) })
var head = function(str) { return str.slice(0, 1) }
var toUpperCase = function(str) { return str.toUpperCase() }
var join = curry(function(separator, arr) { return arr.join(separator) })
var map = curry(function(fn, arr) { return arr.map(fn) })
var initials = compose(join('.'), map(compose(toUpperCase, head)), split(' '));
initials("kevin daisy kelly");
可以看到,利用柯里化(curry)和函数组合 (compose) 非常有助于实现 pointfree。
Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。即不使用所要处理的值,只合成运算过程。
那么使用 pointfree 模式究竟有什么好处呢?
pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用,更符合语义,更容易复用,测试也变得轻而易举。
管道 pipe
compose()
函数数据流的运行机制,是从右至左,因为最右侧的函数首先执行,将数据传递给下一个函数,以此类推,最左侧的函数最后执行。
但也有另外一种方式:从左至右执行,最左侧的函数最先执行,最右侧的函数最后执行。类似于Unix下的 ‘|’ 操作,Unix命令的数据流是从左至右的。我们将这种机制叫做管道 pipe ,他与 compose 所做的事情相同,只不过改变了数据流的方向。
实现:
function pipe() {
let args = arguments;
let length = args - 1;
let result;
let start = 0;
return function () {
result = args[start].apply(this,arguments);
while(start++ && start <= length) {
result = args[start].call(this, result);
}
return result;
}
}
pipe(greeting, toUpperCase)('cc','ff'); // "HELLO, cc ff"
总结
组合像一系列管道那样把不同的函数联系在一起,数据就可以也必须在其中流动。组合让我们的代码简单而富有可读性。
Reference
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 左侧固定右侧自适应两栏布局
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论