1213 字
6 分钟
函数柯里化

柯里化#

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

例如需要实现以下得示例

add(1, 2, 3) // 6
add(1) // 1
add(1)(2) // 3
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10

本来有这么一个求和函数dynamicAdd(),接受任意个参数。

function dynamicAdd() {
return [...arguments].reduce((prev, curr) => {
return prev + curr
}, 0)
}

现在需要通过柯里化把它变成一个新的函数,这个新的函数预置了第一个参数,并且可以在调用时继续传入剩余参数。

function curry(fn,firstArg){
// 返回一个新函数
return function(){
// 将arguments转化为真正数组
var restArgs = Array.from(arguments);
return fn.apply(this,[firstArg,...restArgs])
}
}
// 柯里化,预置参数10
var add10 = curry(dynamicAdd, 10)
add10(5); // 15
// 柯里化,预置参数20
var add20 = curry(dynamicAdd, 20);
add20(5); // 25
// 也可以对一个已经柯里化的函数add10继续柯里化,此时预置参数10即可
var anotherAdd20 = curry(add10, 10);
anotherAdd20(5); // 25

柯里化是在一个函数的基础上进行变换,得到一个新的预置了参数的函数。最后在调用新函数时,实际上还是会调用柯里化前的原函数。 并且柯里化得到的新函数可以继续被柯里化

实际使用时也会出现柯里化的变体,不局限于只预置一个参数

function curry(fn) {
// 保存预置参数
var presetArgs = [].slice.call(arguments, 1)
// 返回一个新函数
return function() {
// 新函数调用时会继续传参
var restArgs = [].slice.call(arguments)
// 参数合并,通过apply调用原函数
return fn.apply(this, [...presetArgs, ...restArgs])
}
}

1. 参数定长的柯里化#

假设存在一个原函数fnfn接受三个参数a, b, c,那么函数fn最多被柯里化三次(有效地绑定参数算一次)。

function fn(a, b, c) {
return a + b + c
}
var c1 = curry(fn, 1);
var c2 = curry(c1, 2);
var c3 = curry(c2, 3);
c3(); // 6
// 再次柯里化也没有意义,原函数只需要三个参数
var c4 = curry(c3, 4);
c4();

也就是说,我们可以通过柯里化缓存的参数数量,来判断是否到达了执行时机。那么我们就得到了一个柯里化的通用模式。

function curry(fn) {
// 获取原函数的参数长度
const argLen = fn.length;
// 保存预置参数
const presetArgs = [].slice.call(arguments, 1)
// 返回一个新函数
return function() {
// 新函数调用时会继续传参
const restArgs = [].slice.call(arguments)
const allArgs = [...presetArgs, ...restArgs]
if (allArgs.length >= argLen) {
// 如果参数够了,就执行原函数
return fn.apply(this, allArgs)
} else {
// 否则继续柯里化
return curry.call(null, fn, ...allArgs)
}
}
}
function fn(a,b,c){
return a+b+c;
}
var curried = curry(fn);
curried(1,2,3);
curried(1,2)(3);
curried(1)(2,3);
curried(1)(2)(3);

2. 参数不定长地柯里化#

参数不定长的情况下,要同时支持1~N次调用还是挺难

如果要支持参数不定长的场景,已经柯里化的函数在执行完毕时不能返回一个值,只能返回一个函数;同时要让JS引擎在解析得到的这个结果时,能求出我们预期的值

  1. curry处理,得到一个新函数,这一点不变。

// curry是一个函数 var curried = curry(add);

```

2. 新函数执行后仍然返回一个结果函数。

// curried10也是一个函数
var curried10 = curried(10);
var curried30 = curried10(20);
  1. 结果函数可以被Javascript引擎解析,得到一个预期的值。
    curried10; // 10

关键点在于3,如何让Javascript引擎按我们的预期进行解析,这就回到Javascript基础了。在解析一个函数的原始值时,会用到toString

function curry(fn) {
// 保存预置参数
const presetArgs = [].slice.call(arguments, 1)
// 返回一个新函数
function curried () {
// 新函数调用时会继续传参
const restArgs = [].slice.call(arguments)
const allArgs = [...presetArgs, ...restArgs]
return curry.call(null, fn, ...allArgs)
}
// 重写toString
curried.toString = function() {
return fn.apply(null, presetArgs)
}
return curried;
}

总结#

柯里化是一种函数式编程思想,实际上在项目中可能用得少,或者说用得不深入,但是如果你掌握了这种思想,也许在未来的某个时间点,你会用得上!

大概来说,柯里化有如下特点:

  • 简洁代码:柯里化应用在较复杂的场景中,有简洁代码,可读性高的优点。
  • 参数复用:公共的参数已经通过柯里化预置了。
  • 延迟执行:柯里化时只是返回一个预置参数的新函数,并没有立刻执行,实际上在满足条件后才会执行。
  • 管道式流水线编程:利于使用函数组装管道式的流水线工序,不污染原函数。
函数柯里化
https://nollieleo.github.io/posts/函数柯里化/
作者
翁先森
发布于
2020-08-24
许可协议
CC BY-NC-SA 4.0