(转载)柯里化与反柯里化

原文链接:https://github.com/tcatche/tcatche.github.io/issues/22

柯里化

柯里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

柯里化,作为高阶函数的一种应用,是一个逐步接收参数的过程。

1
2
3
4
5
6
7
8
9
10
11
12
var add = (x, y) => x + y;

var curriedAdd = (x) => (y) => x + y;
var addOne = curriedAdd(1);

var addTen = curriedAdd(10);

addOne(5);
// 6

addTen(5);
// 15

上述示例,将原本接收两个参数的 add 改造成了接收一个参数的 addOneaddTen 函数,每个函数都具有独立的语义。

这种方式有一个优点,可以把易变的参数固定下来。这个最典型的应用场景是使用 bind 函数(偏函数的一种实现)绑定 this 对象:

1
2
3
4
5
6
7
Function.prototype.bind = function(context) {
var _this = this
var _args = Array.prototype.slice.call(arguments, 1);
return function() {
return _this.apply(context, _args.concat(Array.prototype.slice.call(arguments)));
}
}

柯里化的另一个应用场景是在如果有多个不同的执行场景,可以提前确定当前执行环境。这个最典型的例子是兼容现代浏览器以及 IE 浏览器的事件监听:

1
2
3
4
5
6
7
var addEvent = (root, ele, type, fn, capture = false) => {
if (root.attachEvent) {
ele.attachEvent('on' + type, fn);
} else if (root.addEventListener) {
ele.addEventListener(type, fn, capture);
}
};

这个函数执行倒没有问题,只是每次调用都会执行一次 if...else,挺繁琐的,完全可以通过柯里化只做一次判定:

1
2
3
4
5
6
7
var addEvent = ((root) => {
if (root.attachEvent) {
return (ele, type, fn) => ele.attachEvent('on' + type, fn);
} else if (root.addEventListener) {
return (ele, type, fn, capture = false) => ele.addEventListener(type, fn, capture);
}
})(window);

这样 addEvent 函数实际上已经是本浏览器支持的事件添加方法。

curry 的实现

curry 的实现有两种方法,第一种是自己手工实现:

1
2
3
var add = (x, y, z) => x + y + z;

var curriedAdd = (x) => (y) => (z) => x + y + z;

还有一种方法就是使用 curry 函数进行转换, 如 lodashramda 都提供有函数可以自动完成 curry, 这里写一个简单的实现:

1
2
3
4
5
6
7
8
9
10
11
var curry = (fn, length = fn.length) => {
var args = [];
var _curryN = (...arguments) => {
args = args.concat(Array.prototype.slice.call(arguments));
if (args.length == length) {
return fn.apply(null, args);
}
return _curryN;
};
return _curryN;
};

就是,生成一个 curry 函数,每次调用 curry 函数的时候计算函数的参数是否满足定义时候的参数数量,如果不满足,则缓存当前的参数,否则,把多次调用 curry 函数的参数传入原始函数执行。

中间的 _curryN 是个挺有用的工具,可以将一个接受多个参数的函数转化为接受部分参数的 curry 函数,将其进一步分离,并优化代码结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var curryN = (fn, length) => {
//对原始函数的包装,合并多次调用的柯里化的函数的参数,作为原始函数 fn 的参数,调用 fn
var _warpFunc = (fn, args) => (...arguments) => fn.apply(null, args.concat(Array.prototype.slice.call(arguments)));

return function () {
var args = Array.prototype.slice.call(arguments);
if (args.length < length) {
return curryN(_warpFunc(fn, args), length - args.length);
}
return fn.apply(null, args);
};
};

var curry = (fn, length = fn.length) => curryN(fn, length);

反柯里化 uncurry

反柯里化(uncurry)从字面上就可以看出和柯里化(curry)的含义正好相反,如果说柯里化的作用是固定部分参数,使函数针对性更强,那么反柯里化的作用就是扩大一个函数的应用范围,使一个函数适用于其他的对象。

如果说 curry 是预先传入一些参数,那么 uncurry 就是把原来已经固定的参数或者 this 上下文当作参数延迟来传递,也就是把 this.method 的调用模式转化成 method(this,arg1,arg2....),目的是创建一个更普适性的函数,可以被不同的对象使用。

比如,Array 上有一个 push 的方法,想让 push 这个函数不仅仅支持数组,还能够被其他对象使用:push(obj,args) ,如下:

1
2
3
4
5
6
7
8
var arr = [1, 2, 3];
arr.push(4);
// [1, 2, 3, 4]

var push = Array.prototype.push.unCurry();
var obj = {};
push(obj, 'a');
// Object {0: "a", length: 1}

javascript 里面,很多函数都不做对象的类型检测,而是只关心这些对象能做什么,如 ArrayStringprototype 上的方法就被特意设计成了这种模式,这些方法不对 this 的数据类型做任何校验,因此 obj 可以冒用 Arraypush 方法进行操作。这里再看一个 String 的例子:

1
2
3
var toUpperCase = String.prototype.toUpperCase.unCurry();
toUpperCase('js');
// JS

call 方法也可以被 unCurry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var a = {
name: 'a',
print: function () {
console.log(this.name);
},
change: function (name) {
this.name = name;
console.log(this.name);
},
};

a.print();
// a

var b = {
name: 'b',
};

a.print.call(b);
// b

var call = Function.prototype.call.unCurry();
call(a.print, b);
// b

call(a.print, b, 'bb');
a.name;
// "aa"

b.name;
// "bb"

unCurry 本身也是方法,它也可以被反柯里化:

1
2
3
4
var unCurry = Function.prototype.unCurry.unCurry();
var toUpperCase = unCurry(String.prototype.toUpperCase);
toUpperCase('js');
// JS

反柯里化的实现

实现的代码很简单,只有几行,但是比较绕:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**

- 为 Function 原型添加 unCurry 方法,这样所有的 function 都可以被修改适用范围;
- 需要返回一个修改后的适用范围的函数,此时需要借用 call 方法实现
- 但是需要处理参数,此时借用 apply 传入参数
*/

Function.prototype.unCurry = function () {
// _fn 在本例中是 Array.prototype.push
var _fn = this;
return function () {
// 这里有点绕,做个说明:
// return Function.prototype.call.apply(_fn, arguments);
//
// 等价于:
// var fCall = Function.prototype.call;
// return fCall.apply(_fn, arguments);
//
// 等价于:
// return _fn.fCall(...arguments);
//
// 等价于:
// return _fn.apply(arguments[0], [].slice.call(arguments, 1));
//
// 即修改 _fn 中的 this 指向第一个参数,在本例中是 obj,
// 剩下的参数传入原函数 _fn
return Function.prototype.call.apply(_fn, arguments);
};
};

柯里化扩展

实现 add(1)(2, 3)(4)(5) = 15 的效果。
很多人这里就犯嘀咕了:我怎么知道执行的时机?
其实,这里有个忍者技艺:valueOftoStringjs 在获取当前变量值的时候,会根据语境,隐式调用 valueOftoString 方法进行获取需要的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function currying(fn) {
var allArgs = [];

function next() {
var args = [].slice.call(arguments);
allArgs = allArgs.concat(args);
return next;
}
// 字符类型
next.toString = function () {
return fn.apply(null, allArgs);
};
// 数值类型
next.valueOf = function () {
return fn.apply(null, allArgs);
};

return next;
}
var add = currying(function () {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
});
---- 本文结束,感谢您的阅读 ----