原文来自 Currying in JavaScript(https://blog.bitsrc.io/understanding-currying-in-javascript-ceb2188c339)
函数式编程是一种将函数作为参数来传递和返回的,并且没有副作用(只是返回新的值,不改变系统变量)的编程方式。所以很多语言采纳了这种编程方式,这里面JavaScript、Haskell、Clojure·、Erlang和Scala是最受欢迎的。
并且由于它能够传递和返回函数,它也带来了许多概念:
- 纯函数(Pure Functions)
- 柯里化(Currying)
- 高阶函数(Higher-order functions)
在这里我们要来探索的一个概念是柯里化(Currying)。
在本文中,我们将来了解柯里化是如何工作的,并且在开发过程中有何用途。
|什么是柯里化
柯里化是把接受多个参数的函数转换成接受单一参数的函数的操作。它返回接受余下的参数而且返回结果的新函数。
它持续地返回一个新函数直到所有的参数用尽为止。这些参数全部保持“活着”的状态(通过闭包),然后当柯里化链中的最后一个函数被返回和执行时会全部被用来执行。
Currying is the process of turning a function with multiple arity into a function with less arity(柯里化是将一个多元函数转换为低元函数的操作)—Kristina Brainwave
这里的‘元’,指的是函数所需要的参数数量,举个例子:
![u=1888025457,2161988340&fm=173&app=25&f=JPEG?w=640&h=559&s=F5A63D7649426E4D5AD5847A02005073](https://i-blog.csdnimg.cn/blog_migrate/df8d0d20c59f291416c75ffc9d05fe7f.jpeg)
函数fn接受2个参数(2元函数),_fn接受3个参数(3元函数)。所以,柯里化将一个多参数的函数转换成一系列只接受单个参数的函数。
让我们来看个简单的例子:
![u=1728236203,636109669&fm=173&app=25&f=JPEG?w=639&h=420&s=7DA63D760942444D5AF5847A02009033](https://i-blog.csdnimg.cn/blog_migrate/1cd6a1c2fc061a2f94a2bc395755779a.jpeg)
这个函数接受3个数字,相乘并返回结果。我们用了全部的参数来调用这个乘法函数。让我们来创建一个柯里化版本的函数,然后来看看咱们是如何调用这个相同的函数(和返回相同的结果):
![u=1315428694,1517809743&fm=173&app=25&f=JPEG?w=640&h=556&s=FDA63D76594AEE4D1AF9857E0200C033](https://i-blog.csdnimg.cn/blog_migrate/4440984616d65ce5969fc49b88afd491.jpeg)
这里我们将multiply(1,2,3)调用变成了multiply (1) (2) (3) 调用。
单独一个函数被转换成了一系列函数。为了得到数字1、2、3相乘的结果,这些数字被一个接一个地传递,每个数字预填了下一个函数内联调用。
我们把multiply (1) (2) (3) 分割一下来帮助理解:
![u=2798253254,311204497&fm=173&app=25&f=JPEG?w=640&h=457&s=34B66D3609464D4D5271847E0200C033](https://i-blog.csdnimg.cn/blog_migrate/e402c40c2deabe7f747c4d03bd3e3aeb.jpeg)
当我们把1传递给multiply函数时,它返回了这个函数:
![u=3082364865,2268967999&fm=173&app=25&f=JPEG?w=640&h=508&s=ED863D7649526C4D0865847A0200C073](https://i-blog.csdnimg.cn/blog_migrate/398846f028db4d4485681d9a15b31539.jpeg)
现在,变量mul1拿到了上述这个需要参数b的函数定义。我们调用mul1函数并传2进去,它会返回下面这第三个函数:
![u=2335869393,2403947532&fm=173&app=25&f=JPEG?w=602&h=458&s=7DA63D7649526C4D4875C47A0200C033](https://i-blog.csdnimg.cn/blog_migrate/6acafcdf2e71d21b0b8979a48b1d7d91.jpeg)
这个返回的函数现在存进了mul2变量里,实质上,mul2就相当于:
![u=3245075308,1516217057&fm=173&app=25&f=JPEG?w=602&h=458&s=7DA63D7649526C494875847A0200C033](https://i-blog.csdnimg.cn/blog_migrate/965461f750f92cd31b78d08ce62c42be.jpeg)
当mul2使用3作为参数调用时,它一起使用了之前已拿到的参数a=1和b=2进行运算并返回结果6。
作为一个嵌套函数,mul2能够访问到外部的两个函数multiply和mul1的作用域。这就是为什么mul2能利用定义在已经‘离场’的函数中的参数来进行乘法操作的原因。即使这些函数早已返回并且从内存中垃圾回收了,但其变量仍然保持‘活着’。你可以看到3个数字每次只有1个提供给函数,并且同一时间里一个新函数会被返回,直到所有的数字用尽为止。
让我们来看另一个例子:
![u=1538149271,810668979&fm=173&app=25&f=JPEG?w=640&h=295&s=A5B66D3603526C634CDD017A0200C073](https://i-blog.csdnimg.cn/blog_migrate/928c509d8f826112a36e1f27bf5ac432.jpeg)
我们有一个函数用来计算固体的体积。柯里化版本的它会接受一个参数并返回一个函数,一直这样运行直到最后一个参数到达和最后一个函数被返回,然后再把最后一个参数与之前的参数进行乘法操作:
![u=2146986893,2145872075&fm=173&app=25&f=JPEG?w=640&h=376&s=75B62D7619124C4D18D8C17A0200C073](https://i-blog.csdnimg.cn/blog_migrate/84deef37dc1ae8612afd18752971c8db.jpeg)
就和我们在multiply函数中一样,最后一个函数只接受h参数,但是会与其他那些早已返回的函数作用域中的参数一起相乘,是闭包使这一切成为可能。
柯里化背后的思想就是获取一个函数并派生出一个返回特殊函数的函数。
|数学中的柯里化
我很喜欢数学举证,你可以到维基百科里找柯里化概念的进一步解释。让我们来用自己的例子看看,如果我们有一个等式:
![u=2804989847,121448494&fm=173&app=25&f=JPEG?w=602&h=378&s=FDA6357643124C64145080780200D033](https://i-blog.csdnimg.cn/blog_migrate/617efbfd35cb30d5410aa40efee30181.jpeg)
这里有x和y两个变量。如果x=3并且y=4,让我们来计算一下y:
![u=3958122400,4071526344&fm=173&app=25&f=JPEG?w=640&h=227&s=FD863D760B266D22065988F80200D033](https://i-blog.csdnimg.cn/blog_migrate/3f49c068bf319fdedd8ccc33dc7cccc0.jpeg)
得到了结果z=13。我们能柯里化 f (x, y) 来提供变量给一系列函数:
![u=3911701155,347911120&fm=173&app=25&f=JPEG?w=639&h=200&s=E5A63D761B664D225041607A0200D070](https://i-blog.csdnimg.cn/blog_migrate/2e1e8d7cb86c767bc2c041d9ac835c90.jpeg)
如果我们在等式hx(y) = x^2 + y中固定x=3,它会返回一个以y为变量的新等式:
![u=618304416,2655074299&fm=173&app=25&f=JPEG?w=640&h=398&s=FDA635760D4A644D5AF4C57A0200C033](https://i-blog.csdnimg.cn/blog_migrate/bcac15c012dd48fb9d6ca6b7daa3fad6.jpeg)
跟这个等价:
![u=3450250338,2535569369&fm=173&app=25&f=JPEG?w=639&h=236&s=7DA63D760B626D2056D0C9780200C033](https://i-blog.csdnimg.cn/blog_migrate/3dc5860361f34ef27b0985f8134a85c7.jpeg)
最终结果还没有确定下来,它只是返回了一个新等式(9+y)并且等待着另一个参数y。接下来我们传递y=4:
![u=3494818916,680494536&fm=173&app=25&f=JPEG?w=640&h=261&s=FDA63D7607024D62125800F80200C032](https://i-blog.csdnimg.cn/blog_migrate/60e4869a94e4b9c81b54d719bd6aeeee.jpeg)
y是变量链条中的最后一个,与之前仍保留着的变量x=3做加法运算,得出了结果13。
基本上,我们将方程f(x,y)= 3 ^ 2 + y柯里化为一系列方程式:
![u=3958057811,3323285789&fm=173&app=25&f=JPEG?w=640&h=383&s=FDA63D7609D24C491245447A0200D033](https://i-blog.csdnimg.cn/blog_migrate/a6ed6ca912ac53255e5baee843902467.jpeg)
这里如果你觉得不够清楚的话,可以到这(https://en.m.wikipedia.org/wiki/Currying)看详情。
|柯里化和部分函数应用
现在,有些人可能开始在想柯里化函数的嵌套函数数量取决于它接收的参数数量。但是我也能这样来设计我的体积计算的柯里化函数:
![u=3445608118,2719957097&fm=173&app=25&f=JPEG?w=640&h=508&s=FDA63D7649426C4D48F5847E0200C073](https://i-blog.csdnimg.cn/blog_migrate/aba853fc5c604ecafc84aaafe90e42a2.jpeg)
所以它能被这样来调用:
![u=3249643426,3001986823&fm=173&app=25&f=JPEG?w=640&h=713&s=E5F62D76118ECF4D02C9D1FA02005070](https://i-blog.csdnimg.cn/blog_migrate/3c717f4f009f31fae758c1ef01c47964.jpeg)
我们只是定义了一个特殊函数用来计算长度为70的任何物体的体积。这里有3个参数和2个嵌套函数,不像我们之前的版本有3个参数和3个嵌套函数。这个版本不能称之为柯里化,我们只是做了volume函数的部分应用。
柯里化和部分应用是有关联的,但是它们也有些不同的概念。
部分应用将一个函数转换为另一个更少参数的函数:
![u=792577860,1235597005&fm=173&app=25&f=JPEG?w=640&h=629&s=F5A63D76190EE44D5275907B0200C073](https://i-blog.csdnimg.cn/blog_migrate/e6902af4b3d89e6a73d65ad2e5a5b584.jpeg)
注意:我故意省略了performOp函数的实现。因为这里没有必要。 所有你需要了解的是柯里化和部分应用背后的概念。
这是acidityRatio函数的部分应用,这里没有涉及到柯里化。 acidityRatio函数被部分应用于接受比原函数更少的参数。
如果要把它柯里化,它看起来会是这样的:
![u=2509756908,3307776794&fm=173&app=25&f=JPEG?w=639&h=444&s=F5A63D7609426C4D14D1847E0200D073](https://i-blog.csdnimg.cn/blog_migrate/7b38187ea7137d77ab397f72d3a58d8a.jpeg)
柯里化根据函数的参数数量来创建嵌套函数,每个函数接受一个参数,如果没有参数的话也就不存在柯里化。
这里存在一种情况柯里化和部分应用会彼此相遇,让我们来看一个函数:
![u=3156565491,2132261518&fm=173&app=25&f=JPEG?w=582&h=458&s=FDA63D7649424C4D4874847E02008033](https://i-blog.csdnimg.cn/blog_migrate/c29531adae471e5d6cd477cc81a7296b.jpeg)
它的柯里化和部分应用都是这样的:
![u=776851395,3091195141&fm=173&app=25&f=JPEG?w=582&h=538&s=FDA63D7649426E4D0870847E02008073](https://i-blog.csdnimg.cn/blog_migrate/a907b8e4bdc63ac9f2d1d2d7b4358d64.jpeg)
虽然柯里化和部分应用给出了一样的结果,但它们是两个不同的实体。
就像我们之前说过的,柯里化和部分应用是有关联的,但其实设计上并不一样。它们之间的共通点是他们都依靠闭包来工作。
|柯里化有用吗?
当然有用,柯里化用来:
1. 编写可以轻松复用和配置的小代码块,就像我们使用npm一样:
举个例子,你有一家商店,然后你想给你的优惠顾客10%的折扣:
![u=954573915,72816055&fm=173&app=25&f=JPEG?w=639&h=321&s=75A63D7601526C615655887C0200C073](https://i-blog.csdnimg.cn/blog_migrate/e46a3789fbed51b306674b2887c46abb.jpeg)
当一个优惠顾客消费了500元,你会给他:
![u=1196873865,3684029830&fm=173&app=25&f=JPEG?w=639&h=265&s=75B63D7603D04C60127060FC0200C033](https://i-blog.csdnimg.cn/blog_migrate/7b3cc4b8cb30d46cc23c5f74a2f94d75.jpeg)
从长远的看,你会发现你每天都要计算10%的折扣。
![u=4036574411,1951403679&fm=173&app=25&f=JPEG?w=640&h=461&s=E5F6AD7613FEC9CC18C9257E0200C032](https://i-blog.csdnimg.cn/blog_migrate/b01b8fe70a7170135b97c99647fd45c9.jpeg)
我们能将这个函数柯里化,然后我们就不用每次都写那0.10了:
![u=2841838037,2679423675&fm=173&app=25&f=JPEG?w=640&h=367&s=75B62D76094A4C4D1ED9857E02005073](https://i-blog.csdnimg.cn/blog_migrate/5efd816fd7e2f84abaf44cc1eebd9ccc.jpeg)
现在,我们只需用商品价格来计算就可以了:
![u=841008259,1673353687&fm=173&app=25&f=JPEG?w=640&h=328&s=EDA63D760BD26C411278207002008033](https://i-blog.csdnimg.cn/blog_migrate/3f1af52240fc4155bdd09c11231dcf5c.jpeg)
接下来,有些优惠顾客越来越重要,让我们称为vip顾客,然后我们要给20%的折扣,我们这样来使用柯里化了的discount函数:
![u=3742179233,370896821&fm=173&app=25&f=JPEG?w=639&h=227&s=74B63D7613624D205451687202005032](https://i-blog.csdnimg.cn/blog_migrate/1b8dd154217454661fc7c84668284f05.jpeg)
我们为vip顾客使用0.2调用柯里化discount函数来配置了一个新的函数。这个twentyPercentDiscount函数会被用来计算vip顾客的折扣:
![u=261460856,1114516252&fm=173&app=25&f=JPEG?w=640&h=367&s=E5B2257611DACDC81ACD057602005072](https://i-blog.csdnimg.cn/blog_migrate/e8ea17c2489095d742fe01a6799fe394.jpeg)
2. 避免频繁调用具有相同参数的函数:
比如我们有个用来计算体积的函数:
![u=2496657238,2645295718&fm=173&app=25&f=JPEG?w=640&h=408&s=7DA63D7641524C6D5A55807802008033](https://i-blog.csdnimg.cn/blog_migrate/425198ecd80a812faf70bb546786fd49.jpeg)
碰巧你仓库里的所有物品都是100m高。你会看到你不停地用h=100来调用这个函数:
![u=2678610945,1878687278&fm=173&app=25&f=JPEG?w=639&h=352&s=75A63D76114A4D4D0065D0F00200C033](https://i-blog.csdnimg.cn/blog_migrate/dbe84098ec998b3731497f636d22f616.jpeg)
为了解决这个问题,你把volume函数柯里化(像我们之前做过的):
![u=3474715769,303657429&fm=173&app=25&f=JPEG?w=640&h=522&s=FDA63D7649426C4D18F1847E02008033](https://i-blog.csdnimg.cn/blog_migrate/1ef8ae34a3cdae74f73d5943ec475a89.jpeg)
我们能给同类物品定义一个特殊函数:
![u=1571270630,3708152014&fm=173&app=25&f=JPEG?w=640&h=291&s=E5F63D7609D04C41146540E20200C033](https://i-blog.csdnimg.cn/blog_migrate/943b74fdc775d39bb45acec7e02285fa.jpeg)
|通用的柯里函数
让我们建立一个函数来接受任何函数并且返回柯里化版本的函数。
为此我们需要这样做(这里是作者的简单版本,看看就好了,不作为参考。你会有不一样的做法):
![u=2911821323,640934690&fm=173&app=25&f=JPEG?w=640&h=378&s=F5B63D7619424C4D5E55847E02001073](https://i-blog.csdnimg.cn/blog_migrate/4d3c270b06a54f26ccbbd276e79f73c7.jpeg)
我们在这里做了什么?我们的curry函数接受一个我们想要柯里化的函数(fn)和一个变量(...args)。这里的rest操作符用来将参数聚集成一个...args。接下来我们返回一个函数,该函数将其余参数收集为..._args。此函数通过spread运算符将... args和..._ args作为参数解构传入来调用原始函数fn,然后将值返回给用户。
让我们使用我们的curry函数来创建一个特殊的函数(一个专门用来计算100m长度的物品体积):
![u=1736375111,2831012930&fm=173&app=25&f=JPEG?w=640&h=466&s=F5B63D76014AED4D14FD917A0200C033](https://i-blog.csdnimg.cn/blog_migrate/717156aff332b8d798292e7de4311260.jpeg)
结语
闭包使在JavaScript中进行柯里化成为可能。它能够保留已经执行的函数的状态,使我们能够创建工厂函数(可以为其参数添加特定值的函数)。
柯里化、闭包和函数式编程是非常棘手的。 但我向你保证只要花时间不断地练习,你会开始掌握它,看到它是多么值得。