偏函数和柯里化解决的最核心的两个问题:
- 函数组合链中的多元参数的问题
- 函数的逻辑复用的问题
回顾一下上一篇的函数们,不难发现,它们都是一元函数,这种属于最理想的情况了,但是实际开发中,往往在处理的过程中会夹杂进来其他的参数,比如这样:
function multiply(x, y) {
return x*y
}
此时我们没办法使用之前的pipe方法了,所以我们的想法当然是,能不能把多餐函数通过某种方式转位单参函数,也就是参数对齐。
任何时候,只要我们想对函数的入参数量进行改造,必须先想到柯里化&偏函数。
柯里化
柯里化就是把一个n元函数,改造为n个相互嵌套的一元函数的过程。
举个例子:
function addThreeNum(a, b, c) {
return a+b+c
}
正常调用就是:addThreeNum(1, 2, 3)
但是柯里化之后,调用方式变成了这样:addThreeNum(1)(2)(3)
// 将原函数改造为三个嵌套的一元函数
function addThreeNum(a) {
// 第一个函数用于记住参数a
return function(b) {
// 第二个函数用于记住参数b
return function(c) {
// 第三个函数用于执行计算
return a+b+c
}
}
}
// 输出6,输出结果符合预期
addThreeNum(1)(2)(3)
但是这种方式其实是改变了addThreeNum这个方法,有没有一种方式,不改变方法本身,还能达到同样的效果呢?
有的,那就是使用高阶函数。
// 定义高阶函数 curry
function curry(addThreeNum) {
// 返回一个嵌套了三层的函数
return function addA(a) {
// 第一层“记住”参数a
return function addB(b) {
// 第二层“记住”参数b
return function addC(c) {
// 第三层直接调用现有函数 addThreeNum
return addThreeNum(a, b, c)
}
}
}
}
// 借助 curry 函数将 add
const curriedAddThreeNum = curry(addThreeNum)
// 输出6,输出结果符合预期
curriedAddThreeNum(1)(2)(3)
偏函数
柯里化是将n元函数变为n个一元函数。
偏函数说的是一个 n 元函数变成一个 m(m < n) 元函数。
对于柯里化来说,不仅函数的元发生了变化,函数的数量也发生了变化(1个变成n个)。
对于偏函数来说,仅有函数的元发生了变化(减少了),函数的数量是不变的。
那么怎样使用偏函数实现参数对齐呢?
function multiply(x, y) {
return x * y
}
function displayMultiply(fun, num) {
function wrapperFun(inputNum) {
return fun(inputNum, num)
}
return wrapperFun
}
const multiply3 = displayMultiply(multiply, 3)
console.log(multiply3(2))
其实偏函数和柯里化都是“调整函数的元”的问题。
那怎样实现自动柯里化?自动完成这个套完的过程呢?
我们来分析一下,也就是说,不管我的函数传入多少参数,curry都应该分析出参数的数量,并且动态的根据参数的数量,自动的做嵌套。
我们简单的来拆分一下这个函数的任务:
1.计算出传入参数的数量。
2.根据数量,自动写出递归的层数。
3.在嵌套的最后一层,调用回调函数,传入所有参数。
// curry 函数借助 Function.length 读取函数元数
function curry(func, arity=func.length) {
// 定义一个递归式 generateCurried
function generateCurried(prevArgs) {
// generateCurried 函数必定返回一层嵌套
return function curried(nextArg) {
// 统计目前“已记忆”+“未记忆”的参数
const args = [...prevArgs, nextArg]
// 若 “已记忆”+“未记忆”的参数数量 >= 回调函数元数,则认为已经记忆了所有的参数
if(args.length >= arity) {
// 触碰递归边界,传入所有参数,调用回调函数
return func(...args)
} else {
// 未触碰递归边界,则递归调用 generateCurried 自身,创造新一层的嵌套
return generateCurried(args)
}
}
}
// 调用 generateCurried,起始传参为空数组,表示“目前还没有记住任何参数”
return generateCurried([])
}
简洁写法:
function curry2(fn) {
return function curried(...args) {
if (args.length < fn.length) {
return function () {
return curried(...args, ...arguments)
}
}
return fn(...args)
}
}
举个例子
function add(a, b) {
return a + b
}
function multiply(a, b, c) {
return a*b*c
}
function addMore(a, b, c, d) {
return a+b+c+d
}
function divide(a, b) {
return a/b
}
我们需要首先对四个函数分别作“一元化”处理。
这“一元化”处理的第一步,就是借助 curry 函数把它们各自的传参方式重构掉:
const curriedAdd = curry(add)
const curriedMultiply = curry(multiply)
const curriedAddMore = curry(addMore)
const curriedDivide = curry(divide)
然后对这些函数逐个传参,传至每个函数只剩下一个待传参数为止。这样,我们就得到了一堆一元函数:
const compute = pipe(
curriedAdd(1),
curriedMultiply(2)(3),
curriedAddMore(1)(2)(3),
curriedDivide(300)
)