博客 / 詳情

返回

javascript 柯里化

Currying is not idiomatic in JavaScript

1.你説的"不常用"是什麼意思?

稍微激進的標題可能會導致標題黨誤讀了這篇博客。我要澄清一下:我本人喜歡函數式編程。我的主要觀點是:
  • 一些核心JavaScript功能與currying相沖突。
  • 在我看來currying還有其他選擇(尤其是即將到來的提案)。在partial application(偏函數應用)中使用是非常好的,這沒有什麼衝突。

JavaScript最好的特性之一就是可以適用多種不同的編程風格。因此:如果你Currying編程到沒有任何提到的替代品適合你,那麼你就肆意適用Curring吧。 如果你這樣做,請保持你的代碼風格一致。並且準備好你的代碼與生態系統的其他部分稍有不同。

2.Currying

Currying在函數式編程中是一種流行的技術, 它有助於偏函數應用。

其思想如下:如果您不為函數提供所有參數,則返回一個函數。函數的入參是剩餘參數,其輸出是原始函數的結果。

例如,如果想要數組的所有元素加2,並有一個二進制函數Add(x,y),則可以如下所示:

const arr = [1, 2, 3];

arr.map(add(2)); // [3, 4, 5]`

順便説一下,具有自動currying的函數式編程語言通常具有可以這樣使用的加法操作符。

你有兩個選擇執行add()方法來支持currying。

2.1 簡單 currying

ES6箭頭函數可以很容易地手動編寫柯里函數:

const add = x => y => x + y;

// 等價於(譯者注)
function add(x) { 
    return function(y) {
        return x + y;
    }
}

如下所示你可以這樣執行add()

add(2)(3); // 5

一個函數持有多個需要轉化的嵌套函數,多數具有自動currying的函數式編程語言在語法上add(1, 2) 和add(1)(2)沒有什麼區別。

2.2 重載 currying

一些庫提供了重載的函數。 根據參數數量,這些重載函數中的每一個都會有不同的表現:

  • 如果只使用一個參數,將會柯里化。
  • 如果使用所有的參數,將會是一個普通的函數調用。
  • 如果使用了多個參數(而不是所有參數),則會返回一個綁定到這些參數的函數。

這種形式的curring的實現如下。

  function add(...args) {
      if (args.length < 2) {
          return add.bind(this, ...args);
      }
      const [x, y] = args;
      return x + y;
  }

可以編寫一個工具函數,將普通函數轉換為這種實現。

2.3 小結

柯里化是一種有助於偏函數應用的函數轉化技術。

3. Currying 和一些javascript基礎有衝突

如果想要在JavaScript使用currying編程你有兩選擇:

  • 使用真currying

    • 提倡:容易靜態輸入。
    • 反對:不常用的函數調用語法。
  • 使用重載currying

    • 提倡:優雅的語法。
    • 反對:難以取得的靜態類型。

如果你轉換了內置函數或方法來適應你的currying風格,那麼你將面對很多問題。

注意:並非所有這些缺點同樣重要。

3.1 具名參數

javascript中具名參數可以通過Object字面量模擬(Simulating named parameters ES6中)我喜歡這種綁定參數的方式,他使得代碼更有自我描述性。不是所有currying的javascript庫都支持具名參數。

3.2 不清楚函數調用和非函數調用

通常情況下在函數的後面使用()就可以執行該函數。Currying後你必須清楚的知道函數函數foo中函數的個數。以便知道 foo(123)執行後得到的是一個函數還是函數的結果。

比較下面兩個偏函數應用

  foo(123, ?)
  _ => foo(123, _)

foo()支持多種方式調用。

使用具名參數,在curry中會丟失更多信息

  arr.map(findCity({ latitude: ‎48.137154 })); // curried
  arr.map(_ => findCity({ latitude: ‎48.137154, longitude: _ }))

靜態類型系統(TypeScript,Flow,...)會減少錯誤,但是替代方法仍然更易於閲讀。

3.3 Currying 與默認參數的衝突

如果忽略參數,就會使用函數的默認參數

  function add(x=0, y=0) {
      return x + y;
  }

默認參數可以使的添加參數更加透明並且不會破壞現有調用,這種技術對命名參數特別有用,我經常用來保證通用函數和方法的適用性。

使用參數默認值省略參數意味着使用默認值,使用currying省略參數意味着返回偏函數, 因此兩者有衝突。

3.4 Currying 友好的函數

使用Currying,參數對函數開始是對配置和結束運算都很重要。而Javascript中並不是這樣。

例如parseInt()的簽名是:

  parseInt(s : string, radix : number);

這使得它不可能這樣預先填充基數:

  ['32', '17', '5'].map(parseInt(10)) // doesn’t work

偏函數編程有那些替代方案?

當談到Currying,人們通常想要的是偏函數編程。讓我們看看下面的例子重載Currying並探索替代方案。

  [1, 2, 3].map(add(2))

替代 1: .bind().

  [1, 2, 3].map(add.bind(null, 2))

替代 2: arrow functions.

  [1, 2, 3].map(x => add(2, x))

替代 3: an upcoming proposal for partial application.

  [1, 2, 3].map(add(2, ?))

注:語法固定,提案處於早期階段。

這個建議的好處在於,它可以很好地處理任意的簽名,並且具有很好的描述性語法。

5. 擴展閲讀

  • Currying versus partial application (with JavaScript code)
  • Arrow functions vs. bind()
  • Uncurrying “this” in JavaScript

原文地址


  • 我的blog: neverland.github.io
  • 我的email enix@foxmail.com
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.