用强大的咖喱函数为你的 JavaScript 增添趣味!(函数式编程与咖喱函数)
函数式编程和柯里化这两个话题,有些人会盯着墙,一边说着“没有勺子”,一边悲伤地摇头。然而,我们知道那里有一个强大的工具,所以我们继续努力,试图掌握这门黑暗艺术。
我最初是一名 C/C++ 程序员,多年来用各种编程语言赚过钱,但函数式编程却让我走上了一条截然不同的道路。我在这个领域已经走了不少路,所以想分享一下我的理解以及我一路走来编写的一个实用程序。
基础知识
让我们从基础开始。
如果你有一个函数:
const calculate = (a, b, c) => (a * b) / c
您可以将其重写为:
const calculate = a => b => c => (a * b) / c
你可以这样调用第一个:
console.log(calculate(100, 20, 3))
你可以这样调用第二个:
console.log(calculate(100)(20)(3))
第二种实现是一个函数,它创建一个函数,该函数创建一个函数来计算答案(这是从《黑客帝国》转移到《盗梦空间》吧?)
我们使用 JavaScript 箭头函数转换了原始函数,基本上将a,
其替换为a =>
。第一个函数返回一个参数a
,并返回一个包含该参数的函数b
。由于闭包的存在,最终函数可以访问所有先前的参数,从而完成其工作。
这样做的好处是代码重用。直到最后一个函数,我们基本上都在运行一个工厂来创建已经提供参数的函数。
const calculateTheAnswer = calculate(100)(20)
for(let i = 1; i < 1000; i++) {
console.log(calculateTheAnswer(i))
}
在这种情况下,你可能会说:“哦,不错,看起来不错,但就是看不出来有什么意义”。当你开始通过将函数作为参数传递,并用多个函数“组合”出解决方案来处理更复杂的事情时,它的优势就显现出来了。让我们来看看。
柯里化
为了本文的目的,我想举一个简单的例子,但不仅仅是“将两个数字相乘”。所以我想出了一个包含乘法和减法的例子 ;) 说实话,我希望它能提供一个实用的视角。
好的,想象一下我们正在为一家制造公司建立一个网站,我们的任务是显示该公司各种尺寸和材料制成的“UberStorage”容器的重量。
一些聪明的家伙为我们提供了一个库函数来计算单位的重量。
function weightOfHollowBox(
edgeThickness,
heightInM,
widthInM,
depthInM,
densityInCm3
) {
return (
heightInM * widthInM * depthInM * (densityInCm3 * 1000) -
(heightInM - edgeThickness * 2) *
(widthInM - edgeThickness * 2) *
(depthInM - edgeThickness * 2) *
(densityInCm3 * 1000)
)
}
(参见乘法和减法)。我们不想弄乱它,因为它不是我们的代码,而且可能会改变,但我们可以依赖传递参数的“契约”。
我们的网站需要显示许多不同的输出,如下所示:
因此,我们必须迭代尺寸和材料并产生一些输出。
我们希望编写尽可能少的代码,所以我们想到了函数式编程和咖喱!
首先,我们可以为该函数创建一个包装器:
const getHollowBoxWeight = (edgeThickness) => (heightInM) => (widthInM) => (
depthInM
) => (densityInCm3) =>
weightOfHollowBox(
edgeThickness,
heightInM,
widthInM,
depthInM,
densityInCm3
)
但我们很快就发现一些问题,我们必须以正确的顺序调用这些函数。考虑到我们的问题,我们需要认真思考能否找到一个完美的顺序,从而最大限度地提高复用率。我们应该把密度放在第一位吗?这是材质的一个属性。边缘厚度是我们大多数产品的标准,所以我们可以把它放在第一位。等等等等。最后一个参数呢?我们可能希望迭代它,但我们现在同时迭代材质和尺寸。嗯……
你可能觉得写几个版本的包装函数没问题,也可能觉得直接放弃说“我就直接叫 weightOfHollowBox 就行了”,但还有另一种选择。使用curry生成器将 转换weightOfHollowBox
为 curry 函数。
简单的咖喱,没有太多的配料
好的,一个简单的柯里化函数会接受weightOfHollowBox
一个参数,并返回一个可以用多个参数调用的函数。如果我们完成了所有参数,则计算权重;否则,返回一个需要剩余参数的函数。这样的包装器看起来有点像这样:
const currySimple = (fn, ...provided) => {
// fn.length is the number of parameters before
// the first one with a default value
const length = fn.length
// Return a function that takes parameters
return (...params) => {
// Combine any parameters we had before with the
// new ones
const all = [...provided, ...params]
// If we have enough parameters, call the fn
// otherwise return a new function that knows
// about the already passed params
if (all.length >= length) {
return fn(...all)
} else {
return currySimple(fn, ...all)
}
}
}
如果我们在 weightOfHollowBox 上调用这个函数,我们最终会得到一个比手写函数更灵活的函数:
const getWeightOfBox = currySimple(weightOfHollowBox)
// All of these combinations work
console.log(getWeightOfBox(0.1)(10)(10)(3)(.124))
console.log(getWeightOfBox(0.1, 10, 10)(3)(.124))
我们可以传递所有参数或任何子集,并且在这些情况下都能正常工作。但这并不能解决参数排序问题。我们非常希望有一个版本可以忽略中间参数,并专门为这些参数提供一个函数。
例如
const getWeightOfBox = curry(weightOfHollowBox)
const varyByWidth = getWeightOfBox(0.1, 10, MISSING, 3, .124)
console.log(varyByWidth(4))
贾尔弗雷齐
警告:创建这个新函数需要一些更高级的代码
curry
——如果您不想理解,可以不理解。您可以使用这个实现或其他许多实现,而无需了解其内部工作原理。如果您想了解如何实现,请继续阅读,否则请跳至下一部分。
好吧,让我们来点正经的咖喱。首先,我们需要一个唯一标识缺失参数的东西。
const MISSING = Symbol("Missing")
有了工具箱中的这个,我们就可以继续编写新的 curry 函数了。
const curry = (
fn,
missingParameters = Array.from({ length: fn.length }, (_, i) => i),
parameters = []
) => {
return (...params) => {
// Keeps a track of the values we haven't supplied yet
const missing = [...missingParameters]
// Keeps a track of the values we have supplied
const values = [...parameters]
// Loop through the new parameters
let scan = 0
for (let parameter of params) {
// If it is missing move on
if (parameter === MISSING) {
scan++
continue
}
// Update the value and the missing list
values[missing[scan] ?? values.length] = parameter
missing.splice(scan, 1)
}
// Call the function when we have enough params
if (missing.length <= 0) {
return fn(...values)
} else {
// Curry again? Yes please
return curry(fn, missing, values)
}
}
}
好的,让我们从这些参数开始。fn
是要柯里化的函数,接下来的两个参数用于递归,以防我们需要创建另一个中间函数而不是调用fn
。missingParameters
默认为数字 0..n,其中n
是 -1 所需的参数数量fn
。换句话说,当我们第一次调用它时,它是 所需所有参数的索引fn
。下一个参数是一个空数组,如果需要,我们将填充它并传递下去。
我们返回的函数接受任意数量的参数。我们复制缺失的索引和现有参数,然后迭代新的参数。如果参数值为真,MISSING
则继续查找下一个缺失的索引。如果不是,则MISSING
在值数组中填充正确的索引(我们允许该数组接受比函数更多的参数,因为这是处理任何可能被默认的参数的方式)。填充数组后,我们删除缺失的索引。
一旦完成所有操作,如果缺失列表为空,则我们调用该函数,并将值传递给它,否则我们进行递归。
注意:我们从不设置数组的长度,如果您在数组中写入索引,Javascript 数组会自动将其长度设置为最大值。
就是这样,此功能允许我们创建一系列模板。
示例网站
现在我们有了一种包装方法,weightOfHollowBox
我们可以开始组合我们的网页元素了。
首先,让我们编写一个代码来显示物品的重量及其材质。我们可以看到,内部元素是基于材质的迭代而生成的。材质的定义如下:
const materials = [
{ name: "Aluminium", density: 2.71 },
{ name: "Steel", density: 7.7 },
{ name: "Oak", density: 0.73 }
]
因此,我们编写了一个柯里化函数来渲染项目,该项目采用一种计算重量的方法(我们将从柯里化中创建的函数weightOfHollowBox
)和一种材料:
const material = (weightInKg) => (material) => (
<ListItem key={material.name}>
<ListItemText
primary={material.name}
secondary={
<span>
{(weightInKg(material.density) / 1000).toFixed(1)} tons
</span>
}
/>
</ListItem>
)
只要我们能给它一个计算需要密度的重量的函数,它就能显示任何材料。
让我向你展示一种现在可以使用的简单方法:
function Simple() {
const weightInKg = curriedWeight(0.05, 10, 3, 3)
return (
<List className="App">
{materials.map(material(weightInKg))}
</List>
)
}
我们创建一个重量计算器来寻找density
,然后我们调用我们的材料函数,传递它,它返回一个需要的函数material
,这将由传递materials.map()
。
不过,我们打算为该网站做一些更有趣的事情。
一个方块容纳所有材料
我们想要输出材料列表,所以让我们为此编写一个函数。
const materialBlock = (header) => (weightCalculator) => (
materials
) => (dimension) => (
<Fragment key={dimension}>
{header(dimension)}
{materials.map(material(weightCalculator(dimension)))}
</Fragment>
)
这个柯里化函数允许我们提供一些可以写入标题的内容,然后给出一个重量计算器、一个材料列表和一个尺寸,它将输出该组的所有材料。
这有点棘手,让我们看看如何以孤立的方式使用它:
const ShowByHeight = () => {
const heights = [2, 3, 5, 10]
const weightCalculator = curriedWeight(0.05, MISSING, 5, 3)
const outputter = materialBlock((height) => (
<ListSubheader>5 m wide x {height} m tall</ListSubheader>
))(weightCalculator)(materials)
return <List className="App">{heights.map(outputter)}</List>
}
这里我们有一个 React 组件,它知道我们单位的标准高度。它创建了一个仍然需要的重量计算器height
,density
然后提供了materialBlock
一个可以放在其上的标题。
对于网站来说,我们可以获得更好的代码重用!
const ShowBy = (weightCalculator) => (header) => (values) => (
<List className="App">
{values.map(
materialBlock(header)(weightCalculator)(materials)
)}
</List>
)
我们创建一个可重复使用的 ShowBy 函数,然后可以使用它来创建标准宽度和高度的版本。
const widths = [1, 4, 7, 10]
const heights = [2, 3, 5, 10]
const ByWidth = () =>
ShowBy(curriedWeight(0.05, 10, MISSING, 3))((width) => (
<ListSubheader>10 m tall x {width} m wide</ListSubheader>
))(widths)
const ByHeight = () =>
ShowBy(curriedWeight(0.05, MISSING, 5, 3))((height) => (
<ListSubheader>5 m wide x {height} m tall</ListSubheader>
))(heights)
整合
我们的最终函数用于将各个部分组合在一起:
const Advanced = () => (
<Box>
<Box mb={2}>
<Card>
<CardHeader title="By Width" />
<CardContent>
<ByWidth />
</CardContent>
</Card>
</Box>
<Box mb={2}>
<Card>
<CardHeader title="By Height" />
<CardContent>
<ByHeight />
</CardContent>
</Card>
</Box>
</Box>
)
以下是整个事情的经过:
结论
希望本文能让您深入了解 JavaScript 中的柯里化。函数式编程领域非常深奥,我们只是略知皮毛,但其中蕴含着一些在很多场景下都能实用的技巧。
感谢阅读!
(所有代码均获得 MIT 许可)
文章来源:https://dev.to/miketalbot/javascript-curry-anyone-3l34