内容简介:不只 OOP 有 Design Pattern,事實上 FP 也有不少 Pattern,而 Currying 算是 FP 最基礎、且用的最多的 Pattern。一些正統 FP 語言,如 Haskell、Clojure、F#、ReasonML … 都在語言內直接支援 Currying;JavaScript 雖然沒有直接支援,但因為 JavaScript 有 First-class Function 與 Closure,使得 Currying 在 JavaScript 中使用成為可能。ECMAScript 2
不只 OOP 有 Design Pattern,事實上 FP 也有不少 Pattern,而 Currying 算是 FP 最基礎、且用的最多的 Pattern。
一些正統 FP 語言,如 Haskell、Clojure、F#、ReasonML … 都在語言內直接支援 Currying;JavaScript 雖然沒有直接支援,但因為 JavaScript 有 First-class Function 與 Closure,使得 Currying 在 JavaScript 中使用成為可能。
Version
ECMAScript 2015
Definition
Currying
There is a way to reduce functions of more than one argument to functions of one argument, a way called currying
將一個多 argument 的 function 改寫成多個只有一個 argument 的 function,稱為 currying
Haskell B. Curry
Haskell B. Curry 是位數學家,為了紀念他,Haskell 語言是使用其 名
,而 Curry 概念則是使用其 姓
。
Simple Currying
NonCurrying.js
const greeting = function (hi, target, name) { return hi + ' ' + target + ' ' + name; }; const words = greeting('Hello', 'World', 'Sam'); console.log(words); // Hello World Sam
我們以最簡單的 Hello World 為例,傳統 function 都會有多個 argument,在 greeting()
我們分別有 hi
、 target
與 name
3 個 argument。
根據 Currying 的定義,我們可將一個 function 有 3 個 argument,改寫成 3 個 function 各有 1 個 argument 。
CurryingES5.js
const greeting = function (hi) { return function (target) { return function (name) { return hi + ' ' + target + ' ' + name; } } }; const words = greeting('Hello')('World')('Sam'); console.log(words); // Hello World Sam
第 1 行
const greeting = function (hi) { return function (target) { return function (name) { return hi + ' ' + target + ' ' + name; } } };
由於 Currying 要求每個 function 都只能有 1 個 argument,因此我們必須 return
兩次 function,直到最後一個 return
才會真正回傳值。
為什麼最內層的 function (name)
可以抓到 hi
與 target
呢 ? 拜 JavaScript 的 Closure 之賜: 內層 function 可以直接 reference 到 funtion 之外的變數,而不必靠 parameter 傳入
,因此 function (name)
可直接使用 hi
與 target
。
第 9 行
const words = greeting('Hello')('World')('Sam');
因此 greeting('Hello')
為只有 1 個 argument 的 function,可再傳入 World
。
而 greeting('Hello')('World')
亦為一只有 1 個 argument 的 function,可再傳入 Sam
。
所以 greeting('Hello')('World')('Sam')
其實相當於 greeting('Hello', 'World', 'Sam')
,我們將原本 1 個 function 有 3 個 argument,變成 3 個 function 各有 1 個 argument。
CurryingES6.js
const greeting = hi => target => name => hi + ' ' + target + ' ' + name; const words = greeting('Hello')('World')('Sam'); console.log(words); // Hello World Sam
拜 ECMAScript 2015 之賜,我們有了 Arrow Function,就不必再使用 巢狀 function
的寫法,程式碼更簡潔,可讀性也變高,這也使得 Currying 的實用性更高。
在此亂入一下 F# 的 Currying,與 JavaScript 的 Currying 比較:
CurryingFSharp.fs
let greeting hi target name = hi + " " + target + " " + name let words = greeting "Hello" "World" "Sam" printfn "%s" words // Hello World Sam
JavaScript 的 const
相當於 F# 的 let
。
JavaScript 的 argument 寫在 =
之後,每個參數以 =>
隔開;而 F# 只要在 function 名稱之後以 space 隔開即可。
JavaScript 的 parameter 須以 ()
一一傳入;而 F# 只要在 function 名稱之後以 space 隔開即可。
ECMAScript 2015 有了 Arrow Function 之後,可讀性與簡潔性已經與正統 FP 的 F# 差距不大了。
Q:將傳統 function 改寫成 Currying 不難,但為什麼要這樣寫呢 ?
的確,要改寫成 Currying 並不難,尤其在 ECMAScript 2015 之後,Arrow Function 使得 Currying 寫法非常精簡,也沒有必要再因為 巢狀 function
可讀性不高而排斥 Currying。
但回到一個更基本的問題,為什麼要使用 Currying 這種設計模式呢 ? 請耐心看下去,我將一一說明。
Why Currying ?
Reuse Small Function
拆成眾多的小 function,以利後續 code reuse
const greeting = function (hi, target, name) { return hi + ' ' + target + ' ' + name; };
若一次得傳入 3 個 parameter,我們只有一個 greeting()
function 可用。
const greeting = hi => target => name => hi + ' ' + target + ' ' + name;
若改用 Currying 寫法,我們總共有 3 個 function 可用:
-
greeting()
-
greeting()()
-
greeting()()()
在原本 greeting()
,我們要用 reuse,一次就得提供 3 個 argument,否則就無法重複使用。
但 Currying 過的 greeting()
,變成了 3 個 function,我們可以依實際需求取用 greeting()
,儘管只有 1 個 parameter,也一樣能夠使用 greeting()
。
假設我們有個 function,只有 name
為 argument,回傳為 Hello World Sam
或 Hello World Kevin
,原本 3 個 argument 的 greeting()
就無法被重複使用,但 Currying 過的 greeting()
就能被重複使用。
ReuseSmallFunction.js
const greeting = hi => target => name => hi + ' ' + target + ' ' + name; const helloWorld = greeting('Hello')('World'); const words = helloWorld('Sam'); console.log(words);
第 4 行
const helloWorld = greeting('Hello')('World');
藉由 greeting('Hello')('World')
輕鬆建立新的 helloWorld()
,將來只接受 1 個 argument。
Currying 過的 greeting()
,因為顆粒變小,因此能被 reuse 的機會就更高了。
回想小時候玩樂高積木,哪一種積木最好用 ?
就是顆粒最小的積木最好用,可以說是百搭。Currying 就是把 function 都切成顆粒最小的單一 argument function,因此可藉由 argument 的組合,由一個 function 不斷地組合出新的 function
Higher Order Function
Higher Order Function
可以傳入 function 或傳回 function 的 function,通常會將 重複部分
抽成 higher order function,將 不同部分
以 arrow function 傳入
要支援 Higher Order Function 有個前提,語言必須支援 First-Class Function,這在 JavaScript 很早就支援,所以沒有問題。
BeforeRefactoring.js
const prices = [10, 20, 30]; const calculatePrice1 = prices => { const sum = prices => prices.reduce((acc, elm) => acc + elm); return sum(prices) - 10; }; const calculatePrice2 = prices => { const sum = prices => prices.reduce((acc, elm) => acc + elm); return sum(prices) * 0.9 }; console.log(calculatePrice1(prices)); console.log(calculatePrice2(prices)); // 50 // 54
第 3 行
const calculatePrice1 = prices => { const sum = prices => prices.reduce((acc, elm) => acc + elm); return sum(prices) - 10; };
與
10 行
const calculatePrice2 = prices => { const sum = prices => prices.reduce((acc, elm) => acc + elm); return sum(prices) * 0.9 };
非常類似,最少已經看到以下這部分重複:
const sum = prices => prices.reduce((acc, elm) => acc + elm); return sum(prices)
所以想將這部分抽成 Higher Order Function。
HigherOrderFunction.js
const prices = [10, 20, 30]; const sum = prices => prices.reduce((acc, elm) => acc + elm); const calculate = prices => action => action(sum(prices)); const calculatePrice = calculate(prices); console.log(calculatePrice(sum => sum - 10)); console.log(calculatePrice(sum => sum * 0.9)); // 50 // 54
第 3 行
const sum = prices => prices.reduce((acc, elm) => acc + elm);
將 sum()
先抽成 function。
第 6 行
const calculate = prices => action => action(sum(prices));
將共用部分抽成 calculate()
higher order function,argument 除了原本的 prices
外,還多了 action
,其中 action
正是 不同部分
。
將 sum(prices)
運算結果傳給 action()
。
第 9 行
const calculatePrice = calculate(prices);
由於 calculate()
已經 currying 過,因此 calculate(prices)
回傳為 funciton。
第 10 行
console.log(calculatePrice(sum => sum - 10)); console.log(calculatePrice(sum => sum * 0.9));
將 不同部分
分別以 sum => sum -10
與 sum => sum * 0.9
帶入 calculate()
higher order function,正式計算其值。
若我們不將 calculate()
curry 化,則無法傳回 function,只能回傳值,如此就無法將 不同部分
以 arrow function 傳入
Function Composition
將小 function 組合成功能強大的新 function
ComposeFailed.js
const prices = [10, 20, 30]; const discount = (rate, prices) => prices.map(elm => elm * rate); const sum = prices => prices.reduce((acc, elm) => acc + elm); const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args))); const action = compose(sum, discount(0.8)); console.log(action(prices));
第 3 行
const discount = (rate, prices) => prices.map(elm => elm * rate);
宣告 discount()
,使用傳統 2 個 argument 的寫法。
第 6 行
const sum = prices => prices.reduce((acc, elm) => acc + elm);
宣告 sum()
,使用 reduce()
計算 array 的總和。
第 9 行
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
自己寫一個 compose()
,目的將所有 function 組合成一個新的 function。
實務上可以使用 Ramda.js 的 R.compose()
將 function 組合
12 行
const action = compose(sum, discount(0.8)); console.log(action(prices));
這裡會出問題,因為 discount()
尚未 currying,必須一次提供 2 個 argument,無法單獨只提供 0.8
一個 argument。
在純 FP 語言如 Haskell、F# 會自動 currying,所以不是問題,但 JavaScript 必須手動 currying,或者使用 Ramda.js 的 R.curry()
將原本的 function 加以 currying
CurryingCompose.js
const prices = [10, 20, 30]; const discount = rate => prices => prices.map(elm => elm * rate); const sum = prices => prices.reduce((acc, elm) => acc + elm); const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args))); const action = compose(sum, discount(0.8)); console.log(action(prices)); // 48
第 3 行
const discount = rate => prices => prices.map(elm => elm * rate);
將 discount()
改成 currying 寫法後,就可以使用 compose()
將 sum()
與 discount()
組合成一個新的 action()
。
為了使用 Function Composition,我們會將多個 argument 的 function,currying 成眾多單一 argument 的 function,然後再加以組合
Conclusion
-
JavaScript 不像其他 FP 語言支援自動 currying,但所幸 JavaScript 支援 First-Class Function 與 Closure,因此仍然可以手動將 function 加以 currying,或者使用 Ramda.js 的
R.compose()
- Currying 會將 function 的顆粒拆成更小,更有利於 reuse 與 compose,亦可透過 currying 回傳 Higher Order Function,避免程式碼重複
Sample Code
完整的範例可以在我的 GitHub 上找到
Reference
歐陽繼超, 前端函數式攻城指南
Martin Novak , JavaScript ES6 curry functions with practical examples
Adam Beme , Currying in JavaScript ES6
techsith , JavaScript Currying function (method) explained Tutorial
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 设计模式(2)- 深入浅出设计模式 阅读笔记
- 设计模式(1)- 深入浅出设计模式 阅读笔记
- 深入理解iOS设计模式
- 深入理解iOS设计模式
- Redis 专题:深入解读哨兵模式
- 深入理解[代理模式]原理与技术
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。