Jamie Chien

Jamie Chien • 2023-06-25

Javascript Currying(柯里化)

JS Curry

前言

說來慚愧,寫了這麼久的 Javascript,一直沒有好好認識認識 Curry Function,這一篇文章就要來說說什麼是 Currying,以及為什麼需要認識他。 在文章的後半段,會提供一個實際在 React 中應用 Currying 的例子,那麼廢話不多說,開始吧~

什麼是 Currying(柯里化)

引用一下維基說的話,柯里化就是『把接受多個參數的 function 轉變成多個接受單一參數的 functions』的過程。

把傳入 function 的參數,透過 closure(閉包)來存放在 function,並透過這個 function 裡面另外的 function 來回傳,來達成 currying。

e.g. 在沒有 Currying 的情況下:

function sum(a, b, c) {
  return a + b + c;
}

sum(1, 2, 3); // 6

轉變成 Curry function 之後:

function sum(a) {
  return function (b) {
    return function (c) {
      return a + b + c;
    };
  };
}

sum(1)(2)(3); //6

從上面的範例中,可以很清楚的看出兩者之間的差異,但這時候可能會很好奇,為什麼需要這樣寫呢?往下看,下面會透過一些例子來解釋一下為什麼需要 Currying(柯里化)

為什麼需要 Currying(柯里化)

簡單說有幾個好處:

  1. One at a Time(一次處理一個參數),每個 function 可以各自處理傳入的參數,藉此提高程式的彈性
  2. 也因為拆分成許多片段,所以可以更好地重複利用

這樣說可能會有點抽象,舉個例子:

假設今天要計算一個商品的售價,那剛好今天全館商品打 8 折,在沒有 Currying 的情況下的計算函式會像下面這樣

function calcDiscountPrice(price, discount) {
  return price * discount;
}

calcDiscountPrice(100, 0.8); //80
calcDiscountPrice(80, 0.8); //64
...

轉成 Curry function 之後:

function calcDiscountPrice(discount) {
  return function (price) {
    return price * discount;
  };
}

const calcWith20PercentOff = calcDiscountPrice(0.8);
calcWith20PercentOff(100); //80
calcWith20PercentOff(80); //64
...

看出差別了嗎?當我們遇到全館商品都打 8 折的情況下,就不需要每次計算價格時,都還要再帶入一次折扣,一方面減少錯誤機率,另一方面也可以提高程式擴充性

在 React 上的實際應用

用一個常見的例子來展現一下 Curry function 的好用之處啦

e.g. 在沒有使用 Curry function 的情況下,每個 onChange 事件我們都要寫一個 function 來儲存他的值,那如果我們有一大堆的 input 需要儲存呢?那我們就需要寫很攏長的程式碼來處理它

export default function App() {
  const [user, setUser] = useState({
    name: '',
    age: '',
    phone: ''
  });

  // First handler
  const handleNameChange = (e) => {
    setUser((prev) => ({
      ...prev,
      name: e.target.value
    }));
  };

  // Second handler
  const handleAgeChange = (e) => {
    setUser((prev) => ({
      ...prev,
      age: e.target.value
    }));
  };

  // Third handler
  const handlePhoneChange = (e) => {
    setUser((prev) => ({
      ...prev,
      phone: e.target.value
    }));
  };

  return (
    <>
      <input value={user.name} onChange={handleNameChange} />
      <input value={user.age} onChange={handleAgeChange} />
      <input value={user.phone} onChange={handlePhoneChange} />
    </>
  );
}

轉成 Curry function 之後,我們可以透過傳遞要修改的 field 作為第一個參數來控制要修改的值是哪個,只需要一個 curry function 就可以避免原本需要寫一堆 function 的情況了

export default function App() {
  const [user, setUser] = useState({
    name: '',
    age: '',
    phone: ''
  });

  const handleInputChange = (field) => {
    return (e) => {
      setUser((prev) => ({
        ...prev,
        [field]: e.target.value
      }));
    };
  };

  return (
    <>
      <input value={user.name} onChange={handleInputChange('name')} />
      <input value={user.age} onChange={handleInputChange('age')} />
      <input value={user.phone} onChange={handleInputChange('phone')} />
    </>
  );
}

結論

雖然可能一開始看 Currying 的時候會有點母煞煞,也不知道為什麼需要他,但當寫久了之後就會發現他的好用之處,非常值得學習的一個技巧。網路上有許多實作 Curry function 的文章,LeetCode 上也有相關的題目,甚至 lodash 也有提供一個方法來達到 currying 喔!有興趣的話可以更深入的研究囉~

相關資源

Reference