理解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

相关文章

Spring @Scheduled注解及工作原理

《Spring@Scheduled注解及工作原理》Spring的@Scheduled注解用于标记定时任务,无需额外库,需配置@EnableScheduling,设置fixedRate、fixedDe... 目录1.@Scheduled注解定义2.配置 @Scheduled2.1 开启定时任务支持2.2 创建

SpringBoot中使用Flux实现流式返回的方法小结

《SpringBoot中使用Flux实现流式返回的方法小结》文章介绍流式返回(StreamingResponse)在SpringBoot中通过Flux实现,优势包括提升用户体验、降低内存消耗、支持长连... 目录背景流式返回的核心概念与优势1. 提升用户体验2. 降低内存消耗3. 支持长连接与实时通信在Sp

Spring Boot 实现 IP 限流的原理、实践与利弊解析

《SpringBoot实现IP限流的原理、实践与利弊解析》在SpringBoot中实现IP限流是一种简单而有效的方式来保障系统的稳定性和可用性,本文给大家介绍SpringBoot实现IP限... 目录一、引言二、IP 限流原理2.1 令牌桶算法2.2 漏桶算法三、使用场景3.1 防止恶意攻击3.2 控制资源

Mac系统下卸载JAVA和JDK的步骤

《Mac系统下卸载JAVA和JDK的步骤》JDK是Java语言的软件开发工具包,它提供了开发和运行Java应用程序所需的工具、库和资源,:本文主要介绍Mac系统下卸载JAVA和JDK的相关资料,需... 目录1. 卸载系统自带的 Java 版本检查当前 Java 版本通过命令卸载系统 Java2. 卸载自定

springboot下载接口限速功能实现

《springboot下载接口限速功能实现》通过Redis统计并发数动态调整每个用户带宽,核心逻辑为每秒读取并发送限定数据量,防止单用户占用过多资源,确保整体下载均衡且高效,本文给大家介绍spring... 目录 一、整体目标 二、涉及的主要类/方法✅ 三、核心流程图解(简化) 四、关键代码详解1️⃣ 设置

Java Spring ApplicationEvent 代码示例解析

《JavaSpringApplicationEvent代码示例解析》本文解析了Spring事件机制,涵盖核心概念(发布-订阅/观察者模式)、代码实现(事件定义、发布、监听)及高级应用(异步处理、... 目录一、Spring 事件机制核心概念1. 事件驱动架构模型2. 核心组件二、代码示例解析1. 事件定义

SpringMVC高效获取JavaBean对象指南

《SpringMVC高效获取JavaBean对象指南》SpringMVC通过数据绑定自动将请求参数映射到JavaBean,支持表单、URL及JSON数据,需用@ModelAttribute、@Requ... 目录Spring MVC 获取 JavaBean 对象指南核心机制:数据绑定实现步骤1. 定义 Ja

javax.net.ssl.SSLHandshakeException:异常原因及解决方案

《javax.net.ssl.SSLHandshakeException:异常原因及解决方案》javax.net.ssl.SSLHandshakeException是一个SSL握手异常,通常在建立SS... 目录报错原因在程序中绕过服务器的安全验证注意点最后多说一句报错原因一般出现这种问题是因为目标服务器

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

springboot项目中整合高德地图的实践

《springboot项目中整合高德地图的实践》:本文主要介绍springboot项目中整合高德地图的实践,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一:高德开放平台的使用二:创建数据库(我是用的是mysql)三:Springboot所需的依赖(根据你的需求再