最近在学习函数式编程,看到柯里化函数这个东东,原以为是个新的概念,没想到一查,竟然老早就存在了,且已经成熟运用多年,深感惭愧啊,我到现在都还没接触过。
官方解释
先是查看了柯里化的介绍
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
这句话听起来有点拗口啊,反正我没能完全理解,不过没关系,我们可以查看网上大牛们的解释,经过一顿搜索,算是屡清楚了头绪。
我的理解
我们先来看个栗子,这是一个求和的函数,求4个参数的和,那么我们在使用这个函数的时候,就要先知道每个参数的具体的值,然后才能调用这个函数。
function add(a, b, c, d) {
return a + b + c + d;
};
console.log(add(2,3,4,5)) // 14
复制代码
而假如,我们想让调用更加灵活一些,比如改成下面这种调用方式
console.log(add(2)(3)(4)(5)) // 14
复制代码
可以看到,这种调用方式有个好处,不需要等待所有参数明确后在执行具体方法,当在参数不满足的情况下,每次调用返回的是函数,比如 add(2)
返回的是一个函数,返回的函数不断的接受新的参数,直到满足预定好的4个参数时,便输出结果。 而 add(2,3,4,5) => add(2)(3)(4)(5)
这一个过程便是函数柯里化的过程。这样子解释,不知道大伙好理解不?
talk is cheap , show me the code,ok 那我们看下具体的实现,先写一个函数将 add()
转换成柯里化函数
/**
* 转换函数
* @param {*} fn 目标函数
* @param {...any} args 其他参数
*/
const createCurry = (fn, ...args) => {
// 获取目标函数的参数个数
let length = fn.length;
return (...rest) => {
// 将已有的参数和新的参数合并
let allArgs = args.slice(0);
allArgs.push(...rest);
// 若参数个数已经满足目标函数的参数要求,则执行目标函数。否则继续返回转换函数
if (allArgs.length < length) {
return createCurry.call(this, fn, ...allArgs)
} else {
return fn.apply(this, allArgs)
}
}
}
function add(a, b, c, d) {
return a + b + c + d;
};
复制代码
使用如下:
const curryAdd = createCurry(add,2);
const sum = curryAdd(3)(4)(5);
console.log(sum) // 14
复制代码
转换柯里化函数有个关键的点,那就是要明确触发条件,比如说上面的栗子中,我们的触发条件是参数的个数,根据参数的个数来区分返回的是函数还是具体的结果。
柯里化函数的特点
通过上面的栗子我们可以看出柯里化函数有这么几个特点:
- 参数复用
- 业务解耦,调用时机灵活
- 延迟执行,部分求值
继续举栗子
光说理论太枯燥,那么我们把上面的栗子在稍微扩展下,把add
函数改成不定参数,就是说,我可以传n个参数,求这n个参数的和,调用如下
const curryAdd = createCurry(add);
const sum = curryAdd(3)(4)(5); // 12
const sum = curryAdd(3)(4)(5)(6); // 18
复制代码
OK,首先我们要对add
函数做下修改
/**
* 不定参数求和
* @param {...any} arg
*/
function add(...arg){
return arg.reduce((result,value)=>{ return result += value },0)
}
复制代码
那么这个函数使用就变成 add(1,2,3)
,但我们的目标是这样的 add(1)(2)(3)
,在写转换函数之前,我们先要确定我们的触发条件,我想到的条件是:当新传入的参数个数为 0 的时候,就是没有参数,就执行目标函数,否则返回函数。那么柯里化函数的调用方式需要稍微改成如下
add(1)(2)(3)();
复制代码
具体的转换函数和调用如下
const createCurry = (fn, ...args) => {
return (...rest) => {
let allArgs = args.slice(0);
allArgs.push(...rest)
// 触发条件
if (rest.length !== 0) {
return createCurry.call(this, fn, ...allArgs)
} else {
return fn.apply(this, allArgs)
}
}
}
const curryAdd = createCurry1(add);
console.log(curryAdd(1)(2)(3)(4)()) // 10
复制代码
柯里化到底是什么?
通过上面两个小栗子,大伙应该有个大致概念了,所以柯里化是一种设计思想,主动掌握了函数的控制权,根据我们的需要,设定相应的触发机制。有句话,让代码编写代码,柯里化算是有那么点意思吧,函数生成函数,函数去调用函数,我们只要编写原子性的函数和设定好条件。
再来两个栗子
数组处理
写个通用的数组处理柯里化函数,currying
的第一个参数是目标函数,currying
直接返回函数,没有特定的触发条件,在使用mapSQ([1, 2, 3, 4, 5])
会将数组和currying
的第二个参数一起传给目标函数,即currying
第一个参数,详细如下:
function currying(fn) {
var slice = Array.prototype.slice,
__args = slice.call(arguments, 1);
return function () {
var __inargs = slice.call(arguments);
return fn.apply(null, __args.concat(__inargs));
};
}
function square(i) {
return i * i;
}
function double(i) {
return i *= 2;
}
function map(handeler, list) {
return list.map((value) => {
return handeler(value)
});
}
var mapSQ = currying(map, square); // 平方
mapSQ([1, 2, 3, 4, 5]); //[1, 4, 9, 16, 25]
var mapTwo = currying(map, double); // 两倍
mapSQ([1, 2, 3, 4, 5]); //[2,4,6,8,10]
复制代码
以参数长度为条件的转换函数
这个栗子是在 30secondsofcode 上看到,我觉得写得非常精简直观,给大家分享下
const curry = (fn, arity = fn.length, ...args) => arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);
curry(Math.pow)(2)(10); // 1024
curry(Math.min, 3)(10)(50)(2); // 2
复制代码
官方写成了一行,阅读可能不太方便,我改成通俗版,加了些注释
/**
* 生成柯里化函数,以参数长度达标作为触发条件
* @param {*} fn 目标函数
* @param {*} arity 目标函数参数个数
* @param {...any} args 调用传入的参数
*/
const curry = (fn, arity = fn.length, ...args) => {
if (arity <= args.length) {
return fn(...args)
} else {
return curry.bind(null, fn, arity, ...args);
}
}
复制代码
小结
函数柯里化可以给我们带来很多想象,可以将耦合的业务逻辑拆解,使得函数编程更加纯粹。不过我个人觉得柯里化函数要是太复杂,对大大降低代码的可阅读性和可维护性,所以柯里化虽然看着高大上,但还是不能滥用。