函数式编程--柯理化(Currying)

651 阅读6分钟

基本概念

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

上面是百度百科以及维基百科关于柯理化的定义,单纯从字面上面理解是很困难的。 简单说,柯里化(Currying)是一种处理多元函数的方法。它产生一系列连锁函数,其中每个函数固定部分参数,并返回一个新函数,用于传回其它剩余参数的功能

下面我们通过一些实例,拆解和说明一下柯理化的具体含义。

这是一个普通的三元函数,执行运算得出三个参数的和。

function add(a,b,c){
    return a + b + c
}

add(1,2,3) // 6

按照上面的定义,add函数的的柯理化转换过程:add(a,b,c) => curriedAdd(a)(b)(c)。

实现代码如下:

function curriedAdd(a){
    return function(b){
        return function(c){
            return a + b + c
        }
    }
}

执行验证

add(1,2,3) // 6
curriedAdd(1)(2)(3) // 6

上面是基于极简场景的实现,完整的curriedAdd函数,应当是满足下面的调用场景:

  1. 初始传入一个参数,curriedAdd(1)(2,3) 或者 curriedAdd(1)(2)(3)

    • 如果首次执行curriedAdd只传入一个参数,那么将返回一个接受剩余两个参数的函数currying1
    • 如果继续执行currying1,并传入两个参数,将返回执行结果a+b+c;但是,如果继续执行currying1,并传入一个参数,那么将返回一个接受第三个参数的函数currying2
    • 如果继续执行curryring2,并传入参数,将返回执行结果a+b+c
  2. 初始传入两个参数,curriedAdd(1,2)(3)

    • 如果首次执行curriedAdd,并传入两个参数,那么将返回一个接受最后一个参数的函数curryring1
    • 如果继续执行curryring1,并传入参数,将返回执行结果a+b+c
  3. 初始传入三个参数,curriedAdd(1,2,3)

    • 如果首次执行curriedAdd,直接传入三个参数,那么将直接返回执行结果a+b+c

把上述拆解分析的逻辑过程,转化成函数:

function curriedAdd() {
    let args = [].slice.call(arguments)
    if (args.length >= 3) {
        return args[0] + args[1] + args[2]
    } else if (args.length === 2) {
        return function () {
            return args[0] + args[1] + arguments[0]
        }
    } else if (args.length === 1) {
        return function c() {
            let args1 = [].slice.call(arguments)
            if (args1.length >= 2) {
                return args[0] + args1[0] + args1[0]
            } else if(args1.length === 1) {
                return function () {
                    return args[0] + args1[0] + arguments[0]
                }
            }else{
                return c
            }
        }
    } else {
        return curriedAdd
    }
}

执行验证


console.log(curriedAdd(1)) // [Function (anonymous)]
console.log(curriedAdd(1)(2)) // [Function (anonymous)]
console.log(curriedAdd(1)(2)(3)) // 6

console.log(curriedAdd(1, 2)) // [Function (anonymous)]
console.log(curriedAdd(1, 2)(3)) // 6

console.log(curriedAdd(1, 2, 3)) // 6

验证实例运行结果:

[Function (anonymous)]
[Function (anonymous)]
6
[Function (anonymous)]
6
6

总之,柯理化是把一个多元函数,转换成一系列更少元函数的处理方法。

实现原理

实现一个二元或者三元的柯理化函数相对简单,就像上面的curriedAdd函数。难的是实现一个任意元函数的通用柯理化函数。 基于上面的分析,实现一个通用的柯理化函数,需要以下几个条件: 1、首先需要一个函数fn作为参数; 2、其次,可以获取到fn声明时虚参的数量,通过fn.length属性可以实现; 3、最后,可以判断返回接受剩余参数的新函数,或者返回fn(...参数)执行结果,以及缓存已经固定的参数。通过fn.length、闭包和递归可以实现。

function currying(fn) {
    return function curried() {
        var args = [].slice.call(arguments),
            context = this

        return args.length >= fn.length ?
            fn.apply(context, args) :
            function () {
                var rest = [].slice.call(arguments)
                return curried.apply(context, args.concat(rest))
            }
    }
}

通过currying创建上面的curriedAdd函数,执行验证得出相同的结果。

var curriedAdd = currying(add)

console.log(curriedAdd(1)) // [Function (anonymous)]
console.log(curriedAdd(1)(2)) // [Function (anonymous)]
console.log(curriedAdd(1)(2)(3)) // 6

console.log(curriedAdd(1, 2)) // [Function (anonymous)]
console.log(curriedAdd(1, 2)(3)) // 6

console.log(curriedAdd(1, 2, 3)) // 6

此外,还有一个问题需要补充说明。对参数不固定的函数进行柯理化变换是没有意义的。 例如,下面这个对不定数量的数字进行排序的sort函数。函数声明时参数的数量并不确定。通过sort.length获取到的虚参的数量是0,无论给curriedSort传入多少参数都会立即执行。

function sort(){
    return [].slice.call(arguments).sort(function(a,b){
        return a-b
    })
}

sort(1,3,6,2) // [1,2,3,6]

var curriedSort = currying(sort)
var currying1 = curriedSort(1,3,6,2) // [1,2,3,6]

currying1(5) // TypeError: curriedSort(...) is not a function

虽然无法通过length属性获取到不确定参数的长度,但是可以再柯理化转换的同时,指定目标参数长度,用于替代sort.length的作用。下面调整一下currying,适配参数不固定的函数。

function currying(fn, len) {
    return function curried() {
        var args = [].slice.call(arguments),
            context = this
        var _len = fn.length || len

        return args.length >= _len ?
            fn.apply(context, args) :
            function () {
                var rest = [].slice.call(arguments)
                return curried.apply(context, args.concat(rest))
            }
    }
}

var curriedSort = currying(sort,5)
var currying1 = curriedSort(1,3,6,2) // [Function (anonymous)]

currying1(5) // [1,2,3,5,6]

应用实践

  • 解决重复传参问题,提高函数适用性

柯理化(currying)应用很广泛也很常见。比如,批量发送双11活动邮件,通常我们这样做

function sendEmail(from, content, to){
    console.log(`${from} send email to ${to}, content is ${content}`)
}

sendEmail('xx公司', '双11优惠折上5折', 'zhangsan@xx.com')
sendEmail('xx公司', '双11优惠折上5折', 'lisi@xx.com')
sendEmail('xx公司', '双11优惠折上6折', 'wangwu@xx.com')
sendEmail('xx公司', '双11优惠折上6折', 'maliu@xx.com')

// ...

邮件发送方是固定的,邮件内容是相对固定的,唯一不同的是邮件的接受者。这正符合柯理化(currying)固定部分参数,并返回接受剩余参数新函数的规则。柯理化创建两个临时性的、适用性更强的函数sendEmailToS5和sendEmailToS6,向目标群体,发送指定类型的邮件。

var sendEmailContent = currying(sendEmail)('xx公司')
var sendEmailToS5 = sendEmailContent('双11优惠折上5折')
var sendEmailToS5 = sendEmailContent('双11优惠折上6折')

// 打五折的群组
sendEmailToS5('zhangsan@xx.com')
sendEmailToS5('lisi@xx.com')

// ...

// 打六折的群组
sendEmailToS6('wangwu@xx.com')
sendEmailToS6('maliu@xx.com')

// ...

因此,柯理化(currying)可以解决重复传参的问题,并提高函数功能的适用性。

  • 降低函数参数元次,适配应用

通常,在创建工具函数时,我们尽量使其更加抽象,以提高其通用性。但是,这样做的弊端也很明显,会降低其适用性。比如,我们创建一个获取对象目标属性的函数getObjKeys(obj,keys)

function getObjKeys(keys, obj){
    var o = {}
    keys.forEach(function(k){
        o[k] = obj[k]
    })
    return o
}

var person = {
    name:'zhangsan',
    age: 20,
    work: 'worker',
    tel: '13699887766'
}

getObjKeys(['name','tel'], person) // {name:'zhangsan',tel:'13699887766'}

假设,另外一个场景,我们需要查询车间,所以worker的姓名、年龄和电话。我们可以这样做

workers.map(function(worker){
    return getObjKeys(worker,['name','age','tel'])
})

除此之外,利用柯理化,我们可以固定getObjKeys函数keys参数,同时得到一个接受另外一个参数obj的函数。这个函数,可以作为map函数的callback直接被使用。

var callback = currying(getObjKeys)(['name','age','tel'])
workers.map(callback) // [{name,age,tel},...]

参考资料

baike.baidu.com/item/%E6%9F… github.com/shfshanyue/… juejin.cn/post/684490… developer.mozilla.org/zh-CN/docs/… www.yuque.com/webqiang/qn… github.com/mqyqingfeng…