【译】理解JavaScript中的柯里化

译文开始

函数式编程是一种编程风格,这种编程风格就是试图将传递函数做为参数(即将做为回调函数)和返回一个函数,但没有函数反作用(函数反作用即会改变程序的状态)。
有不少语言采用这种编程风格,其中包括JavaScript、Haskell、Clojure、Erlang和Scala等一些很流行的编程语言。
函数式编程凭借其传递和返回函数的能力,带来了许多概念:javascript

  • 纯函数
  • 柯里化
  • 高阶函数
    其中一个咱们将要看到的概念就是柯里化。
    在这篇文章,咱们将看到柯里化是如何工做以及它如何在咱们做为软件开发者的工做中发挥做用。

什么是柯里化

柯里化是函数式编程中的一种过程,能够将接受具备多个参数的函数转化为一个的嵌套函数队列,而后返回一个新的函数以及指望下一个的内联参数。它不断返回一个新函数(指望当前参数,就像咱们以前说的那样)直到全部参数都用完为止。这些参数会一直保持“存活”不会被销毁(利用闭包的特性)以及当柯里化链中最后的函数返回并执行时,全部参数都用于执行。java

柯里化就是将具备多个arity的函数转化为具备较少的arity的函数。——kbrainwave
备注:术语arity(元数):指的是函数的参数个数,例如:npm

function fn(a, b) {
    //...
}
function _fn(a, b, c) {
    //...
}

函数fn有两个参数(即 2-arity函数)以及_fn有三个参数(即3-arity函数)。
所以,柯里化将一个具备多个参数的函数转化为一系列只需一个参数的函数。
下面,咱们看一个简单的例子:编程

function multiply(a, b, c) {
    return a * b * c;
}

这个函数接收三个数字而且相乘,而后返回计算结果。闭包

multiply(1,2,3); // 6

接下来看看,咱们如何用完整参数调用乘法函数。咱们来建立一个柯里化版本的函数,而后看看如何在一系列调用中调用相同的函数(而且获得一样的结果)。app

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)多个函数调用的形式。
一个单独的函数已经转化为一系列的函数。为了获得三个数字一、二、3的乘法结果,这些数字一个接一个传递,每一个数字会预先填充用做下一个函数内联调用。
咱们能够分开这个multiply(1)(2)(3)函数调用步骤,更好理解一点。编程语言

const mul1 = multiply(1);
const mul2 = mul1(2);
const result = mul2(3);
log(result); // 6

咱们来一个接一个地传递参数。首先传参数1到multiply函数:函数式编程

let mul1 = multiply(1);

以上代码执行会返回一个函数:函数

return (b) => {
        return (c) => {
            return a * b * c
        }
    }

如今,变量mul1会保持以上的函数定义,这个函数接收参数b。
咱们调用函数mul1,传入参数2:学习

let mul2 = mul1(2);

函数mul1执行后会返回第三个函数

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函数能够访问外部函数的变量做用域,即multiply函数和mul1函数。
这就是为何mul2函数能使用已经执行完函数中定义的变量中进行乘法计算。虽然函数早已返回并且已经在内存中执行垃圾回收。可是它的变量仍是以某种方式保持“存活”。

备注:以上变量保持存活是闭包特性,不明白能够查看闭包相关文章了解更多
你能够看到三个数字每次只传递一个参数应用于函数,而且每次都返回一个新的函数,值得全部的参数用完为止。
下面来看一个其余的例子:

function volume(l,w,h) {
    return l * w * h;
}
const aCylinder = volume(100,20,90) // 180000

上面是一个计算任何实心形状体积的函数。
这个柯里化版本将接受一个参数以及返回一个函数,该函数一样也接受一个参数和返回一个新的函数。而后一直这样循环/继续,直到到达最后一个参数并返回最后一个函数。而后执行以前的参数和最后一个参数的乘法运算。

function volume(l) {
    return (w) => {
        return (h) => {
            return l * w * h
        }
    }
}
const aCylinder = volume(100)(20)(90) // 180000

就像以前的multiply函数那样,最后的函数只接受一个参数h,可是仍然会对那些早已执行完返回的函数做用域中里的其余变量执行操做。能这样操做是由于有闭包的特性。

译者注:以上写的很啰嗦,感受另外的例子彻底就是重复说明。
柯里化背后的想法实际上是获取一个函数并派生出一个返回特殊函数的函数。

柯里化在数学方面的应用

我有点喜欢数学说明👉维基百科进一步展现了柯里化的概念。下面用咱们的例子来进一步看下柯里化。
假设有一个方程

f(x,y) = x^2 + y = z

有两个变量x和y,若是这两个变量分别赋值x=3和y=4,能够获得z的值。
下面咱们在函数f(x,y)中替换变量的值为y=4和x=3:

f(x,y) = f(3,4) = x^2 + y = 3^2 + 4 = 13 = z

获得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]

注:hx表示h下标为x的标识符,同理hy表示h下标为y的标识符。w.r.t(with respect to),数学符号,表示关于,经常使用于求导,或者知足必定条件之类的状况

咱们使方程f(x,y)=x^2+y的变量x=3,它将返回一个以y为变量的新方程。

h3(y) = 3^2 + y = 9 + y
注:h3 表示h下标为3的标识符

也等同于:

h3(y) = h(3)(y) = f(3,y) = 3^2 + y = 9 + y

函数的结果仍是没有肯定的,而是返回一个指望其余变量y的一个新方程 9+y。
下一步,咱们传入y=4

h3(4) = h(3)(4) = f(3,4) = 9 + 4 = 13

变量y是变量链中的最后一个,而后与前一个保留的变量x=3执行加法运算,值最后被解析,结果是12。
因此基本上,咱们将这个方程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

好了,这就是柯里化在数学方面的一些应用,若是你以为这些说明得还不够清楚。能够在维基百科阅读更详细的内容。

柯里化和部分应用函数

如今,有些人可能开始认为柯里化函数的嵌套函数的数量取决于它接受的参数。是的,这就是柯里化。
我能够设计一个这样的柯里化函数volume:

function volume(l) {
    return (w, h) => {
        return l * w * h
    }
}

因此,能够像这样去调用:

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层嵌套函数的版本不同。
可是这个版本并非柯里化。咱们只是作了一个部分应用的volume函数。
柯里化和部分应用函数有关联,可是它们是不一样的概念。
部分应用函数是将一个函数转化为具备更少的元素(即更是的参数)的函数。

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)
        }
    }
}

柯里化是根据函数的参数数量建立嵌套函数,每一个函数接受一个参数。若是没有参数,那就没有柯里化。
可能存在一种状况,即柯里化和部分应用彼此相遇。假设咱们有一个函数:

function div(x,y) {
    return x/y;
}

若是写出部分应用形式,获得的结果:

function div(x) {
    return (y) => {
        return x/y;
    }
}

一样地,柯里化也是一样地结果:

function div(x) {
    return (y) => {
        return x/y;
    }
}

虽然柯里化和部分应用函数给出一样地结果,但它们是两个不一样的存在。
像咱们以前说的,柯里化和部分应用是相关的,但设计上实际是彻底不同的。相同之处就是它们都依赖闭包。

函数柯里化有用吗?

固然有用,柯里化立刻能派上用场,若是你想:

一、编写轻松重用和配置的小代码块,就像咱们使用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

咱们能够将discount函数柯里化,这样咱们就不用老是每次增长这0.01的折扣。

function discount(discount) {
    return (price) => {
        return price * discount;
    }
}
const tenPercentDiscount = discount(0.1);

如今,咱们能够只计算你的顾客买的物品都价格了:

tenPercentDiscount(500); // $50
// $500 - $50 = $450

一样地,有些优惠顾客比一些优惠顾客更重要-让咱们称之为超级客户。而且咱们想给这些超级客户提供20%的折扣。
可使用咱们的柯里化的discount函数:

const twentyPercentDiscount = discount(0.2);

咱们经过这个柯里化的discount函数折扣调为0.2(即20%),给咱们的超级客户配置了一个新的函数。
返回的函数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

二、避免频繁调用具备相同参数的函数

举个例子,咱们有一个计算圆柱体积的函数

function volume(l, w, h) {
    return l * w * h;
}

碰巧仓库全部的气缸高度为100米,你将会看到你将重复调用此函数,h为100米

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

要解决以上问题,你能够将volume函数柯里化(像咱们以前作的):

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);
    }
}

上面代码作了什么?curry函数接受一个咱们想要柯里化的函数(fn)和 一些可变数量的参数(…args)。剩下的操做用于将fn以后的参数数量收集到…args中。
而后,返回一个函数,一样地将余下的参数收集为…args。这个函数调用原始函数fn经过使用spread运算符做为参数传入... args和... args,而后,将值返回给使用。
如今咱们能够用curry函数来建立特定的函数啦。
下面咱们用curry函数来建立更多计算体检的特定函数(其中一个就是计算高度100米的圆柱体积函数)

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

结语

闭包使JavaScript柯里化成为可能。可以保留已经执行的函数的状态,使咱们可以建立工厂函数 - 能够为其参数添加特定值的函数。柯里化、闭包和函数式编程是很棘手的。可是我能够保证,投入时间和练习,你就会开始掌握它,看看它多么有价值。

参考

柯里化-维基百科
部分应用函数
(完)

后记

以上译文仅用于学习交流,水平有限,不免有错误之处,敬请指正。

原文

https://blog.bitsrc.io/understanding-currying-in-javascript-ceb2188c339

相关文章
相关标签/搜索