浅谈函数柯里化

423 阅读6分钟

什么是函数柯里化

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

简单来说,就是固定一个需要多个参数的函数的部分参数,返回一个接受剩余参数的函数,当然这个转化流程需要固定多少个参数,返回的函数需要接收多少个参数,这都是有开发者自己定义的,当然,这两个的个数相加会等于这个函数原始情况需要的参数个数。

函数柯里化的作用是什么

上面简单的说明过了函数柯里化的作用,固定某些参数,得到一个接收余下参数的函数

这能够给我们带来的就是

  1. 参数复用,在不侵入原函数的情况下,为原函数预置通用参数。
  2. 提前返回,在一个复杂函数柯里化之后,能够得到一个固定完部分逻辑的函数。
  3. 延迟执行,得到函数以后,执行的时机点也还是能够由开发者决定。

参数复用

假如说现在设计一个函数 add 这个函数接收两个参数 xy ,返回的结果就是两个数相加。

但是当我们需要多次调用这个函数并且每次 x 的值都是固定的的时候。

function add(x, y) {
    return x + y
}
add(1,2)
add(1,3)
add(1,4)
add(1,5)
add(1,6)
add(1,7)

我们可以稍微改造一下上面的函数,来减少多次调用需要传入的参数。

function add(x, y) {
  return x + y
}
const curryingAdd = function(x){
  return (y)=>add(x, y)
}
const oneAdd = curryingAdd(1)
console.log(oneAdd(2));
console.log(oneAdd(3));
console.log(oneAdd(4));
console.log(add(1,2));
console.log(add(1,3));
console.log(add(1,4));

在将 add 改造成 curryingAdd 之后,我们就可以用一个数先缓存下第一个参数,在之后的多次调用中就不需要再次传入。

提前返回

async function getData (params) {
  params.a = 1
  console.log(params);
  let data = await new Promise((res, rej) => { setTimeout(() => res({ a: 1 }), 300 )})
  return data
}
console.log(getData({}));

假设我们需要请求一份数据,一般是这样进行请求,如果 params 涉及到复杂的逻辑判断,那么每次请求都会重新执行一次,但是这些再第一次请求过后就知道了,是没有必要的开销。

function curryingGetData (params) {
  params.a = 1
  console.log(params);
  return async () => {
    console.log(params);
    let data = await new Promise((res, rej) => { setTimeout(() => res({ a: 1 }), 300 )})
    return data
  }
}

const getData2 = curryingGetData({})
  
console.log(getData2());

通过函数柯里化,将第一次拿到的参数缓存,之后每次调用获取数据都不会重复执行 params 的处理逻辑。

这里只是举一个例子,实际使用可能不会这么写。

延迟执行

上面的两个例子应该也都体现出了延迟执行的特点,返回的函数都不会立刻被调用,并且在部分场景还可以不断的柯里化,累积传入的参数,把调用的时机点延后到了新函数被执行的时候。

函数柯里化的优缺点

优点:

  • 更好的复用性,柯里化可以使得函数变得可复用,不需要传入一些重复的参数。
  • 更高的适用性,通过传入某些固定的参数,降低了适用的范围,提高了函数的适用性

缺点:

  • 可读性降低,柯里化会使得函数的调用又嵌套了多层,这在其他人需要修改这底下的代码的时候,无疑会使一种负担。

函数柯里化使用的例子

bind 函数

bind 函数就是一个很典型的柯里化使用场景,让我们先看一下 bind 的简单使用。

const obj = { name: 'Alice' };
function sayHello(a,b) {  
    console.log(a)
    console.log(b)
    console.log(`Hello, ${this.name}`);
}
const boundFunc = sayHello.bind(obj, 1);
boundFunc(2); 
// 输出:
// 1
// 2
// Hello, Alice

从上面的例子我们不难看出 bind 的作用,改变一个函数的 this 指向,并且可以传递可选的参数进行预设参数。这其中的预设参数的作用,正式柯里化函数的实践。

然后来看一下 bind 的原生实现方法:

(function(){
//context就是传入的obj用来改变this指向的,如果没有就默认写的是window
function myBind(context=window,...outerArgs){    
  let _this = this;
  return function(...innerArgs){
    _this.call(context,...innerArgs.concat(outerArgs))
  }
}
Function.prototype.myBind = myBind;
})()

image-20230926172004777.png

可以看到,通过柯里化的方式返回一个新的函数,call 用于改变新函数的 this 指向,outerArgs 就作为预设参数缓存了下来。

immer之produce函数

看一下在 react 中引入了 produce 后两种修改状态的写法

import {produce} from 'immer';

...

this.setState((prevState) => {
  return produce(prevState, draftState =>{
	draftState.address.city.area = 'JingAn';
	draftState.address.city.postcode = draftState.address.city.postcode + 10;
  });
});

或者

this.setState(produce(draftState => {
  draftState.address.city.area = "JingAn";
  draftState.address.city.postcode = draftState.address.city.postcode + 10;
}));

很明显第二种写法采用了柯里化的写法,在 Immer 源码中的 immerClass.ts 这个类上我们能够找到它的定义:

/**
 * The `produce` function takes a value and a "recipe function" (whose
 * return value often depends on the base state). The recipe function is
 * free to mutate its first argument however it wants. All mutations are
 * only ever applied to a __copy__ of the base state.
 *
 * Pass only a function to create a "curried producer" which relieves you
 * from passing the recipe function every time.
 *
 * Only plain objects and arrays are made mutable. All other objects are
 * considered uncopyable.
 *
 * Note: This function is __bound__ to its `Immer` instance.
 *
 * @param {any} base - the initial state
 * @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified
 * @param {Function} patchListener - optional function that will be called with all the patches produced here
 * @returns {any} a new state, or the initial state if nothing was modified
 */
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
	if (typeof base === "function" && typeof recipe !== "function") {
		const defaultBase = recipe 
		recipe = base 

		const self = this
		return function curriedProduce(  // 返回一个新的函数
			this: any, 
			base = defaultBase,
			...args: any[]
		) {
			return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args))
		}
	}

	if (typeof recipe !== "function") die(6)
	if (patchListener !== undefined && typeof patchListener !== "function")
		die(7)

	let result

	// Only plain objects, arrays, and "immerable classes" are drafted.
	if (isDraftable(base)) {
		const scope = enterScope(this) 
		const proxy = createProxy(this, base, undefined) 
		let hasError = true
		try {
			result = recipe(proxy) 
			hasError = false
		} finally {
			if (hasError) revokeScope(scope)
			else leaveScope(scope)
		}
		if (typeof Promise !== "undefined" && result instanceof Promise) {
			return result.then(
				result => {
					usePatchesInScope(scope, patchListener)
					return processResult(result, scope)
				},
				error => {
					revokeScope(scope)
					throw error
				}
			)
		}
		usePatchesInScope(scope, patchListener)
		return processResult(result, scope)
	} else if (!base || typeof base !== "object") {
		result = recipe(base)
		if (result === NOTHING) return undefined
		if (result === undefined) result = base
		if (this.autoFreeze_) freeze(result, true)
		return result
	} else die(21, base)
}

可以在最开始的判断中看到,柯里化的用法就是在最开始的时候去判断第一个参数是否是一个函数,是的话就会缓存数据,并且最后返回的还是标准的调用方法。

总结

在这里为止简单的举了两个柯里化函数的使用例子,当然不仅仅是在这两个地方,还有更多的我们耳熟能详的函数当中使用了它,在这里就不做过多的介绍,但是我们要清楚的是,柯里化是十分重要的,在函数式编程当中,你随处都可以看见它的应用