函數式編程之柯里化和組合詳解

提到函數式編程,就不得不提柯里化和組合。說實話,在以前的項目開發中,對柯里化和組合的運用不是太多,由於不太清楚應該在哪些狀況下應該使用它們。因此在這篇文章中,咱們將詳細的介紹柯里化和組合的用法以及使用場景。前端

柯里化 Curry

首先說說什麼是柯里化, 簡單來說就是部分應用, 也就是說 只傳遞函數的一部分參數來調用它,讓它返回一個函數去處理剩下的參數面試

參數複用

先來看個例子,建立一個 say 函數,打印出帶有名字,前綴和問候語的一句話。編程

const say = (name, prefix, greeting) => `${greeting}, ${prefix} ${name}!`;

say('Tom', 'Mr', 'Hello'); // "Hello, Mr Tom"
say('James', 'Mr', 'Hello'); // "Hello, Mr James"

在上面的例子中,咱們每一次調用 say 函數都必須傳入完整的三個參數,才能保證正確的運行結果,不然,雖然程序仍是會正常運行,但是未傳入的部分會變成 undefined數組

利用柯里化,咱們能夠固定住其中的部分參數,在調用的時候,這個參數就至關於已經被記住了,不須要再次傳遞,也就是咱們這裏說的參數複用app

const say = prefix => greeting => name => `${greeting}, ${prefix} ${name}!`;
const sayToMr = say('Mr');
const sayToMiss = say('Miss');
const greetMr = sayToMr('Hello');
const greetMiss = sayToMiss('Hi');

greetMr('Tom'); // "Hi, Miss Cindy!"
greetMiss('Cindy'); // "Hello, Mr Tom!"

這時候若是咱們想輸入相同的問候語 Hello, 咱們發現,原來的結構好像不太知足了呃,因而咱們開始調整參數的位置。函數式編程

const say = greeting => prefix => name => `${greeting}, ${prefix} ${name}!`;
const greet = say('Hello');
const greetMeiNv = greet('美女');
const greetShuaiGe = greet('帥哥');

greetShuaiGe('Tom'); // "Hello, 帥哥 Tom!"
greetMeiNv('Cindy'); // "Hello, 美女 Cindy!"
Note: 在使用柯里化的時候,參數的順序很重要,能夠考慮根據 易變化的程度來排列參數,把不容易變化的參數經過柯里化固定起來,將須要處理的參數放到最後一位。

延遲執行

在上面的例子中,經過柯里化,咱們竟然多造出了 3 個函數!簡直就是函數工廠嘛!可是猛地一想,若是若是是 100 個參數呢,難道要寫一百次?有沒有一種方法能夠簡單的幫咱們實現柯里化?函數

我要開始放書上的代碼了。學習

function curry(fn) {
  var outerArgs = Array.prototype.slice.call(arguments, 1);
  
  return function() {
    var innerArgs = Array.prototype.slice.call(arguments),
      finalArgs = outerArgs.concat(innerArgs);
    return fn.apply(null, finalArgs);
  };
}

const say = (name, prefix, greeting) => `${greeting}, ${prefix} ${name}!`;

const curriedSay = curry(say);
curriedSay('Tom', 'Mr', 'Hello'); // "Hello, Mr Tom!"
curry(say,'Tom', 'Mr')('Hello');  // "Hello, Mr Tom!"

簡單解釋一下上面的代碼,首先是獲得除了第一個參數 fn 以外的全部的外部傳參 outerArgs,這裏的 arguments 是一個長得像數組的對象,因此咱們要使用 Array.proptype.slice 將其轉變成真正的數組。 innerArgs 用來獲取調用這個匿名函數時的傳參。最後將外部傳參 outerArgs 和內部傳參 innerArgs 合併,調用 fn。也就是說這時 fn 才被調用。prototype

就比如刷信用卡和儲蓄卡,刷儲蓄卡就是把你的錢立刻轉到別人口袋,刷信用卡是銀行先幫你墊着,到下個月再把錢還給銀行。總之,最後都是花本身的錢。不過這樣有一個好處就是,就是可讓你養成拆分函數,並給函數良好命名的習慣,以及更好的處理和抽象代碼的邏輯。code

使用 Ramda / Lodash 生成柯里化函數

固然,你也能夠可使用 lodash 或者 ramda 這樣的庫來快速柯里化你的函數,這樣能夠省去不少重複造輪子的工做。

下面以使用 lodash 爲例。

const say = (prefix, name, greeting) => `${greeting}, ${prefix} ${name}!`;
const curreiedSay = _.curry(say);

curreiedSay('Mr','Tom','Hello'); // "Hello, Mr Tom!"
curreiedSay('Mr')('Tom','Hello'); // "Hello, Mr Tom!"
curreiedSay('Mr')('Tom')('Hello'); // "Hello, Mr Tom!"
curreiedSay('Tom')(_,'Hello')('Mr'); // "Hello, Mr Tom!"
lodash 和 Ramda 都提供了一系列柯里化函數的包裝方法,感興趣的同窗能夠打開 lodash / ramda 官網,在 console 裏面試一下。

組合 Compose

組合,顧名思義,也就是把多個函數組合起來變成一個函數。

const compose = (fn1, fn2) => args => fn1(fn2(args));

const toUpperCase = value => value.toUpperCase();
const addSuffix = value => `${value} is good!`;

const format = compose(toUpperCase, addSuffix);
format('apple'); // "APPLE IS GOOD!"

上面的例子中,fn2 先執行,而後將返回值做爲 fn1 的參數,因此 compose 裏面的方法是從右向左執行的。就像一條流水線同樣,a 流水線先把汽車組裝好,而後交給 b 流水線進行噴漆,再交給 c 流水線打磨等等,最後獲得一輛嶄新的汽車。

結合柯里化和組合 Curry + Compose

學習完柯里化和組合以後,讓咱們將它們結合起來使用,必定可以碰撞出更強的火花,產生更大的威力。

說寫就寫。

假設有一個數組,咱們指望先對數組進行去重,而後對數組進行求和或求積。

const unique = arr => _.uniq(arr); // 數組去重
const sum = arr => _.reduce(arr, (total, n) => total + n); // 數組元素的累加之和

const multiply = arr => _.reduce(arr, (total, n) => total * n); // 數組元素的乘積

const getTotal = fn => arr => _.flowRight(fn, unique)(arr); // 從右至左, 先去重, 再執行 fn

const arr1 = [1, 2, 3, 4, 4, 5, 5];
const arr2 = [1, 2, 2, 3, 4, 4, 5];

const getSumTotal = getTotal(sum); // 經過柯里化產生一個新的函數
const getMultiplyTotal = getTotal(multiply);   // 經過柯里化產生一個新的函數

getSumTotal(arr1); // 15
getMultiplyTotal(arr2); // 120

如今的前端社區中,函數式編程隨處可見,柯里化和組合也成爲了咱們必須掌握的技能。在項目開發中,能夠不斷的去增強練習。

相關文章
相關標籤/搜索