「译」理解JavaScript的柯里化

理解JavaScript的柯里化

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

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

因为这种风格具备传递和返回函数的能力,它带来了许多概念:git

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

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

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

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

什么是柯里化?

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

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

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

注意:上面的术语 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;
    }
}
复制代码

尽管柯里化和部分应用得出了相同的结果,可是它们是两个彻底不一样的概念。

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

柯里化有用吗?

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

一、编写小模块的代码,能够更轻松的重用和配置,就行 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
复制代码

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

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

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

相关文章
相关标签/搜索