函数柯里化与反柯里化

函数式编程近期再度被热炒,各社区对函数式编程的讲解和推崇也是层出不穷。好像要是不知道函数式编程,就没法跟大佬们愉快的玩耍了。
这一高大上的概念听起来似乎非常晦涩难懂让人摸不到头脑,然而实际上,也确实如此……
本文记录对函数式编程密切相关的两个概念:柯里化与反柯里化的个人理解。

从函数式编程说起

函数式编程有被称为函数范式,是一种编程范式,一种编程思想。
函数式的核心思想就是通过“纯函数”进行过程抽象,使代码逻辑清晰、降低耦合、便于维护。
这里出现了一个新概念:“纯函数”。纯函数即无状态、无副作用、幂等、无关时序的函数,指的是函数的输出完全由输入所决定,运行过程不依赖于系统的状态和上下文环境,运行过程不改变它作用域之外的环境状态。
而本文要说的柯里化(currying)与反柯里(uncurrying)化有什么关系呢?
柯里化和反柯里化是函数式编程的一个特性,是函数提纯的一种手段。

柯里化(currying)

柯里化又称部分求值,维基百科上的定义:

是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数并且返回结果的新函数的技术。

通俗来说就是不会立刻求值,而是到了需要的时候再去求值。
柯里化有3个常见作用:

  1. 参数复用
  2. 提前返回
  3. 延迟计算/运行

    柯里化函数创建的步骤

柯里化函数一般由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。
下面是创建柯里化函数的通用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 来自《JavaScript高级程序设计》22.1.5
function curry(fn) {
var args = Array.prototype.slice.call(arguments, 1)
return function () {
var innerArgs = Array.prototype.slice.call(arguments, 1)
var finalArgs = args.concat(innerArgs)
return fn.apply(null, finalArgs)
}
}

// 测试用例
function add(num1, num2) {
return num1 + num2
}
var curriedAdd = curry(add, 5)
alert(curridAdd(3)) // 8

以上函数就是一个简单的柯里化函数,被柯里化的函数只需传入部分值即可,因为部分参数已经保存在了柯里化函数返回的闭包中。

一个通用的柯里化函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var curry = function (fn) {
var _args = []
return function _fn() {
// 如果没有传入参数就开始求值,
// 否则将参数放入闭包_args中,并继续curring
if (arguments.length === 0) {
return fn.apply(this, _args)
} else {
[].push.apply(_args, arguments)
return _fn
}
}
}

// 测试用例
var plus = curry(function () {
var result = 0
for (var i = 0; i < arguments.length; i++) {
result += arguments[i]
}
return result
})
plus(1)(2, 3, 4)() // 等价于plus(1, 2)(3, 4)() //10

以上函数是更为通用的柯里化函数。原理还是一样不断的往闭包中的_args数组中存放参数,。不同的是只要是传参调用curried函数,则返回函数本身用于下次继续调用,只在不传参的时候,才真正调用原函数并传递闭包中保存的所有参数(_args)。

反柯里化(uncurrying)

柯里化(currying)是预先填入一些参数,目的是为了固定参数, 延迟计算等
反柯里化(uncurrying)就是把原来已经固定的参数或者this上下文等当作参数延迟到未来传递。

反柯里化的三种实现

反柯里化函数有三种实现,其中两种大同小异。

第一种实现方法:

1
2
3
4
5
6
7
// 因为反柯里化只针对函数,所以直接将uncurrying放到Function原型对象上
Function.prototype.uncurrying = function () {
var _that = this
return function () {
return Function.prototype.call.apply(_that, arguments)
}
}

以上代码做了3件事:

  • Function原型上增加uncurrying方法,方便所有函数继承
  • 返回函数,即暴露方法对外的接口
  • 使用call将调用对象设置为原函数,并用apply将参数传入其中

第二种实现:

1
2
3
4
5
6
Function.prototype.uncurrying = function () {
var _that = this
return function () {
return _that.apply(arguments[0], Array.prototype.slice.call(arguments, 1))
}
}

这种方式同上一种差不多,相比之下这种容易理解一点。
闭包中的_taht仍是原函数,返回的函数调用时直接利用_that.apply方法将调用时执行上下文换为传入的第一个参数。
结合一个例子看下:

1
2
3
4
5
6
var push = Array.prototype.push.uncurrying() // 被反柯里化的函数是数组的push方法
var obj = { // boj是一个类数组对象
0: 1,
length: 1
}
push(obj, 2) // {0: 1, 1: 2, length: 2}

可以看到本无push方法的obj对象被传入反柯里化的push方法中,成功的使用了数组的push方法
以上两种实现方法的核心是使用call或者apply来切换原函数调用时的上下文,使原函数能应用于原本没有该方法的对象。

第三种实现方法

前两种实现都是借助callapply来在调用过程中切换函数执行上下文实现。而在ES5中,提供了bind方法原生实现绑定执行上下文。
bind实现反柯里化代码如下:

1
2
3
Function.prototype.uncurrying = function () {
return this.call.bind(this)
}

bind返回一个已经绑定了执行上下文的函数
假设函数fn调用了uncurrying方法,将返回一个如下方法:

1
2
3
function () {
Function.prototype.call.apply(fn, arguments)
}

可见效果是同第一种实现一样的。

反柯里化的例子

前面已经有了一个push方法的例子,这里看看其他运用uncurrying的例子。
例一:

1
2
3
4
5
6
7
8
9
10
11
12
// uncurrying的使用
var person1 = {
name: 'Jack',
sayName: function () {
return this.name
}
}
var person2 = {
name: 'Tom'
}
var sayName = person1.sayName.uncurrying()
sayName(person2) // 'Tom'

例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 反柯里化自身
var person1 = {
name: 'Jack',
sayName: function () {
return this.name
}
}
var person2 = {
name: 'Tom'
}
var uncurrying = Function.prototype.uncurrying.uncurrying()
var sayName = uncurrying(person1.sayName)
sayName(person2) // 'Tom'

总结

柯里化和反柯里化作为函数范式的特征,都是很好的利用了高阶函数。引用一段我认为对柯里、反柯里化很好的理解:

柯里化体现的思想是”归一”, 多个参数化为一个参数, 然后逐个处理, 便于产生偏函数, 实现链式调用;
反柯里化体现的思想是”延伸”, 通过拓展方法的作用域, 使得它变得更通用, 提高了代码的复用性. 它们都提升了代码的优雅性.

参考资料