「譯」理解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

相關文章
相關標籤/搜索