Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

函数式编程之组合与管道 #26

Open
xuexueq opened this issue Oct 20, 2018 · 0 comments
Open

函数式编程之组合与管道 #26

xuexueq opened this issue Oct 20, 2018 · 0 comments
Labels
函数式编程 函数式编程

Comments

@xuexueq
Copy link
Owner

xuexueq commented Oct 20, 2018

组合 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专题之函数组合

@xuexueq xuexueq added Javascript Javascript 函数式编程 函数式编程 and removed Javascript Javascript labels Oct 20, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
函数式编程 函数式编程
Projects
None yet
Development

No branches or pull requests

1 participant