函数式编程之组合与管道

发布于 2022-05-27 13:11:31 字数 5312 浏览 1215 评论 0

组合 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

JavaScript函数式编程
JavaScript专题之函数组合

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84960 人气
更多

推荐作者

留蓝

文章 0 评论 0

18790681156

文章 0 评论 0

zach7772

文章 0 评论 0

Wini

文章 0 评论 0

ayeshaaroy

文章 0 评论 0

初雪

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文