柯里化到底是什么?
字面上地,
如何编写柯里化函数?
与辅助函数一起使用
从现有函数创建新函数
制作我们自己的辅助函数
万岁🎉
进一步阅读
大家好,很高兴在我的网络小窝里见到你们!我正在复习一些我刚开始学习 JavaScript 时忽略的概念,现在就来聊聊吧。现在,我们来看看 JavaScript 中一个很花哨的术语——柯里化(Currying)到底是怎么回事。
字面上地,
尽管“柯里化”这个术语听起来很花哨,但(根据维基百科)它是一种将一个接受多个参数的函数转换为一系列每个接受单个参数的函数的技术。现在,无论你是 JavaScript 爱好者,还是在高中学过基础代数,你都能理解它的含义。
但如果不是,它只是说我们有一个接受多个参数的函数,并且我们将它转换为另一个接受相同数量参数的函数,但每次接受一个参数或按顺序接受。下图清楚地说明了这一点。
无论如何,这在现实世界中是如何发生的(或以编程方式),让我们来找出答案!
如何编写柯里化函数?
我们知道如何编写一个普通函数(不带柯里化),例如add()
上面的函数,就这么简单。
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
但这就是我们以可柯里化的方式编写相同函数的方式。
function add(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
console.log(add(1)(2)(3)); // 6
如果你注意到柯里函数,我们会发现,对于每个传入的参数,add()
我们都会返回一个新函数,这个新函数接受另一个参数并返回另一个函数。最后,在传递最后一个参数后,我们返回最终结果。这就是柯里函数的基本结构。
因此add(1)
,在 curry 函数中,我们不会得到结果值,而是一个以(2)
参数为参数的全新函数,它会一直运行直到我们在输出中获得一个值。
与辅助函数一起使用
由于柯里化函数并不容易编写(总是),但作为函数式编程中的一个主要概念,我们有许多辅助函数可以帮助我们将普通函数转换为柯里化函数。
大多数 JavaScript 实用程序库(例如Lodash、Rambda等)都可以完成这些操作,只需一个简单的步骤即可完成。此外,我在此示例中使用了 lodash。
const _ = require("lodash");
function add(a, b, c) {
return a + b + c;
}
const curryAdd = _.curry(add);
console.log(add(1, 2, 3)); // 6
console.log(curryAdd(1)(2)(3)); // 6
从现有函数创建新函数
这就是 curry 函数在现实生活中的主要用途,因为它可以帮助我们创建全新的函数并将其导出以在任何地方使用。
例如,我们有这个对象数组。
const items = [
{ name: "Mango", type: "Fruit" },
{ name: "Tomato", type: "Vegetable" },
{ name: "Strawberry", type: "Fruit" },
{ name: "Potato", type: "Vegetable" },
{ name: "Turnip", type: "Vegetable" },
{ name: "Banana", type: "Fruit" },
{ name: "Carrot", type: "Vegetable" },
];
现在我们需要创建一个函数,返回所有类型为Fruits
或 的项Vegetables
。那么,让我们使用刚刚学到的柯里化概念来实现它。
const isType = obj => type => obj.type === type;
const isFruit = item => isType(item)("Fruit");
const isVegetable = item => isType(item)("Vegetable");
const fruits = items.filter(isFruit);
const vegetables = items.filter(isVegetable);
哇,看起来真干净。但这是怎么工作的呢?
首先,我们将其isType()
作为一个 curry 函数,它接受一个对象并在检查其类型是否等于传递的参数类型后返回一个布尔值(true/false)。
但是,我们并不直接使用这个函数,而是创建了另外两个函数,分别检查类型是水果还是蔬菜。通过查看它的结构,你会注意到它只接受一个参数,即当前项目,并isType()
通过传递item
所需的类型来调用它。
最后,为了获取水果和蔬菜,我们运行一个.filter()
数组方法,并将isFruit
或isVegetable
作为回调函数传递。默认情况下,这会将items
数组中的当前项传递给回调函数。更清晰地说,我们最终所做的与下面的相同。
const fruits = items.filter(item => isFruit(item));
const vegetables = items.filter(item => isVegetable(item));
结果
console.log(fruits)
[
{ name: 'Mango', type: 'Fruit' },
{ name: 'Strawberry', type: 'Fruit' },
{ name: 'Banana', type: 'Fruit' }
]
console.log(vegetables)
[
{ name: 'Tomato', type: 'Vegetable' },
{ name: 'Potato', type: 'Vegetable' },
{ name: 'Turnip', type: 'Vegetable' },
{ name: 'Carrot', type: 'Vegetable' }
]
最终,我们知道了如何编写 curry 函数,这比我们想象的要容易得多。
编辑
你能想出一种稍微不同的方法来编写上面例子中的函数,以减少代码量并使其更直观吗?这可能吗?
是的,感谢Rudy Nappée在评论中向我们展示同样的内容,我认为这对阅读这篇文章的每个人都很有用。
所以,我们应该做的是:“始终将点(部分应用最终操作的参数)放在最后的位置”。
const isType = (type) => (obj) => obj.type === type
这样,我们就不必像对isFruit
和那样重新定义另一个函数,isVegetable
而是可以这样写。
const isFruit = isType("Fruit")
const isVegetable = isType("Vegetable")
或者您可以用更直接的方式编写它,直接在过滤器回调中编写。
const fruits = items.filter(isType("Fruit"))
const vegetables = items.filter(isType("Vegetable"))
制作我们自己的辅助函数
在学会了使用辅助函数(例如Lodash.curry()
中的)将普通函数转换为柯里化函数之后,我很好奇如何自己构建一个。因为,你知道,有时候我们只需要几个函数,不需要像Lodash这样的大型实用程序库。
让我们首先创建一个简单的用例,然后再创建实际的 curry 函数。
// Assume that curry is a valid function
const multiply = (a, b) => a * b;
const curryMultiply = curry(multiply);
console.log(curryMultiply(2, 4)); // 8
console.log(curryMultiply(2)(4)); // 8
现在先不要运行,因为我们还需要满足假设,创建一个有效的 curry 函数。运行后肯定会报错,Reference Error
因为curry
“is notdefined”。
构建实际curry()
功能。
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(undefined, args);
} else {
return function (...rest) {
return curried.apply(undefined, rest.concat(args));
};
}
};
}
首先,我们创建一个名为 curry 的函数,并从中返回另一个名为 curried 的函数。如你所见,在返回的函数中,我们检查传递给它的参数数量。如果该数量超过或等于函数实际func
期望的参数数量(也称为arity),则我们通过调用 apply 并传入所有参数来返回该函数。
如果参数数量较少(这种情况在我们逐个传递参数时发生),我们会返回另一个函数,该函数将其参数存储在名为 的变量中rest
。最后,我们递归调用同一个函数curried()
,并将新的参数传递给rest
变量,并将其与变量中先前获得的参数连接起来args
。
另外,如果你好奇那三个点( )是什么,它们是ES6或ECMAScript2015...
中的新特性。它们返回传递给函数的所有参数。
现在,如果您按下回车键或运行代码,您将看到获得正确的输出,如下所示。
console.log(curryMultiply(2, 4)); // 8
console.log(curryMultiply(2)(4)); // 8
您可以将此代码片段保存在您的 Gist 中或正确理解它,因此,任何时候您只想使用带有柯里化的函数而不是再次编写该函数,您可以通过将旧的非柯里化函数传递到curry()
我们刚刚创建的这个方法中来创建一个新函数。
万岁🎉
如果你已经跟上我的思路,那就太棒了!希望你能学到一些新东西,或者在修复老 bug 方面有所进步。如果你发现我遗漏了什么,或者只是想打个招呼(这在当今社会真的变得很重要),可以发推文到@heytulsiprasad。接下来几天,你可以期待更多关于函数式编程的博客。