用强大的咖喱函数为你的 JavaScript 增添趣味!(函数式编程与咖喱函数)

2025-05-25

用强大的咖喱函数为你的 JavaScript 增添趣味!(函数式编程与咖喱函数)

函数式编程和柯里化这两个话题,有些人会盯着墙,一边说着“没有勺子”,一边悲伤地摇头。然而,我们知道那里有一个强大的工具,所以我们继续努力,试图掌握这门黑暗艺术。

我最初是一名 C/C++ 程序员,多年来用各种编程语言赚过钱,但函数式编程却让我走上了一条截然不同的道路。我在这个领域已经走了不少路,所以想分享一下我的理解以及我一路走来编写的一个实用程序。

基础知识

让我们从基础开始。

如果你有一个函数:

const calculate = (a, b, c) => (a * b) / c 
Enter fullscreen mode Exit fullscreen mode

您可以将其重写为:

const calculate = a => b => c => (a * b) / c
Enter fullscreen mode Exit fullscreen mode

你可以这样调用第一个:

   console.log(calculate(100, 20, 3))
Enter fullscreen mode Exit fullscreen mode

你可以这样调用第二个:

   console.log(calculate(100)(20)(3))
Enter fullscreen mode Exit fullscreen mode

第二种实现是一个函数,它创建一个函数,该函数创建一个函数来计算答案(这是从《黑客帝国》转移到《盗梦空间》吧?)

我们使用 JavaScript 箭头函数转换了原始函数,基本上将a,其替换为a =>。第一个函数返回一个参数a,并返回一个包含该参数的函数b。由于闭包的存在,最终函数可以访问所有先前的参数,从而完成其工作。

这样做的好处是代码重用。直到最后一个函数,我们基本上都在运行一个工厂来创建已经提供参数的函数。

  const calculateTheAnswer = calculate(100)(20)
  for(let i = 1; i < 1000; i++) {
     console.log(calculateTheAnswer(i))
  }
Enter fullscreen mode Exit fullscreen mode

在这种情况下,你可能会说:“哦,不错,看起来不错,但就是看不出来有什么意义”。当你开始通过将函数作为参数传递,并用多个函数“组合”出解决方案来处理更复杂的事情时,它的优势就显现出来了。让我们来看看。

柯里化

为了本文的目的,我想举一个简单的例子,但不仅仅是“将两个数字相乘”。所以我想出了一个包含乘法和减法的例子 ;) 说实话,我希望它能提供一个实用的视角。

好的,想象一下我们正在为一家制造公司建立一个网站,我们的任务是显示该公司各种尺寸和材料制成的“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)
    )
}
Enter fullscreen mode Exit fullscreen mode

(参见乘法和减法)。我们不想弄乱它,因为它不是我们的代码,而且可能会改变,但我们可以依赖传递参数的“契约”。

我们的网站需要显示许多不同的输出,如下所示:

因此,我们必须迭代尺寸和材料并产生一些输出。

我们希望编写尽可能少的代码,所以我们想到了函数式编程和咖喱!

首先,我们可以为该函数创建一个包装器:

const getHollowBoxWeight = (edgeThickness) => (heightInM) => (widthInM) => (
    depthInM
) => (densityInCm3) =>
    weightOfHollowBox(
        edgeThickness,
        heightInM,
        widthInM,
        depthInM,
        densityInCm3
    )
Enter fullscreen mode Exit fullscreen mode

但我们很快就发现一些问题,我们必须以正确的顺序调用这些函数。考虑到我们的问题,我们需要认真思考能否找到一个完美的顺序,从而最大限度地提高复用率。我们应该把密度放在第一位吗?这是材质的一个属性。边缘厚度是我们大多数产品的标准,所以我们可以把它放在第一位。等等等等。最后一个参数呢?我们可能希望迭代它,但我们现在同时迭代材质和尺寸。嗯……

你可能觉得写几个版本的包装函数没问题,也可能觉得直接放弃说“我就直接叫 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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

如果我们在 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))
Enter fullscreen mode Exit fullscreen mode

我们可以传递所有参数或任何子集,并且在这些情况下都能正常工作。但这并不能解决参数排序问题。我们非常希望有一个版本可以忽略中间参数,并专门为这些参数提供一个函数。

例如

   const getWeightOfBox = curry(weightOfHollowBox)
   const varyByWidth = getWeightOfBox(0.1, 10, MISSING, 3, .124)
   console.log(varyByWidth(4))
Enter fullscreen mode Exit fullscreen mode

贾尔弗雷齐

警告:创建这个新函数需要一些更高级的代码curry——如果您不想理解,可以不理解。您可以使用这个实现或其他许多实现,而无需了解其内部工作原理。如果您想了解如何实现,请继续阅读,否则请跳至下一部分。

好吧,让我们来点正经的咖喱。首先,我们需要一个唯一标识缺失参数的东西。

const MISSING = Symbol("Missing")
Enter fullscreen mode Exit fullscreen mode

有了工具箱中的这个,我们就可以继续编写新的 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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

好的,让我们从这些参数开始。fn是要柯里化的函数,接下来的两个参数用于递归,以防我们需要创建另一个中间函数而不是调用fnmissingParameters默认为数字 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 }
]
Enter fullscreen mode Exit fullscreen mode

因此,我们编写了一个柯里化函数来渲染项目,该项目采用一种计算重量的方法(我们将从柯里化中创建的函数weightOfHollowBox)和一种材料:

const material = (weightInKg) => (material) => (
    <ListItem key={material.name}>
        <ListItemText
            primary={material.name}
            secondary={
                <span>
                    {(weightInKg(material.density) / 1000).toFixed(1)} tons
                </span>
            }
        />
    </ListItem>
)
Enter fullscreen mode Exit fullscreen mode

只要我们能给它一个计算需要密度的重量的函数,它就能显示任何材料。

让我向你展示一种现在可以使用的简单方法:

function Simple() {
    const weightInKg = curriedWeight(0.05, 10, 3, 3)
    return (
        <List className="App">
            {materials.map(material(weightInKg))}
        </List>
    )
}
Enter fullscreen mode Exit fullscreen mode

我们创建一个重量计算器来寻找density,然后我们调用我们的材料函数,传递它,它返回一个需要的函数material,这将由传递materials.map()

不过,我们打算为该网站做一些更有趣的事情。

一个方块容纳所有材料

我们想要输出材料列表,所以让我们为此编写一个函数。


const materialBlock = (header) => (weightCalculator) => (
    materials
) => (dimension) => (
    <Fragment key={dimension}>
        {header(dimension)}
        {materials.map(material(weightCalculator(dimension)))}
    </Fragment>
)
Enter fullscreen mode Exit fullscreen mode

这个柯里化函数允许我们提供一些可以写入标题的内容,然后给出一个重量计算器、一个材料列表和一个尺寸,它将输出该组的所有材料。

这有点棘手,让我们看看如何以孤立的方式使用它:

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>
}
Enter fullscreen mode Exit fullscreen mode

这里我们有一个 React 组件,它知道我们单位的标准高度。它创建了一个仍然需要的重量计算器heightdensity然后提供了materialBlock一个可以放在其上的标题。

对于网站来说,我们可以获得更好的代码重用!

const ShowBy = (weightCalculator) => (header) => (values) => (
    <List className="App">
        {values.map(
            materialBlock(header)(weightCalculator)(materials)
        )}
    </List>
)
Enter fullscreen mode Exit fullscreen mode

我们创建一个可重复使用的 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)
Enter fullscreen mode Exit fullscreen mode

整合

我们的最终函数用于将各个部分组合在一起:


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>
)
Enter fullscreen mode Exit fullscreen mode

以下是整个事情的经过:

结论

希望本文能让您深入了解 JavaScript 中的柯里化。函数式编程领域非常深奥,我们只是略知皮毛,但其中蕴含着一些在很多场景下都能实用的技巧。

感谢阅读!

(所有代码均获得 MIT 许可)

文章来源:https://dev.to/miketalbot/javascript-curry-anyone-3l34
PREV
React Virtual Window - 虚拟化一切以提高性能!
NEXT
60fps JS 同时对数百万条记录进行排序、映射和减少(使用空闲时间协程) js-coroutines 更新协程 在此处获取库:它是如何工作的?