函数组合
函数式编程有两个最基本的运算:合成和柯里化。
背景知识
- 纯函数和柯里化很容易写出洋葱代码
h(g(f(x)))
比如:获取数组的最后一个元素再转换成大写字母
// 使用lodash提供的API 先翻转数组 --> 再取第一个元素 --> 再转换成大写字母
_.toUpper(_.first(_.reverse(array)))
函数组合可以让我们把细粒度的函数重新组合生成一个新的函数,避免写出洋葱代码
管道
下面这个图表示了函数处理数据的过程,给fn输入参数a,得到结果b。可以理解成a数据通过管道fn得到b数据。
下面这个图更是把管道fn拆分成了三个管道f1,f2,f3,数据a通过管道f3得到m,m通过管道f2得到n,n通过管道f1得到数据b。其实m和n是什么我们不用关心
类似于下面的函数
fn = compose(f1, f2, f3)
b = fn(a)
函数组合
- 函数组合 (compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把所有中间步骤合并成一个函数
- 函数组合默认是从右到左执行
- 参与合成的函数也必须是纯的
- f(x)和g(x)合成为f(g(x)),有一个隐藏的前提,就是f和g都只能接受一个参数。
// 函数组合演示
function compose(f, g) {
return function (value) {
return f(g(value))
}
}
// 数组翻转函数
function reverse (array) {
return array.reverse()
}
// 获取第一个元素函数
function first (array) {
return array[0]
}
// 组合函数,获取函数最后一个元素
const last = compose(first, reverse)
console.log(last([1, 2, 3, 4])) // 4
Lodash中的组合函数 —— flow()/flowRight()
lodash 中组合函数 flow() 或者 flowRight(),都可以组合多个函数。
- flow() 是从左到右运行
- flowRight() 是从右到左运行
下面实例是获取数组的最后一个元素并转化成大写字母
const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = str => str.toUpperCase()
const f = _.flowRight(toUpper, first, reverse)
console.log(f(['AAA', 'BBB', 'CCC'])) // CCC
函数组合原理模拟
上面的例子我们来分析一下:
入参不固定,参数都是函数,出参是一个函数,这个函数要有一个初始的参数值
function compose (...args) {
// 返回的函数,有一个传入的初始参数即value
return function (value) { // 因为前面提到:组合有一个隐藏的前提,函数都只能接受一个参数,所以不需要...arg来接收参数,只一个value就可以了
// reduce的第一个参数是一个回调函数,第二个参数是acc的初始值,这里acc的初始值就是value
return args.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value)
}
}
const fc = compose(toUpper, first, reverse)
console.log(fc(['AAA', 'BBB', 'CCC'])) // CCC
函数组合-结合律
什么是函数组合结合律?
下面三个情况结果一样,我们既可以把 g 和 h 组合,还可以把 f 和 g 组合。
// 结合律(associativity)
compose(f, compose(g, h))
// 等同于
compose(compose(f, g), h)
// 等同于
compose(f, g, h)
函数组合-调试
如果运行的结果和预期不一致,要怎么调试呢?怎么能知道中间运行的结果呢?
下面输入ROYAL NEVER GIVE UP
要对应输出royal-never-give-up
(RNG加油!!!)
思路:先用空格将字符串分割为数组,将数组每个元素转为全小写,在将数组元素用'-'连接
const _ = require('lodash')
// _.split(string, separator),因为_.split方法第一个参数为需要分割的字符串,第二个参数为分割符
// 且我们最后调用组合函数的时候要传入字符串,所以字符串要在第二个参数位置传入,因此需要二次封装一个split函数
// 通过柯里化将多个参数转成一个参数
const split = _.curry((sep, str) => _.split(str, sep))
// 大写变小写,用到toLower(),因为这个函数本身只有一个参数,所以可以在函数组合中直接使用
// _.join(array, [separator=','])
// join方法也需要两个参数,第一个参数是数组,第二个参数是分隔符,数组也是最后的时候才传递,也需要进行柯里化转换
const join = _.curry((sep, array) => _.join(array, sep))
const f = _.flowRight(join('-'), _.toLower, split(' '))
console.log(f('ROYAL NEVER GIVE UP')) // r-o-y-a-l-,-n-e-v-e-r,-g-i-v-e-,-u-p
但是最后的结果却不是我们想要的,那应该怎么调试呢?
const _ = require('lodash')
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))
// 我们需要对中间值进行打印,并且知道其位置,用柯里化输出一下
const log = _.curry((tag, v) => {
console.log(tag, v)
return v
})
// 从右往左在每个函数后面加一个log,并且传入tag的值,就可以知道每次结果输出的是什么
const f = _.flowRight(join('-'), log('after toLower:'), _.toLower, log('after split:'), split(' '))
// 从右到左
// 第一个log:after split: [ 'ROYAL', 'NEVER', 'GIVE', 'UP' ] 正确
// 第二个log: after toLower: royal,never,give,up 转化成小写字母的同时转成了字符串
console.log(f('ROYAL NEVER GIVE UP')) // r-o-y-a-l-,-n-e-v-e-r,-g-i-v-e-,-u-p
// 修改方式,利用数组的map方法,遍历数组的每个元素让其变成小写
// 这里的map需要两个参数,第一个是数组,第二个是回调函数,需要柯里化
const map = _.curry((fn, array) => _.map(array, fn))
const f1 = _.flowRight(join('-'), map(_.toLower), split(' '))
console.log(f1('ROYAL NEVER GIVE UP')) // royal-never-give-up
FP模块
函数组合的时候用到很多的函数需要柯里化处理,我们每次都处理那些函数有些麻烦,所以lodash中有一个FP模块
- lodash 的 fp 模块提供了实用的对函数式编程友好的方法
- 提供了不可变 auto-curried iteratee-first data-last (函数置先,数据置后)的方法
// lodash 模块
const _ = require('lodash')
// 数据置先,函数置后
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
// 数据置先,分割符置后
_.split('Hello World', ' ')
// lodash/fp 模块
const fp = require('lodash/fp')
// 函数置先,数据置后
fp.map(fp.toUpper, ['a', 'b', 'c'])
fp.map(fp.toUpper)(['a', 'b', 'c'])
// 分割符置先,数据置后
fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')
体验FP模块对于组合函数的友好
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(f('ROYAL NEVER GIVE UP')) // royal-never-give-up
Lodash-map方法的小问题
const _ = require('lodash')
const fp = require('lodash/fp')
console.log(_.map(['55', '5', '11'], parseInt))
// [ 55, NaN, 3 ]
// _.map的回调函数接收三个参数,第一个参数是遍历的数组,第二个参数是key/index,第三个参数是对应函数
_.map(['55', '5', '10'], function(...args){
console.log(...args)
})
// 55 0 [ '55', '5', '11' ]
// 5 1 [ '55', '5', '11' ]
// 11 2 [ '55', '5', '11' ]
// parseInt第二个参数表示进制,0默认就是10进制,1不存在2~36范围中,2表示2进制
// parseInt('55', 0, array) 十进制
// parseInt('5', 1, array)
// parseInt('11', 2, array) 二进制
// 要解决的话需要重新封装一个parseInt方法(默认基数为十进制)
// 而使用fp模块的map方法不存在下面的问题
console.log(fp.map(parseInt, ['55', '5', '11']))
// [ 55, 5, 11 ]
附上parseInt的使用,忘记的同学快速复习一下
Point Free
是一种编程风格,具体的实现是函数的组合。
fn = R.pipe(f1, f2, f3);
这个公式说明,如果先定义f1、f2、f3,就可以算出fn。整个过程,根本不需要知道a(输入的数据)或b(最终得到的数据)。
Point Free: 我们完全可以把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合成在一起即可。
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
// 比如完成转换:Hello World => hello_world
// 思路:
// 先将字母换成小写,然后将空格换成下划线。如果空格多于一个,要替换成一个
const fp = require('lodash/fp')
// replace方法接收三个参数
// 第一个是正则匹配pattern,第二个是匹配后替换的数据,第三个是要传的字符串
// 所以这里需要传两个参数
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World')) //hello_world
Pointfree案例
// world wild web -->W. W. W
// 思路:
// 把一个字符串中的单词首字母提取并转换成大写,使用. 作为分隔符
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
console.log(firstLetterToUpper('world wild web')) // W. W. W
// 上面的代码进行了两次的遍历,性能较低
// 优化
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(firstLetterToUpper('world wild web')) // W. W. W
Pointfree本质
Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。这就要求,将一些常用的操作封装成函数。
比如,读取对象的role属性,不要直接写成obj.role,而是要把这个操作封装成函数。
var prop = (p, obj) => obj[p];
var propRole = R.curry(prop)('role');
附录
- 函数式编程指北
- 函数式编程入门
- Pointfree 编程风格指南
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!