javascript
【译】理解JavaScript中的柯里化
譯文開始
函數式編程是一種編程風格,這種編程風格就是試圖將傳遞函數作為參數(即將作為回調函數)和返回一個函數,但沒有函數副作用(函數副作用即會改變程序的狀態)。
有很多語言采用這種編程風格,其中包括JavaScript、Haskell、Clojure、Erlang和Scala等一些很流行的編程語言。
函數式編程憑借其傳遞和返回函數的能力,帶來了許多概念:
- 純函數
- 柯里化
- 高階函數
其中一個我們將要看到的概念就是柯里化。
在這篇文章,我們將看到柯里化是如何工作以及它如何在我們作為軟件開發者的工作中發揮作用。
什么是柯里化
柯里化是函數式編程中的一種過程,可以將接受具有多個參數的函數轉化為一個的嵌套函數隊列,然后返回一個新的函數以及期望下一個的內聯參數。它不斷返回一個新函數(期望當前參數,就像我們之前說的那樣)直到所有參數都用完為止。這些參數會一直保持“存活”不會被銷毀(利用閉包的特性)以及當柯里化鏈中最后的函數返回并執行時,所有參數都用于執行。
柯里化就是將具有多個arity的函數轉化為具有較少的arity的函數。——kbrainwave
備注:術語arity(元數):指的是函數的參數個數,例如:
函數fn有兩個參數(即 2-arity函數)以及_fn有三個參數(即3-arity函數)。
因此,柯里化將一個具有多個參數的函數轉化為一系列只需一個參數的函數。
下面,我們看一個簡單的例子:
這個函數接收三個數字并且相乘,然后返回計算結果。
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、2、3的乘法結果,這些數字一個接一個傳遞,每個數字會預先填充用作下一個函數內聯調用。
我們可以分開這個multiply(1)(2)(3)函數調用步驟,更好理解一點。
我們來一個接一個地傳遞參數。首先傳參數1到multiply函數:
let mul1 = multiply(1);以上代碼執行會返回一個函數:
return (b) => {return (c) => {return a * b * c}}現在,變量mul1會保持以上的函數定義,這個函數接收參數b。
我們調用函數mul1,傳入參數2:
函數mul1執行后會返回第三個函數
return (c) => {return a * b * c}這個返回的函數現在保存在變量mul2中。
本質上,變量mul2可以這么理解:
當傳入參數3調用函數mul2時,
const result = mul2(3);會使用之前傳入的參數進行計算:a=1,b=2,然后結果為6。
log(result); // 6作為一個嵌套函數,mul2函數可以訪問外部函數的變量作用域,即multiply函數和mul1函數。
這就是為什么mul2函數能使用已經執行完函數中定義的變量中進行乘法計算。雖然函數早已返回而且已經在內存中執行垃圾回收。但是它的變量還是以某種方式保持“存活”。
備注:以上變量保持存活是閉包特性,不明白可以查看閉包相關文章了解更多
你可以看到三個數字每次只傳遞一個參數應用于函數,并且每次都返回一個新的函數,值得所有的參數用完為止。
下面來看一個其他的例子:
上面是一個計算任何實心形狀體積的函數。
這個柯里化版本將接受一個參數以及返回一個函數,該函數同樣也接受一個參數和返回一個新的函數。然后一直這樣循環/繼續,直到到達最后一個參數并返回最后一個函數。然后執行之前的參數和最后一個參數的乘法運算。
就像之前的multiply函數那樣,最后的函數只接受一個參數h,但是仍然會對那些早已執行完返回的函數作用域中里的其他變量執行操作。能這樣操作是因為有閉包的特性。
譯者注:以上寫的很啰嗦,感覺另外的例子完全就是重復說明。
柯里化背后的想法其實是獲取一個函數并派生出一個返回特殊函數的函數。
柯里化在數學方面的應用
我有點喜歡數學說明?維基百科進一步展示了柯里化的概念。下面用我們的例子來進一步看下柯里化。
假設有一個方程
有兩個變量x和y,如果這兩個變量分別賦值x=3和y=4,可以得到z的值。
下面我們在函數f(x,y)中替換變量的值為y=4和x=3:
得到z的結果為13
我們也可以將f(x,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
變量y是變量鏈中的最后一個,然后與前一個保留的變量x=3執行加法運算,值最后被解析,結果是12。
所以基本上,我們將這個方程f(x,y)=3^2+y柯里化為一系列的方程式,在最終結果得到之前。
好了,這就是柯里化在數學方面的一些應用,如果你覺得這些說明得還不夠清楚。可以在維基百科閱讀更詳細的內容。
柯里化和部分應用函數
現在,有些人可能開始認為柯里化函數的嵌套函數的數量取決于它接受的參數。是的,這就是柯里化。
我可以設計一個這樣的柯里化函數volume:
所以,可以像這樣去調用:
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函數。
柯里化和部分應用函數有關聯,但是它們是不同的概念。
部分應用函數是將一個函數轉化為具有更少的元素(即更是的參數)的函數。
注:我故意沒有實現performOp函數。因為這里,這個不是必要的。你所需要知道的是柯里化和部分應用函數背后的概念就可以。
這是acidityRatio函數的部分應用,并沒有涉及柯里化。acidityRatio函數應用于接受更少的元數,比原來的函數期望更少的參數。
柯里化可以這樣實現:
柯里化是根據函數的參數數量創建嵌套函數,每個函數接受一個參數。如果沒有參數,那就沒有柯里化。
可能存在一種情況,即柯里化和部分應用彼此相遇。假設我們有一個函數:
如果寫出部分應用形式,得到的結果:
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我們可以將discount函數柯里化,這樣我們就不用總是每次增加這0.01的折扣。
function discount(discount) {return (price) => {return price * discount;} } const tenPercentDiscount = discount(0.1);現在,我們可以只計算你的顧客買的物品都價格了:
tenPercentDiscount(500); // $50 // $500 - $50 = $450同樣地,有些優惠顧客比一些優惠顧客更重要-讓我們稱之為超級客戶。并且我們想給這些超級客戶提供20%的折扣。
可以使用我們的柯里化的discount函數:
我們通過這個柯里化的discount函數折扣調為0.2(即20%),給我們的超級客戶配置了一個新的函數。
返回的函數twentyPercentDiscount將用于計算我們的超級客戶的折扣:
2、避免頻繁調用具有相同參數的函數
舉個例子,我們有一個計算圓柱體積的函數
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通用的柯里化函數
我們來開發一個函數,它接受任何函數并返回一個柯里化版本的函數。
要做到這點,我們將有這個(雖然你的方法可能跟我的不一樣):
上面代碼做了什么?curry函數接受一個我們想要柯里化的函數(fn)和 一些可變數量的參數(…args)。剩下的操作用于將fn之后的參數數量收集到…args中。
然后,返回一個函數,同樣地將余下的參數收集為…args。這個函數調用原始函數fn通過使用spread運算符作為參數傳入... args和... args,然后,將值返回給使用。
現在我們可以用curry函數來創建特定的函數啦。
下面我們用curry函數來創建更多計算體檢的特定函數(其中一個就是計算高度100米的圓柱體積函數)
結語
閉包使JavaScript柯里化成為可能。能夠保留已經執行的函數的狀態,使我們能夠創建工廠函數 - 可以為其參數添加特定值的函數。柯里化、閉包和函數式編程是很棘手的。但是我可以保證,投入時間和練習,你就會開始掌握它,看看它多么有價值。
參考
柯里化-維基百科
部分應用函數
(完)
后記
以上譯文僅用于學習交流,水平有限,難免有錯誤之處,敬請指正。
原文
https://blog.bitsrc.io/understanding-currying-in-javascript-ceb2188c339
轉載于:https://www.cnblogs.com/GeniusLyzh/p/9937829.html
總結
以上是生活随笔為你收集整理的【译】理解JavaScript中的柯里化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 写给正在入坑linux系统的伙伴
- 下一篇: 约束 抛异常