理解JavaScript的柯里化

2023-12-18 16:48
文章标签 java script 理解 柯里化

本文主要是介绍理解JavaScript的柯里化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

理解JavaScript的柯里化

函数式编程是一种编程风格,它可以将函数作为参数传递,并返回没有副作用(改变程序状态)的函数

许多计算机语言都采用了这种编程风格。在这些语言中,JavaScript、Haskell、Clojure、Erlang 和 Scala 是最流行的几种。

由于这种风格具有传递和返回函数的能力,它带来了许多概念:

  • 纯函数
  • 柯里化
  • 高阶函数

我们接下来要谈到的概念就是这其中的柯里化

在这篇文章?中,我们会看到柯里化如何工作以及它是如何被软件开发者运用到实践中的。

提示:除了复制粘贴,你可以使用 Bit 把可复用的 JavaScript 功能转换为组件,这样可以快速地和你的团队在项目之间共享。

什么是柯里化?

柯里化其实是函数式编程的一个过程,在这个过程中我们能把一个带有多个参数的函数转换成一系列的嵌套函数。它返回一个新函数,这个新函数期望传入下一个参数。

它不断地返回新函数(像我们之前讲的,这个新函数期望当前的参数),直到所有的参数都被使用。参数会一直保持 alive (通过闭包),当柯里化函数链中最后一个函数被返回和调用的时候,它们会用于执行。

柯里化是一个把具有较多 arity 的函数转换成具有较少 arity 函数的过程 -- Kristina Brainwave

注意:上面的术语 arity ,指的是函数的参数数量。举个例子,

function fn(a, b)//...
}
function _fn(a, b, c) {//...
}
复制代码

函数fn接受两个参数(2-arity函数),_fn接受3个参数(3-arity函数)

所以,柯里化把一个多参数函数转换为一系列只带单个参数的函数。

让我们来看一个简单的示例:

function multiply(a, b, c) {return a * b * c;
}
复制代码

这个函数接受3个数字,将数字相乘并返回结果。

multiply(1,2,3); // 6
复制代码

你看,我们如何调用这个具有完整参数的乘法函数。让我们创建一个柯里化后的版本,然后看看在一系列的调用中我们如何调用相同的函数(并且得到相同的结果):

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, 23三个数字想成的结果,这些参数一个接一个传递,每个数字都预先传递给下一个函数以便在内部调用。

我们可以拆分 multiply(1)(2)(3) 以便更好的理解它:

const mul1 = multiply(1);
const mul2 = mul1(2);
const result = mul2(3);
log(result); // 6
复制代码

让我们依次调用他们。我们传递了1multiply函数:

let mul1 = multiply(1);
复制代码

它返回这个函数:

return (b) => {return (c) => {return a * b * c}}
复制代码

现在,mul1持有上面这个函数定义,它接受一个参数b

我们调用mul1函数,传递2

let mul2 = mul1(2);
复制代码

num1会返回第三个参数:

return (c) => {return a * b * c
}
复制代码

返回的参数现在存储在变量mul2

mul2会变成:

mul2 = (c) => {return a * b * c
}
复制代码

当传递参数3给函数mul2并调用它,

const result = mul2(3);
复制代码

它和之前传递进来的参数:a = 1, b = 2做了计算,返回了6

log(result); // 6
复制代码

作为嵌套函数,mul2可以访问外部函数的变量作用域。

这就是mul2能够使用在已经退出的函数中定义的变量做加法运算的原因。尽管这些函数很早就返回了,并且从内存进行了垃圾回收,但是它们的变量仍然保持 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,但是会使用早已返回的其它作用域的变量来进行运算。由于闭包的原因,它们仍然可以工作。

柯里化背后的想法是,接受一个函数并且得到一个函数,这个函数返回专用的函数。

数学中的柯里化

我比较喜欢数学插图?Wikipedia,它进一步演示了柯里化的概念。让我们看看我们自己的示例。

假设我们有一个方程式:

f(x,y) = x^2 + y = z
复制代码

这里有两个变量 x 和 y 。如果这两个变量被赋值,x=3y=4,最后得到 z 的值。

:如果我们在方法f(z,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]复制代码

注意hxxh 的下标;hyyh 的下标。

如果我们在方程式 hx(y) = x^2 + y 中设置 x=3,它会返回一个新的方程式,这个方程式有一个变量y

h3(y) = 3^2 + y = 9 + y
Note: h3 is h subscript 3
复制代码

它和下面是一样的:

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
复制代码

在最后得到结果之前。

Wow!!这是一些数学问题,如果你觉得不够清晰?。可以在Wikipedia查看?完整的细节。

柯里化和部分函数应用

现在,有些人可能开始认为,被柯里化的函数所具有的嵌套函数数量取决于它所依赖的参数个数。是的,这是决定它成为柯里化的原因。

我设计了一个被柯里化的求体积的函数:

function volume(l) {return (w, h) => {return l * w * h}
}
复制代码

我们可以如下调用L:

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);
复制代码

我们定义了一个用于专门计算任何长度的圆柱体体积(l)的函数,70

它有3个参数和2个嵌套函数。不像我们之前的版本,有3个参数和3个嵌套函数。

这不是一个柯里化的版本。我们只是做了体积计算函数的部分应用。

柯里化和部分应用是相似的,但是它们是不同的概念。

部分应用将一个函数转换为另一个较小的函数。

function acidityRatio(x, y, z) {return performOp(x,y,z)
}
|
V
function acidityRatio(x) {return (y,z) => {return performOp(x,y,z)}
}
复制代码

注意:我故意忽略了performOp函数的实现。在这里,它不是必要的。你只需要知道柯里化和部分应用背后的概念。

这是 acidityRatio 函数的部分应用。这里面不涉及到柯里化。acidityRatio被部分应用化,它期望接受比原始函数更少的参数。

让它变成柯里化,会是这样:

function acidityRatio(x) {return (y) = > {return (z) = > {return performOp(x,y,z)}}
}
复制代码

柯里化根据函数的参数数量创建嵌套函数。每个函数接受一个参数。如果没有参数,那就不是柯里化。

柯里化在具有两个参数以上的函数工作 -  Wikipedia

柯里化将一个函数转换为一系列只接受单个参数的函数。、

这里有一个柯里化和部分应用相同的例子。假设我们有一个函数:

function div(x,y) {return x/y;
}
复制代码

如果我们部分应用化这个函数。会得到:

function div(x) {return (y) => {return x/y;}
}
复制代码

而且,柯里化会得出相同的结果:

function div(x) {return (y) => {return x/y;}
}
复制代码

尽管柯里化和部分应用得出了相同的结果,但是它们是两个完全不同的概念。

像我们之前说的,柯里化和部分应用是相似的,但是实际上定义却不同。它们之间的相同点就是依赖闭包。

柯里化有用吗?

当然,只要你想,柯里化就信手拈来:

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
复制代码

我们可以柯里化这个折扣函数,这样就不需要每天都添加0.10这个折扣值:

function discount(discount) {return (price) => {return price * discount;}
}
const tenPercentDiscount = discount(0.1);
复制代码

现在,我们可以只用你有价值的客户购买的商品价格来进行计算了:

tenPercentDiscount(500); // $50
// $500 - $50 = $450
复制代码

再一次,发生了这样的情况,有一些有价值的客户比另一些有价值的客户更重要 -- 我们叫他们超级价值客户。并且我们想给超级价值客户20%的折扣。

我们使用被柯里化的折扣函数:

const twentyPercentDiscount = discount(0.2);
复制代码

我们为超级价值客户设置了一个新函数,这个新函数调用了接受折扣值为0.2的柯里化函数。

返回的函数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;
}
复制代码

碰巧,你的仓库所有的圆柱体高度都是 100m。你会发现你会重复调用接受高度为 100 的参数的函数:

volume(200,30,100) // 2003000l
volume(32,45,100); //144000l
volume(2322,232,100) // 53870400l
复制代码

为了解决这个问题,需要柯里化这个计算体积的函数(像我们之前做的一样):

function volume(h) {return (w) => {return (l) => {return l * w * h}}
}
复制代码

我们可以定义一个特定的函数,这个函数用于计算特定的圆柱体高度:

const hCylinderHeight = volume(100);
hCylinderHeight(200)(30); // 600,000l
hCylinderHeight(2322)(232); // 53,870,400l
复制代码

通用的柯里化函数

让我们开发一个函数,它能接受任何函数并返回一个柯里化版本的函数。

为了做到这一点,我们需要这个(尽管你自己使用的方法和我的不同):

function curry(fn, ...args) {return (..._arg) => {return fn(...args, ..._arg);}
}
复制代码

我们在这里做了什么呢?我们的柯里化函数接受一个我们希望柯里化的函数(fn),还有一系列的参数(...args)。扩展运算符是用来收集fn后面的参数到...args中。

接下来,我们返回一个函数,这个函数同样将剩余的参数收集为..._args。这个函数将...args传入原始函数fn并调用它,通过使用扩展运算符将..._args也作为参数传入,然后,得到的值会返回给用户。

现在我们可以使用我们自己的curry函数来创造专用的函数了。

让我们使用自己的柯里化函数来创建更多的专用函数(其中一个就是专门用来计算高度为100m的圆柱体体积的方法)

function volume(l,h,w) {return l * h * w
}
const hCy = curry(volume,100);
hCy(200,900); // 18000000l
hCy(70,60); // 420000l
复制代码

总结

闭包使柯里化在JavaScript中得以实现。它保持着已经执行过的函数的状态,使我们能够创建工厂函数 - 一种我们能够添加特定参数的函数。

要想将你的头脑充满着柯里化、闭包和函数式编程是非常困难的。但我向你保证,花时间并且在日常应用,你会掌握它的诀窍并看到价值?。

参考

?Currying—Wikipedia

?Partial Application Function—Wikipedia


作者:阿里云前端
链接:https://juejin.im/post/5bf18715e51d45244939acc5
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这篇关于理解JavaScript的柯里化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/509120

相关文章

使用Java发送邮件到QQ邮箱的完整指南

《使用Java发送邮件到QQ邮箱的完整指南》在现代软件开发中,邮件发送功能是一个常见的需求,无论是用户注册验证、密码重置,还是系统通知,邮件都是一种重要的通信方式,本文将详细介绍如何使用Java编写程... 目录引言1. 准备工作1.1 获取QQ邮箱的SMTP授权码1.2 添加JavaMail依赖2. 实现

Java嵌套for循环优化方案分享

《Java嵌套for循环优化方案分享》介绍了Java中嵌套for循环的优化方法,包括减少循环次数、合并循环、使用更高效的数据结构、并行处理、预处理和缓存、算法优化、尽量减少对象创建以及本地变量优化,通... 目录Java 嵌套 for 循环优化方案1. 减少循环次数2. 合并循环3. 使用更高效的数据结构4

java两个List的交集,并集方式

《java两个List的交集,并集方式》文章主要介绍了Java中两个List的交集和并集的处理方法,推荐使用Apache的CollectionUtils工具类,因为它简单且不会改变原有集合,同时,文章... 目录Java两个List的交集,并集方法一方法二方法三总结java两个List的交集,并集方法一

Spring AI集成DeepSeek三步搞定Java智能应用的详细过程

《SpringAI集成DeepSeek三步搞定Java智能应用的详细过程》本文介绍了如何使用SpringAI集成DeepSeek,一个国内顶尖的多模态大模型,SpringAI提供了一套统一的接口,简... 目录DeepSeek 介绍Spring AI 是什么?Spring AI 的主要功能包括1、环境准备2

Spring AI集成DeepSeek实现流式输出的操作方法

《SpringAI集成DeepSeek实现流式输出的操作方法》本文介绍了如何在SpringBoot中使用Sse(Server-SentEvents)技术实现流式输出,后端使用SpringMVC中的S... 目录一、后端代码二、前端代码三、运行项目小天有话说题外话参考资料前面一篇文章我们实现了《Spring

Spring AI与DeepSeek实战一之快速打造智能对话应用

《SpringAI与DeepSeek实战一之快速打造智能对话应用》本文详细介绍了如何通过SpringAI框架集成DeepSeek大模型,实现普通对话和流式对话功能,步骤包括申请API-KEY、项目搭... 目录一、概述二、申请DeepSeek的API-KEY三、项目搭建3.1. 开发环境要求3.2. mav

Springboot的自动配置是什么及注意事项

《Springboot的自动配置是什么及注意事项》SpringBoot的自动配置(Auto-configuration)是指框架根据项目的依赖和应用程序的环境自动配置Spring应用上下文中的Bean... 目录核心概念:自动配置的关键特点:自动配置工作原理:示例:需要注意的点1.默认配置可能不适合所有场景

使用Apache POI在Java中实现Excel单元格的合并

《使用ApachePOI在Java中实现Excel单元格的合并》在日常工作中,Excel是一个不可或缺的工具,尤其是在处理大量数据时,本文将介绍如何使用ApachePOI库在Java中实现Excel... 目录工具类介绍工具类代码调用示例依赖配置总结在日常工作中,Excel 是一个不可或缺的工http://

Java8需要知道的4个函数式接口简单教程

《Java8需要知道的4个函数式接口简单教程》:本文主要介绍Java8中引入的函数式接口,包括Consumer、Supplier、Predicate和Function,以及它们的用法和特点,文中... 目录什么是函数是接口?Consumer接口定义核心特点注意事项常见用法1.基本用法2.结合andThen链

spring @EventListener 事件与监听的示例详解

《spring@EventListener事件与监听的示例详解》本文介绍了自定义Spring事件和监听器的方法,包括如何发布事件、监听事件以及如何处理异步事件,通过示例代码和日志,展示了事件的顺序... 目录1、自定义Application Event2、自定义监听3、测试4、源代码5、其他5.1 顺序执行