函数柯里化与反柯里化

函数柯里化和反柯里化都是高阶函数的应用之一。本博客将简要介绍柯里化与反柯里化的具体实现方式和应用。

一. 柯里化

所谓的函数柯里化,就是把原本接收多个参数的函数分解转换为若干次连续的函数调用,在每次函数调用中依次传入参数,直至达到函数参数列表的上限。

具体实现方式就是部分求值:柯里化后的函数会接收一些参数,但是不会立即求值,而是返回一个新函数,将之前传入参数通过闭包的形式保存起来,等到被真正的求值时,再一次性把所有传入的参数进行求值。

例如:

//柯里化之前:
fucntion F(a, b, c, d){
	//......
}

F(a, b, c, d);

//柯里化
const curriedF = curry(F);

//柯里化之后,函数调用变为如下形式:
curriedF(a)(b)(c)(d);

从以上代码可以总结出柯里化函数的一个特点,就是经过柯里化后的函数,只要累计传入的参数个数不大于原本参数列表的数目,就不会立即执行该函数,而是返回一个新的函数,并等待新的参数输入。只有累计传入的参数的个数大于参数列表的数目,才会立即执行。实际上,柯里化的函数就是一个延迟执行的过程。

那么如何实现函数柯里化呢?

为了便于理解,首先介绍一种并不通用的curry函数,该函数允许你在两次传参的过程中,将所有的参数完整的传递给fn:

//其中fn是待柯里化的函数
function curry(fn, ...rest1){
	return function(...rest2){
		return fn(...rest1, ...rest2);
	}
}

//具体使用例子:
function add(a, b, c){
	return a + b + c;
}

const addCurry = curry(add, 1);
addCurry(2, 3);							//6

当然上面的例子稍微修改一下就可以实现一个自定义的硬绑定 bind 函数:


function bind(fn, context, ...rest1){
	return function(...rest2){
		fn.apply(context, [...rest1, ...rest2]);
	}
}

//或者直接添加到Function原型上(不建议直接修改内置构造函数的原型)
Function.prototype.bind = function(context, ...rest1){
	const _this = this;
	return function(...rest2){
		return _this.apply(context, [...rest1, ...rest2]);
	}
}

接下来将介绍如何实现一个更为通用的柯里化函数 curry:


function curry(fn){
	const newFn = function(...rest1){
		if (fn.length <= rest1.length){
			return fn(...rest1);
		}else {
			return function(...rest2){
				return newFn(...rest1, ...rest2);
			}
		}
	}
	
	return newFn;
}

//使用:
const foo = curry((a, b, c, d) => {
	console.log(a, b, c, d);
});

foo(1)(2)(3)(4);							//1 2 3 4
const bar = foo(1)(2)(3);
bar(5);										//1 2 3 5

以上代码的重点就在于,在第一次接收参数的时候就判断传入的参数的个数是否不小于函数的参数列表个数,如果是就立即执行。否则返回另一个函数,并接收后续的参数,然后递归这一个过程。

当然以上代码完全可以简化为箭头函数的形式,因为柯里化过程一般并不涉及动态this绑定的问题,不过这里为了增加可读性,还是以普通函数的形式给出。

实际上柯里化函数并不难,学会这种延迟执行的思想,可以巧妙的把它应用在我们日常的代码中。不过这也要求所使用的语言必须支持高阶函数,比如JavaScript这种语言,函数是第一等的公民,既可以作为函数参数,也可以作为函数的返回值。

不过有时候,我们很可能会混淆柯里化和偏函数的概念。偏函数实际上也是高阶函数的应用之一。下面将简单介绍一下偏函数的概念:

偏函数实际上就是返回一个包含预处理参数的新函数,以便之后可以进行调用。这么描述可能不太好把握,所以还是先看一个例子吧。

比如在使用JavaScript的过程中,可能经常需要判断变量的类型。而最可靠的办法往往是借用Object.prototype.toString()方法进行判断:


function typeJudge(variable){
	return Object.prototype.toString.call(variable).slice(8, -1)
}

const arr = [1, 2, 3];
console.log(typeJudge(arr));				//"Array"

const sym = Symbol("this is a symbol");
console.log(typeJudge(sym));				//"Symbol"

const und = undefined;
console.log(typeJudge(und));				//"Undefined"

const obj = {};
console.log(typeJudge(obj));				//"Object"

经过本人测试,这种方式能够检查一切类型,无论是基本类型还是引用类型。不过,有时候我们可能需要的是一个能够返回布尔值并判断传入参数是否为特定类型的函数,这时候就可以采用偏函数这种方法来构造这种函数:


function isType(type){
	return function(variable){
		return Object.prototype.toString.call(variable) === `[object ${type}]`;
	}
}

const isArray = isType("Array");
const isString = isType("String");

const arr = [1, 2, 3];
const str = "abc";
console.log(isArray(arr), isString(str), isArray(str));				//true true false

这里使用偏函数使得构造任一判断类型的函数非常的方便。但是偏函数并不是柯里化,一定要记住这一点。

介绍完柯里化之后,接下来将介绍一个更加有趣的概念:反柯里化。

二. 反柯里化

反柯里化技术可以让任何对象拥有原生对象的方法。实现反柯里化的代码如下:


Function.prototype.uncurry = function(){
	const _this = this;
	return function(...rest){
		//实际上希望达到的效果就是例如:return Array.prototype.push.call(_this, ...rest),这样是否更加直观了呢?
		return Function.prototype.call.apply(_this, rest);
	}
}

这段代码乍一看可能不太好理解,特别是连续调用call和apply方法,实际上apply方法只是用来分解rest数组将其中元素作为函数参数的。例如apply常见的用法就是展开数组中的元素并作为参数逐个传入到函数中:fn.apply(null, [1, 2, 3])。接下来的一个例子将使你更进一步理解以上代码的含义:

假如我们现在希望能够构造一个newPush函数(该push方法就类似于原生的数组中的push方法那样),该函数接收两个参数,一个是待push新元素的数组或者是对象,另一个则是需要被push的新元素:


const newPush = Array.prototype.push.uncurry();

let arr = [1, 2, 3];
newPush(arr, 4);
console.log(arr);					//(4) [1, 2, 3, 4]

let obj = {};
newPush(obj, "first");
console.log(obj);					//{0: "first", length: 1}

实际上,反柯里化的思想也很简单。在我们平时开发使用原生对象提供的方法时,一般都是采用XXX.push的形式,但是我们希望提供一种push(XXX, parmater1, parmater2, …)形式的函数。这样,任意对象都可以把此种形式的方法作为自己的属性进行调用,就像拥有原生方法那样。

反柯里化实际上是一个挺有意思的特性,了解一下也是不错的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注