函数科里化
定义
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
特征就是执行一个函数后可以返回一个函数,可以继续调用,因而可以实现延迟调用。最大的好处就是参数复用以及参数分割,实现函数参数的"颗粒化"。
应用场景
1. 分割固定参数与可变参数
比如在redux-thunk中,我们经常见到这样的代码。乍一看,感觉还有点懵。
const addValue = (value) => /* 注意,这里返回了一个函数 */(dispatch) => {
dispatch({type: 'add', payload: value})
}
// 在store用到的地方
store.dispatch(addValue(1))
其实,这个addValue的方法的作用是返回了一个可以供redux-thunk执行的函数,就类似于下一段代码
// 在store用到的地方1
store.dispatch((dispatch) => {
dispatch({type: 'add', payload: 1})
})
那这样写跟上面那段看起来更复杂的代码有什么区别呢?
这样可能看不出什么问题,但是用的地方一多,就可以发现 dispatch这个参数未免写得太多了。
// 在store用到的地方2
store.dispatch((dispatch) => {
dispatch({type: 'add', payload: 1})
})
// ...在store用到的地方N
store.dispatch((dispatch) => {
dispatch({type: 'add', payload: 2})
})
而curry化后,将可变参数value、固定参数dispatch分为"两段",你只需要关注value就行,不需要再关注dispatch(当然要写好注释)。
const addValue = (value) => /* 注意,这里返回了一个函数 */(dispatch) => {
dispatch({type: 'add', payload: value})
}
// 在store用到的地方 +1
store.dispatch(addValue(1))
// 在store用到的地方 +2
store.dispatch(addValue(2))
在实际开发中,我们怎么将curry应用到这种场景,降低代码量呢?
很简单,看到js原生方法、库(rxjs、redux-thunk等)使用的回调(promise.then, [].forEach, [].sort等)就可以考虑这个回调的部分逻辑是否会在代码中大量使用,有大量使用使用的可能,就可以考虑将可变参数、回调的固定的参数进行分离,用"两段"函数执行。
// 把一个字符串数组按特定的字符串排序,不在排序数组中则按原顺序排到最后。
// 比如 把['a', 'b', 'c'] 按 cab进行排序;把['a', 'b', 'c'] 按 cab排序
const createComparer = (fields) => {
// 字符串 -> 对应顺序 的对象
const orderMemo = fields.reduce((memo, field, index) => {
memo[field] = index
return memo
}, Object.create(null))
// 获取排序的位置
const getOrder = (str) => (str in orderMemo ? orderMemo[str] : fields.length)
// 生成的排序方法
return (preStr, curStr) => getOrder(preStr) - getOrder(curStr)
}
// 测试
console.log(['a', 'b', 'c'].sort(createComparer(['c', 'a', 'b']))) // cab
console.log(['a', 'b', 'c'].sort(createComparer(['a', 'c', 'b']))) // acb
curry化的面试题
如何实现curry化的函数,可以实现下面方式的调用
let add = (a,b,c) => a+b+c
//一般使用方法
add(1,2,3)
//curry函数使用
let curryAdd = curry(add)
curryAdd(1,2,3)
curryAdd(1,2)(3)
curryAdd(1)(2)(3)
1. 特征分析
科里化函数在形式上可以继续往后面加括号,说明返回了一个函数,但是也不能总是返回函数,否则就拿不到想要的值了,所以我们必须有手段来判断需要返回函数还是返回值。
2. 什么时候返回函数
需要继续接收参数的时候
3. 什么时候返回值
参数已经接收够了,需要执行函数了。判断参数是否达到需要有两种方式
1) 对于参数长度有上限的函数,参数达到函数参数的长度就执行函数
args.length >= fun.length
//缺点是函数参数长度不可加长
2) 对于参数长度无上限的函数,curry函数内部定义一个getValue方法或者无参数传入时执行
缺点是需要在最后加一个getValue()或者空括号(),就不实现了
add(1,2,3)(2)(3)(4)(5)(6)(7)(8).getValue()
add(1,2,3)(2)(3)(4)(5)(6)(7)(8)()
实现
1. 简单版
/**
* @param {function}
* @return {function}
*/
let curry = (fun) => (
/**
* @param {any[]}
* @return {any|function}
*/
function curried(...args1) {
return args1.length >= fun.length
? fun.apply(this, args1)
: (...args2) => curried.apply(this, args1.concat(args2))
}
)
//验证
let add = (a,b,c) => a+b+c
//一般使用方法
add(1,2,3)
//curry函数使用
let curryAdd = curry(add)
console.log(curryAdd(1,2,3))
console.log(curryAdd(1,2)(3))
console.log(curryAdd(1)(2)(3))
console.log(curryAdd()()()()()(1)(2)(3))
//参数复用
let curryAdd1And2 = curry(add)(1,2)
console.log(curryAdd1And2(3))
2. 参数可插队版
上面的curry函数只能往后补加参数,只能乖乖排队,需要复用的参数放前面,不能复用的参数排后面,不一定能满足需求。 因此,需要实现参数“插队”功能。有两种方案,一种是记录“插队”参数的位置,这种不直观,每次还得数一数,而且面临着从0还是从1开始的难题,所以放弃这种方案。 还有一种就是提前“占坑”,插入坑的位置,“非坑”参数一旦达到函数长度,将长度超过的部分插入“坑”中。 比如:
// 传入 1, 2, 3, 4, 5 参数
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
fn(1, _, 3)(_, 5)(2)(4);
fn(1, _, _, 4)(_, 5)(2)(3);
fn(_, 2)(_, _, 4)(1)(3)(4)
改进思路
-
坑位-保证唯一
const _ = Symbol('insertParam')//或者new Object() -
判断返回函数还是执行结果改进
let getParaLen = (argsArr) => argsArr.reduce((preLen, arg) => arg !== _ ? 1 + preLen : preLen , 0) getParaLen(args1) >= fun.length -
执行改进-蹲坑后再执行
let runFun = function (...args) { let funLen = fun.length let argsForRun = [] for (let i = 0; i < fun.length; i++) { argsForRun.push( args[i] === _ ? args[funLen++]//取位于fun.length的参数“蹲坑”,并且下一次取下一个参数 : args[i] ) } return fun.apply(this, argsForRun) }最终代码
const _ = Symbol('insertParam')//或者new Object() let curry = (fun) => { let getParaLen = (argsArr) => argsArr.reduce((preLen, arg) => arg !== _ ? 1 + preLen : preLen , 0) let runFun = function (...args) { let funLen = fun.length let argsForRun = [] for (let i = 0; i < fun.length; i++) { argsForRun.push( args[i] === _ ? args[funLen++]//取位于fun.length的参数“蹲坑”,并且下一次取下一个参数 : args[i] ) } return fun.apply(this, argsForRun) } return ( function curried(...args1) { return getParaLen(args1) >= fun.length ? runFun.apply(this, args1) : (...args2) => curried.apply(this, args1.concat(args2)) } ) } //测试 let fn = curry((a,b,c,d,e)=>[a,b,c,d,e]) console.log(fn(_, 2, 3, 4, 5)(1)); console.log(fn(1, _, 3, 4, 5)(2)); console.log(fn(1, _, 3)(_, 5)(2)(4)); console.log(fn(1, _, _, 4)(_, 2)(3)(5)); console.log(fn(_, 2)(_, _, 5)(1)(3)(4))
对于参数长度无上限的函数,也可以用回调获取运行结果
参考promise的then回调,我们也可以实现下面这种方式的调用
// getValue执行里面的函数,返回上一个并继续返回接收参数的函数
add(1,2,3)(2)(3)(4)(5)(6)(7)(8)
.getValue(currentVlaue=>console.log(currentValue))
(3)(3)
.getValue(currentVlaue=>console.log(currentValue))