深入淺出 FP 之設計模式:Currying

栏目: ASP.NET · 发布时间: 5年前

内容简介:不只 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() 我們分別有 hitargetname 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) 可以抓到 hitarget 呢 ? 拜 JavaScript 的 Closure 之賜: 內層 function 可以直接 reference 到 funtion 之外的變數,而不必靠 parameter 傳入 ,因此 function (name) 可直接使用 hitarget

第 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 SamHello 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 -10sum => 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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

有趣的二进制

有趣的二进制

[ 日] 爱甲健二 / 周自恒 / 人民邮电出版社 / 2015-10 / 39.00元

《有趣的二进制:软件安全与逆向分析》通过逆向工程,揭开人们熟知的软件背后的机器语言的秘密,并教给读者读懂这些二进制代码的方法。理解了这些方法,技术人员就能有效地Debug,防止软件受到恶意攻击和反编译。本书涵盖的技术包括:汇编与反汇编、调试与反调试、缓冲区溢出攻击与底层安全、钩子与注入、Metasploit 等安全工具。 《有趣的二进制:软件安全与逆向分析》适合对计算机原理、底层或计算机安全......一起来看看 《有趣的二进制》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具