函数科里化

224 阅读5分钟

函数科里化

定义

在计算机科学中,柯里化(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)

改进思路

  1. 坑位-保证唯一

    const _ = Symbol('insertParam')//或者new Object()
    
  2. 判断返回函数还是执行结果改进

    let getParaLen = (argsArr) => argsArr.reduce((preLen, arg) =>
        arg !== _
            ? 1 + preLen
            : preLen
        , 0)
    getParaLen(args1) >= fun.length
    
  3. 执行改进-蹲坑后再执行

    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))