「翻譯」JavaScript中的柯里化(Currying in JavaScript)

原文來自 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

這裡的‘元’,指的是函數所需要的參數數量,舉個例子:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

函數fn接受2個參數(2元函數),_fn接受3個參數(3元函數)。所以,柯里化將一個多參數的函數轉換成一系列只接受單個參數的函數。

讓我們來看個簡單的例子:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

這個函數接受3個數字,相乘並返回結果。我們用了全部的參數來調用這個乘法函數。讓我們來創建一個柯里化版本的函數,然後來看看咱們是如何調用這個相同的函數(和返回相同的結果):

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

這裡我們將multiply(1,2,3)調用變成了multiply (1) (2) (3) 調用。

單獨一個函數被轉換成了一系列函數。為了得到數字1、2、3相乘的結果,這些數字被一個接一個地傳遞,每個數字預填了下一個函數內聯調用。

我們把multiply (1) (2) (3) 分割一下來幫助理解:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

當我們把1傳遞給multiply函數時,它返回了這個函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

現在,變量mul1拿到了上述這個需要參數b的函數定義。我們調用mul1函數並傳2進去,它會返回下面這第三個函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

這個返回的函數現在存進了mul2變量裡,實質上,mul2就相當於:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

當mul2使用3作為參數調用時,它一起使用了之前已拿到的參數a=1和b=2進行運算並返回結果6。

作為一個嵌套函數,mul2能夠訪問到外部的兩個函數multiply和mul1的作用域。這就是為什麼mul2能利用定義在已經‘離場’的函數中的參數來進行乘法操作的原因。即使這些函數早已返回並且從內存中垃圾回收了,但其變量仍然保持‘活著’。你可以看到3個數字每次只有1個提供給函數,並且同一時間裡一個新函數會被返回,直到所有的數字用盡為止。

讓我們來看另一個例子:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

我們有一個函數用來計算固體的體積。柯里化版本的它會接受一個參數並返回一個函數,一直這樣運行直到最後一個參數到達和最後一個函數被返回,然後再把最後一個參數與之前的參數進行乘法操作:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

就和我們在multiply函數中一樣,最後一個函數只接受h參數,但是會與其他那些早已返回的函數作用域中的參數一起相乘,是閉包

使這一切稱為可能。

柯里化背後的思想就是獲取一個函數並派生出一個返回特殊函數的函數。

數學中的柯里化

我很喜歡數學舉證,你可以到維基百科裡找柯里化概念的進一步解釋。讓我們來用自己的例子看看,如果我們有一個等式:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

這裡有x和y兩個變量。如果x=3並且y=4,讓我們來計算一下y:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

得到了結果z=13。我們能柯里化 f (x, y) 來提供變量給一系列函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

如果我們在等式hx(y) = x^2 + y中固定x=3,它會返回一個以y為變量的新等式:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

跟這個等價:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

最終結果還沒有確定下來,它只是返回了一個新等式(9+y)並且等待著另一個參數y。接下來我們傳遞y=4:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

y是變量鏈條中的最後一個,與之前仍保留著的變量x=3做加法運算,得出了結果13。

基本上,我們將方程f(x,y)= 3 ^ 2 + y柯里化為一系列方程式:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

這裡如果你覺得不夠清楚的話,可以到這(https://en.m.wikipedia.org/wiki/Currying)看詳情。

柯里化和部分函數應用

現在,有些人可能開始在想柯里化函數的嵌套函數數量取決於它接收的參數數量。但是我也能這樣來設計我的體積計算的柯里化函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

所以它能被這樣來調用:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

我們只是定義了一個特殊函數用來計算長度為70的任何物體的體積。這裡有3個參數和2個嵌套函數,不像我們之前的版本有3個參數和3個嵌套函數。這個版本不能稱之為柯里化,我們只是做了volume函數的部分應用

柯里化和部分應用是有關聯的,但是它們也有些不同的概念。

部分應用將一個函數轉換為另一個更少參數的函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

注意:我故意省略了performOp函數的實現。因為這裡沒有必要。 所有你需要了解的是柯里化和部分應用背後的概念。

這是acidityRatio函數的部分應用,這裡沒有涉及到柯里化。 acidityRatio函數被部分應用於接受比原函數更少的參數。

如果要把它柯里化,它看起來會是這樣的:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

柯里化根據函數的參數數量來創建嵌套函數,每個函數接受一個參數,如果沒有參數的話也就不存在柯里化。

這裡存在一種情況柯里化和部分應用會彼此相遇,讓我們來看一個函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

它的柯里化和部分應用都是這樣的:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

雖然柯里化和部分應用給出了一樣的結果,但它們是兩個不同的實體。

就像我們之前說過的,柯里化和部分應用是有關聯的,但其實設計上並不一樣。它們之間的共通點是他們都依靠閉包來工作。

柯里化有用嗎?

當然有用,柯里化用來:

1. 編寫可以輕鬆複用和配置的小代碼塊,就像我們使用npm一樣:

舉個例子,你有一家商店,然後你想給你的優惠顧客10%的折扣:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

當一個優惠顧客消費了500元,你會給他:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

從長遠的看,你會發現你每天都要計算10%的折扣。

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

我們能將這個函數柯里化,然後我們就不用每次都寫那0.10了:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

現在,我們只需用商品價格來計算就可以了:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

接下來,有些優惠顧客越來越重要,讓我們稱為vip顧客,然後我們要給20%的折扣,我們這樣來使用柯里化了的discount函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

我們為vip顧客使用0.2調用柯里化discount函數來配置了一個新的函數。這個twentyPercentDiscount函數會被用來計算vip顧客的折扣:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

2. 避免頻繁調用具有相同參數的函數:

比如我們有個用來計算體積的函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

碰巧你倉庫裡的所有物品都是100m高。你會看到你不停地用h=100來調用這個函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

為了解決這個問題,你把volume函數柯里化(像我們之前做過的):

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

我們能給同類物品定義一個特殊函數:

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

通用的柯里函數

讓我們建立一個函數來接受任何函數並且返回柯里化版本的函數。

為此我們需要這樣做(這裡是作者的簡單版本,看看就好了,不作為參考。你會有不一樣的做法):

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

我們在這裡做了什麼?我們的curry函數接受一個我們想要柯里化的函數(fn)和一個變量(...args)。這裡的rest操作符用來將參數聚集成一個...args。接下來我們返回一個函數,該函數將其餘參數收集為..._args。此函數通過spread運算符將... args和..._ args作為參數解構傳入來調用原始函數fn,然後將值返回給用戶。

讓我們使用我們的curry函數來創建一個特殊的函數(一個專門用來計算100m長度的物品體積):

「翻譯」JavaScript中的柯里化(Currying in JavaScript)

結語

閉包使在JavaScript中進行柯里化成為可能。它能夠保留已經執行的函數的狀態,使我們能夠創建工廠函數(可以為其參數添加特定值的函數)。

柯里化、閉包和函數式編程是非常棘手的。 但我向你保證只要花時間不斷地練習,你會開始掌握它,看到它是多麼值得。


分享到:


相關文章: