JS 柯里化 (curry)
用 JS 理解柯里化
函数式编程风格,试图以函数作为参数传递(回调)和无副作用的返回函数(修改程序的状态)。
很多语言采用了这种编程风格。JavaScript,Haskell,Clojure,Erlang 和 Scala 是其中最受欢迎的几种语言。
数式编程风格具有传递和返回函数的能力,它带来了许多概念:
- Pure Functions(纯函数)
- Currying(柯里化)
- Higher-Order functions(高阶函数)
我们将在这里介绍这些概念中的一种 Currying。
在本文中,我们将看到 currying 如何工作,以及它如何在软件开发人员的工作中发挥作用。
提示:您可以将其变为 Bit 组件,而不是复制粘贴可重用的 JS 功能,并快速与团队共享项目。
Bit - 使用代码组件共享和构建
Bit 可帮助您在项目和应用程序之间共享,发现和使用代码组件,以构建新功能和... bitsrc.io
什么是 Currying?
Currying 是函数式编程中的一个过程,我们可以将具有多个参数的函数转换为顺序嵌套的函数。它返回一个接收下一个参数的新函数。
它不断返回一个新函数(接收当前参数,就像我们之前所说的那样),直到所有参数都用完为止。保留参数 "alive"
(通过闭包),并且当返回并执行 currying 链中的最终函数时,所有参数都在执行中使用。
Currying 是将具有多个 arity 的函数转换为具有较少 arity 的函数的过程 - Kristina Brainwave
注意:术语 arity
“是指函数所接收的参数个数”。例如,
function fn (a, b) {
// ...
}
function _fn(a, b, c) {
// ...
}
函数 fn
接受两个参数(2-arity 函数),_fn
接受三个参数(3-arity 函数)。
因此,currying 将具有多个参数的函数转换为一系列函数,每个函数都接收一个参数。
我们来看一个简单的例子:
function multiply(a, b, c) {
return a * b * c;
}
此函数接收三个数字,将数字相乘并返回结果。
multiply(1, 2, 3) // 6
请参阅我们如何使用完整参数调用 multiply 函数,来创建一个 curried
版本函数,看看我们如何在一系列调用中调用相同的函数(并得到相同的结果):
function multiply(a) {
return (b) => {
return (c) => {
return a * b * c;
}
}
}
log(multiply(1)(2)(3)) // 6
我们已将 multiply(1,2,3)
函数调用转为 multiply(1)(2)(3)
多个函数调用。
我们已经将一个函数转为一系列函数。为了得到三个数相乘的结果 1
,2
和 3
三个数字一个接一个地传递,每个数字都将在下一个函数的内部调用。我们可以将 multiply(1)(2)(3)
分开以便更好地理解它:
const mul1 = multiply(1);
const mul2 = mul1(2);
const result = mul2(3);
log(result); // 6
让我们一个接一个地接收参数。我们把 1
传递给了 multiply
函数:
let mul1 = multiply(1);
它返回这个函数:
return (b) => {
return (c) => {
return a * b * c;
}
}
现在,mul1
保持上面的函数定义,它带有一个参数 b
。
我们调用了 mul1
函数,传入 2
:
let mul2 = mul1(2);
在 mul2
中将返回第三个函数:
return (c) => {
return a * b * c;
}
返回的函数现在存储在 mul2
变量中。
从本质上讲,mul2
将是:
mul2 = (c) => {
return a * b * c;
}
当 mul2
以 3
作为参数调用时,
const result = mul2(3);
它把之前传入的参数 a = 1
,b = 2
带入计算并返回结果 6
。
log(result); // 6
作为嵌套函数,mul2
可以访问外部函数 multiply
以及 mul1
作用域内的变量。
这就是为什么 mul2
能使用已经在的函数中定义的变量并执行乘法运算的原因。虽然这些函数早已在内存中被回收 garbage collected
,但它的变量仍以某种方式保留 "alive"
。
您会看到三个数字一次一个地应用于该函数,并且每次都返回一个新函数,直到所有数字都用完为止。
让我们看另一个例子:
function volume(l, w, h) {
return l * w * h;
}
const aCylinder = volume(100, 20, 90); // 180000
我们有一个计算实心物体体积的函数 volume
。
柯里化后的版本将接受一个参数并返回一个函数,该函数也将接收一个参数并返回一个函数。这个过程将被循环/继续,直到到达最后一个参数并返回最后一个函数,这将执行与前一个参数和最后一个参数的乘法运算。
function volume(l) {
return (w) => {
return (h) => {
return l * w * h;
}
}
}
const aCylinder = volume(100)(20)(90) // 180000
与我们在 multiply
函数中所使用的一样,最后一个函数只接受 h
,但是它将执行的操作所用到的其他函数作用域内的变量早已被函数返回。因为闭包 Closure ,这些参数仍然有效。
currying 背后的想法是接收一个函数并返回一个特殊函数的函数。
数学中的 Currying
我有点喜欢数学例证👉 维基百科给出了进一步展示 currying 的概念。让我们用自己的例子来看看它。如果我们有一个等式:
f(x,y) = x^2 + y = z
有两个变量 x 和 y。如果这两个变量给出值 x=3
和 y=4
,将得出 z
的值。
如果我们在 f(x,y)
中替换 y
为 4
和 x
为 3
:
f(x,y) = f(3,4) = x^2 + y = 3^2 + 4 = 13 = z
我们得到结果 13
。
我们可以柯里化 f(x,y)
在一系列函数中提供变量:
h = x^2 + y = f(x,y)
hy(x) = x^2 + y = hx(y) = x^2 + y
[hx => w.r.t x] and [hy => w.r.t y]
注意:hx 是下标为 x 的 h 变量,hy 是下标为 y 的 h 变量。wrt 是关系表达式。
如果我们修正等式 hx(y) = x^2 + y
中的变量 x=3
,它将返回一个以 y
作为变量的新等式:
h3(y) = 3^2 + y = 9 + y
注意:h3 是下标为3的 h
它和下面的表达式是一样的:
h3(y) = h(3)(y) = f(3,y) = 3^2 + y = 9 + y
该值尚未解决,它返回了一个 9 + y
接收另一个变量 y
的新方程式。接下来,我们传入 y=4
:
h3(4) = h(3)(4) = f(3,4) = 9 + 4 = 13
y
在变量链中是最后一个变量,加法操作是在前一个变量 x = 3
仍保留并且值被解析的情况下执行的,最终得到结果 13
。
实际上,我们将等式 f(x,y) = 3^2 + y
柯里化为一系列方程:
3^2 + y -> 9 + y
f(3,y) = h3(y) = 3^2 + y = 9 + y
f(3,y) = 9 + y
f(3,4) = h3(4) = 9 + 4 = 13
最后得出结果。
哇!!这是一些数学问题,如果你觉得这个不够清楚😕。你可以阅读👉 维基百科的全部具体内容。
Currying 和 Partial Function 应用
现在,有些人可能会开始认为 curried 函数的嵌套函数的数量取决于它接收的参数的数量。是的,那就是 curry。
我可以设计柯里化函数 volume 是这样的:
function volume(l) {
return (w, h) => {
return l * w * h;
}
}
所以它可以像这样调用:
const hCy = volume(70);
hCy(203, 142);
hCy(220, 122);
hCy(120, 123);
要么
volume(70)(90, 30);
volume(70)(390, 320);
volume(70)(940, 340);
我们刚刚定义了一个专门的函数来计算长度为 70
(l
)的任意圆柱体的体积。
它需要 3
个参数并具有 2
个嵌套函数,不像我们以前的版本需要 3
参数并具有 3
个嵌套函数。
这个版本不是柯里化。我们刚刚部分应用了 volume 函数。
Currying 和 Partial Application 是相关的,但它们有不同的概念。
部分应用程序将函数转换为具有较少参数的另一个函数。
function acidityRatio(x, y, z) {
return performOp(x, y, z);
}
|
V
function acidityRatio(x) {
return (y,z) => {
return performOp(x,y,z)
}
}
注意:我故意省略了 performOp
函数的实现。这里没有必要全部展示。但你必须知道 currying 和部分应用背后的概念。
这是 acidityRatio 函数的部分应用。不涉及到柯里化的问题。acidityRatio 函数部分应用于接受较少的 arity,期望参数少于其原始函数。
为了实现柯里化,它会是这样的:
function acidityRatio(x) {
return (y) => {
return (z) => {
return performOp(x, y, z);
}
}
}
Currying 根据函数的参数个数创建嵌套函数。每个函数都接收一个参数。如果没有参数,那就没有柯里化了。
Currying 适用于具有2个以上的参数的函数 —— Wikipedia
Carrying 将函数转换为函数序列,每个函数都只有一个参数。
可能存在这样一种情况,即 currying 和部分应用彼此相遇。假设我们有一个功能:
function div(x, y) {
return x/y;
}
如果我们部分应用它。我们将得到:
function div(x) {
return (y) => {
return x/y;
}
}
此外,currying 将给我们相同的结果:
function div(x) {
return (y) => {
return x/y;
}
}
虽然 currying 和 部分应用函数给出了相同的结果,但它们是两个不同的实体。
就像我们之前说的那样,currying 和部分应用是相关的,但实际上并没有相同的设计。他们之间的共同点是依靠闭包来工作。
Currying 有用吗?
当然有用,当您想要时,currying 会派上用场:
1. 编写可以轻松重用和配置的小代码模块,就像我们使用 npm 一样:
例如,您拥有一家商店🏠并希望为您的客户提供10%💵折扣:
function discount(price, discount) {
return price * discount;
}
当一个客户购买价值500美元的商品时,你会给他的折扣会是:
const price = discount(500, 0.10); // 50
// $500 - $50 = $450
你看,从长远来看,我们会发现自己每天都在计算10%的折扣。
const price = discount(1500,0.10); // $150
// $1,500 - $150 = $1,350
const price = discount(2000,0.10); // $200
// $2,000 - $200 = $1,800
const price = discount(50,0.10); // $5
// $50 - $5 = $45
const price = discount(5000,0.10); // $500
// $5,000 - $500 = $4,500
const price = discount(300,0.10); // $30
// $300 - $30 = $270
我们可以柯里化 discount 函数,并不总是添加0.10的折扣:
function discount(discount) {
return (price) => {
return price * discount;
}
}
const tenPercentDiscount = discount(0.1);
现在,我们只需计算您的客户购买商品的价格:
tenPercentDiscount(500); // $50
// $500 - $50 = $450
同样,有些优惠客户比另一些优惠客户更重要 - 我们称之为超级客户。我们希望为超级客户提供20%的折扣。
我们使用我们的柯里化后的 discount 函数:
const twentyPercentDiscount = discount(0.2);
我们通过向柯里化函数 discount 中传入 0.2
即 20%
的折扣值,为我们的超级客户设置新函数 。
返回的函数 twentyPercentDiscount
将用于计算我们的超级客户的折扣:
twentyPercentDiscount(500); // 100
// $500 - $100 = $400
twentyPercentDiscount(5000); // 1000
// $5,000 - $1,000 = $4,000
twentyPercentDiscount(1000000); // 200000
// $1,000,000 - $200,000 = $600,000
2. 避免频繁调用具有相同参数的函数:
例如,我们有一个计算圆柱体积的函数:
function volume(l, w, h) {
return l * w * h;
}
碰巧仓库中的所有气缸的高度都为100米。你会看到,你会反复调用 h
为 100
的函数:
volume(200,30,100) // 2003000l
volume(32,45,100); //144000l
volume(2322,232,100) // 53870400l
要解决这个问题,你可以调整 volume
函数(就像我们之前做的那样):
function volume(l) {
return (w) => {
return (h) => {
return l * w * h;
}
}
}
我们可以为特定高度的圆柱定义一个特定的函数:
const hCylinderHeight = volume(100);
hCylinderHeight(200)(30); // 600,000l
hCylinderHeight(2322)(232); // 53,870,400l
General Curry Function 通用柯里化函数
让我们开发一个函数,它接受任何函数并返回函数的 curried 版本。
要做到这一点,我们将有这个方法(虽然你自己的方法可能与我的不同):
function curry(fn, ...args) {
return (..._arg) => {
return fn(...args, ..._arg);
}
}
我们在这做了什么?我们的柯里化函数接受我们想要柯里化的函数(fn)和可变数量的参数(... args)。rest 运算符用于将 fn 之后的参数收集到 ... args 中。
接下来,我们返回一个函数,该函数还将其余参数收集为... _args。该函数调用原始函数 fn 传入 ...args
和 ..._args
使用 spread 运算符作为参数,然后将该值返回给用户。
我们现在可以使用我们自己的 curry
函数来创建特定的函数。
让我们使用 curry
函数来创建一个更具体的函数(计算长度为100m的柱的体积):
function volume(l, h, w) {
return l * h * w;
}
const hCy = curry(volume, 100);
hCy(200, 900); // 180000l
hCy(70, 60); // 4200l
结论
闭包使 currying 在 JavaScript 中实现成为可能。它能够保留已经执行的函数的状态,使我们能够创建工厂函数 - 工厂函数可以为其参数添加特定值。
围绕 currying,closures 和函数式编程问题是非常棘手的。但我向你保证随着时间的推移和不断的练习🏪,你将渐渐掌握它,看看它是一件多么值得做的事😘