函數式編程是一種編程風格,這種編程風格就是試圖將傳遞函數做爲參數(即將做爲回調函數)和返回一個函數,但沒有函數反作用(函數反作用即會改變程序的狀態)。 有不少語言採用這種編程風格,其中包括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;
}
複製代碼
這個函數接收三個數字而且相乘,而後返回計算結果。bash
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)多個函數調用的形式。 一個單獨的函數已經轉化爲一系列的函數。爲了獲得三個數字一、二、3的乘法結果,這些數字一個接一個傳遞,每一個數字會預先填充用做下一個函數內聯調用。 咱們能夠分開這個multiply(1)(2)(3)函數調用步驟,更好理解一點。app
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;
}
}
複製代碼
雖然柯里化和部分應用函數給出一樣地結果,但它們是兩個不一樣的存在。 像咱們以前說的,柯里化和部分應用是相關的,但設計上實際是徹底不同的。相同之處就是它們都依賴閉包。
固然有用,柯里化立刻能派上用場,若是你想:
舉個例子,好比你有一間士多店而且你想給你優惠的顧客給個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柯里化成爲可能。可以保留已經執行的函數的狀態,使咱們可以建立工廠函數 - 能夠爲其參數添加特定值的函數。柯里化、閉包和函數式編程是很棘手的。可是我能夠保證,投入時間和練習,你就會開始掌握它,看看它多麼有價值。
以上譯文僅用於學習交流,水平有限,不免有錯誤之處,敬請指正。