函数组合
函数组合的概念: 如果一个函数要经过多个函数处理才能得到最终的值,这个时候我们可以把中间这些过程函数合并成一个新的函数。
函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
函数组合默认是从右到左执行
进行函数组合时尽量使用只要一个参数的纯函数
基本的函数组合
我们在使用纯函数和柯里化时很容易写出洋葱代码,h(g(f(x))),也就是一层包一层的代码,比如我们要获取数组的最后一个元素,然后在转换成大写字母。
先调用数组对象的reverse方法反转数组,然后调用first方法获取数组第一个元素,再调用toUpper方法将获取的第一个元素转为大写。
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const array = ['a', 'b', 'c', 'd'];
let end = toupper(first(reverse(array)));
console.log(end)
可以发现上面这些方法的调用就是一层包一层的,这就是洋葱代码,这是最基本的函数组合,我们将其封装一下。
比如上面的例子需要调用reverse,first,toUpper三个函数,我们可以通过组合,将这三个函数合并成一个,得到test函数,调用的时候仍旧传入array数组,处理的结果是不变的。函数组合其实就相当于隐藏掉了多个函数调用的中间结果,比如reverse传递给first,first传递给toUpper。
const array = ['a', 'b', 'c', 'd'];
function compose(fn1,fn2,fn3){
return function(...args){
return fn1(fn2(fn3(...args)))
}
}
const test = compose(toupper,first,reverse);
console.log(test(array ))
函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果。
多个函数的合并
接下来,实现多个函数的合并,模拟一下组合函数的原理, 一般函数组合的习惯写法,进行组合的函数从右向左执行。我们要对它进行一个反转,这里给args进行一个reverse处理,反转之后我们要依次调用里面的函数,并且前一个函数的返回值需要是下一个函数的参数。
function compose (...args) {
return function (value) {
return args.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value)
}
}
const test = compose(toupper,first,reverse);
console.log(test(array))
函数组合要满足的特点
函数的组合要满足结合律 (associativity): 我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
const test = compose(toUpper, compose(first, reverse))
console.log(test(['one', 'two', 'three']))
可以发现我们无论先结合前两个还是先结合后两个,得到的结果都是相同的,这就是结合律,和数学中的结合律是一样的。
函数组合的调试
当我们使用函数组合的时候,如果我们执行的结果跟我们预期的不一致,这个时候我们应该如何调试呢?
比如说下面的代码,当我们想知道reverse执行的结果是什么时候。我们可以在reverse函数前面追加一个log函数,把他打印出来看一下
现在我们来实现一个函数,把 NEVER SAY DIE 变成 never-say-die 。 思路是
1.先用 split 函数用空格把字符串切割成为数组--->[NEVER,SAY,DIE],
2.再用 map 函数把数组每一项变为小写,[ never , say , die ],
3.最后用 join 函数用 - 把数组合并成字符串 "never-say-die"。
在函数组合的时候我们需要的是只有一个参数的纯函数, 下面split 和 join 2个函数都是需要2个参数的纯函数,所以用函数的柯里化把它们转化成只需要一个参数的纯函数
function curry (func) {
return function curriedFn(...args) {
// 判断实参和形参的个数
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
};
const split = curry(function(step,arr){
return arr.split(step)
})
const map = function(arr){
return arr.map(function(item){
return item.toLowerCase()
})
}
const join = curry(function(step,str){
return str.join(step)
})
再来写一个调试打印函数trace,用函数的柯里化把trace函数转化成只需要一个参数的纯函数,它的第一个参数tag是描述当前结果是哪一个函数执行完毕后的打印结果(哪一条管道结束后的打印信息)
const trace = curry((tag, v) => {
console.log(tag, v)
return v
})
接下来开始函数组合
const f = compose(join('-'), trace('map 之后'), map, trace('split 之后'), split(' '));
console.log(f('NEVER SAY DIE'))
好了调试的方式就完成了,上一下完整的代码
function compose (...args) {
return function (value) {
return args.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value)
}
};
function curry (func) {
return function curriedFn(...args) {
// 判断实参和形参的个数
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
};
const trace = curry((tag, v) => {
console.log(tag, v)
return v
})
const split = curry(function(step,arr){
return arr.split(step)
})
const map = function(arr){
return arr.map(function(item){
return item.toLowerCase()
})
}
const join = curry(function(step,str){
return str.join(step)
})
const f = compose(join('-'), trace('map 之后'), map, trace('split 之后'), split(' '))
console.log(f('NEVER SAY DIE'))
pointerFree编程风格
它的具体实现就是上面所说的函数组合,我们完全可以把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合成在一起即可。
不使用所要处理的值,只合成运算过程。中文可以译作"无值"风格。
1.不需要指明处理的数据;
2.只需要合成运算过程;
3.需要定义一些辅助的基本运算函数
使用函数组合在处理问题的时候,其实就是一种PointFree模式,比如下面的这个案例,在这个案例中我们先把一些基本的运算合成为一个函数,而在这个过程中是没有指明要处理的数据的,这就是PointFree模式。
看一个例子:还是要把Hello World转换为hello_world这样的形式;
按照我们传统的思维方式,我们会先定义一个函数,来接收一个我们要处理的数据,接着我们在这个函数里面对我们的数据进行处理,得到我们想要的结果,这是非PointFree模式。
function f (word) {
return word.toLowerCase().replace(/\s+/, '_');
}
f('Hello World')
而我们如果使用PointFree模式来解决这个问题的话,我们首先会定义一些基本的运算函数,然后把他们何成为一个新的函数,而在合成的过程中我们不需要指明我们需要处理的数据。
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))
再来一个例子:把单词中的首字母提取并转换成大写
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(join('.'),fp.map(fp.flowRight(fp.first,fp.toUpper)), split(' '))
console.log(firstLetterToUpper('world wild web'))
// => W. W. W
那我们来回顾一下函数式编程的核心,其实就是把运算过程抽象成函数。PointFree模式就是把我们抽象出来的函数再合成为一个新的函数,而这个合成的过程其实又是一个抽象的过程。在这个抽象的过程中我们依然是不需要关心数据的。
上面面我们使用PointFree模式来实现一下上面的案例。在这里进行函数组合时用到了lodash函数库;lodash 是一个非常有用的库,前面用到的柯里化方法就是lodash中的curry,上面我们模拟的compose组合方法也用lodash库的flowRight方法代替;
lodash库里还有fp模块,它提供了实用的 对函数式编程友好的方法,已经被柯里化后的方法!,如果一个方法的参数是函数的话,它会要求函数优先,数据滞后 。