组合 (compose)

如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数组合"(compose)。

这张图展示了什么是组合:

通过组合:

合成两个函数的简单代码如下:

const compose = function (f, g) {
  return function (x) {
    return f(g(x));
  };
}

组合的使用

以下一个例子, 简答地使用组合说明其作用:

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);
shout("send in the clowns"); // SEND IN THE CLOWNS!

在 compose 的定义中,g 将先于 f 执行,因此就创建了一个从右到左的数据流。这样做的可读性远远高于嵌套一大堆的函数调用,如果不用组合,shout 函数将会是这样的:

var shout = function(x){
  return exclaim(toUpperCase(x));
};

让代码从右向左运行,而不是由内而外运行,可以称之为“左倾”。

我们来看一个顺序很重要的例子:

var head = function(x) { return x[0]; };
var reverse = x => x.reduce((acc, x) => [x].concat(acc), []);
var last = compose(head, reverse);
last(['jumpkick', 'roundhouse', 'uppercut']); // 'uppercut'

reverse 反转列表,head 取列表中的第一个元素;所以结果就是得到了一个 last 函数。尽管我们可以定义一个从左向右的版本,但是从右向左执行更加能够反映数学上的含义——是的,组合的概念直接来自于数学课本。

多个函数组合

函数组合并不局限于两个函数的组合, 可以扩展为多个函数组合的形式:

var compose = function(...fns){
  return value => [...fns, value].reverse().reduce((acc,fn) => fn(acc))
}
// 或者
var compose = function(){
  return value => [...arguments, value].reverse().reduce((acc,fn) => fn(acc))
}
// 用真正的 reduce 函数改写一下
var compose = function(...fns){
  return function(value){
      // 这样更容易看出思想,即将 value 作为初始值,然后将其传入最后一个函数,将返回值一直向前传递
      fns.push(value);
      return fns.reverse().reduce((acc,fn)=>{
          return fn(acc);
      })
  }
}

示例:

var toUpperCase = x => x.toUpperCase();
var replace = (reg, rep) => x => x.replace(reg, rep)
var snakeCase = compose(console.log, replace(/\s+/ig, '_'), toUpperCase);
snakeCase('Hello world') // HELLO_WORLD

函数结合律

函数的合成还必须满足结合律。

compose(f, compose(g, h))
// 等同于
compose(compose(f, g), h)
// 等同于
compose(f, g, h)

结合律的一大好处是任何一个函数分组都可以被拆开来,然后再以它们自己的组合方式打包在一起:

var loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
// 或
var last = compose(head, reverse);
var loudLastUpper = compose(exclaim, toUpperCase, last);
// 或
var last = compose(head, reverse);
var angry = compose(exclaim, toUpperCase);
var loudLastUpper = compose(angry, last);
// 更多变种...

pointfree

pointfree 模式指的是,永远不必说出你的数据。意思是,函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。

// 非 pointfree,因为提到了数据:word
var snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};
// pointfree
var toLowerCase = x => x.toLowerCase();
var replace = (reg, rep) => x => x.replace(reg, rep)
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
snakeCase('Hello world') // hello_world

这里所做的事情就是通过管道把数据在接受单个参数的函数间传递。利用 curry,我们能够做到让每个函数都先接收数据,然后操作数据,最后再把数据传递到下一个函数那里去。另外注意在 pointfree 版本中,不需要 word 参数就能构造函数;而在非 pointfree 的版本中,必须要有 word 才能进行一切操作。

再来看一个例子:

// 非 pointfree,因为提到了数据:name
var initials = function (name) {
  return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};
// pointfree
const toUpperCase = x => x.toUpperCase()
const head = x => x[0]
const join = s => x => x.join(s)
const split = s => x => x.split(s)
const map = fun => x => x.map(fun)
var initials = compose(console.log, join('. '), map(compose(toUpperCase, head)), split(' '));
initials("hunter stockton thompson"); // 'H. S. T'

错误追踪

在使用组合的使用, 很可能在某个阶段出问题, 为了跟踪每次执行输出的值, 可以使用错误追踪函数进行错误追踪:

var trace = tag =>{
  return x => {
    console.log(tag, x)
    return x
  }
}

比如上面的例子, 可以使用 trace 进行追踪:

var initials = compose(
  console.log,
  trace('join'), join('. '),
  trace('map'), map(compose(toUpperCase, head)),
  trace('split'), split(' ')
);
initials("hunter stockton thompson")
// split [ 'hunter', 'stockton', 'thompson' ]
// map [ 'H', 'S', 'T' ]
// join H. S. T
// H. S. T

其他原则

ramda 中的map是满足组合律的:

// map 的组合律
var law = compose(map(f), map(g)) == map(compose(f, g));

参考资料

MIT Licensed | Copyright © 2018-present 滇ICP备16006294号

Design by Quanzaiyu | Power by VuePress