Partial & Curry - 函數式編程

什麼是函數式編程

在文章以前,先和你們講一下對於函數式編程(Functional Programming, aka. FP)的理解(下文我會用FP指代函數式編程):ajax

  1. FP須要保證函數都是純淨的,既不依賴外部的狀態變量,也不產生反作用。基於此前提下,那麼純函數的組合與調用,在時間順序上就不會產生依賴,改變多個函數的調用順序也沒必要擔憂產生問題,所以也會消滅許多潛在的bug。
  2. 函數必須有輸入輸出。若是一個函數缺少輸入或輸出,那麼它實際上是一段處理程序procedure而已。
  3. 函數儘量的保持功能的單一,若是一個函數作了多件事情,那麼它理論上應當被拆分爲多個函數。
  4. FP的意義之一就是,在適當的時機使用聲明式編程,抽象了程序流的控制與表現,從理解和維護的角度上會勝於命令式編程。
  5. FP是一種範式,但並不意味這和OOP(面向對象編程)衝突,二者固然是能夠和諧共存的。我的認爲 React 其實就是一個很好的栗子~
  6. Javascript的函數一等公民以及閉包的特性,決定了Javascript的確是適合施展FP的舞臺

理解閉包

閉包對於 Javascript 來講,固然十分重要。然而對於函數式編程來講,這更加是必不可少的,必須掌握的概念,閉包的定義以下:編程

Closure is when a function remembers and accesses variables from outside of its own scope, even when that function is executed in a different scope.api

相信大部分同窗都對閉包有不錯的理解,可是因爲對FP的學習十分重要。接下來我仍是會囉嗦的帶你們過一遍。閉包就是可以讀取其餘函數內部變量的函數數組

簡單示例以下閉包

// Closure demo
function cube(x) {
  let z = 1;
  return function larger(y) {
    return x * y * z++;
  };
}

const makeCube = cube(10);
console.log(makeCube(5)); // 50
console.log(makeCube(5)); // 100
複製代碼

那麼有沒有想過在函數makeCube,或者也能夠說是函數larger是怎麼記住本來不屬於本身做用域的變量x和z的呢?在控制檯查看makeCube.prototype,點開會發現原來是有個[[Scopes]]這個內置屬性裏的Closure(cube)記住了函數larger返回時記住的變量x和z。若是多嵌套幾層函數,也會發現多幾個Closure(name)在[[Scopes]]Scopes[]數組裏,按序查找變量。app

再看下圖測試代碼:ide

function cube(x) {
  return function wrapper(y) {
    let z = 1;
    return function larger() {
      return x * y * z++;
    };
  }
}

const makeCubeY = cube(10);
const makeCube = makeCubeY(5);
const $__VAR1__ = '1. This var is just for test.';
let $__VAR2__ = '2. This var is just for test.';
var $__VAR3__ = '3. This var is just for test.';
console.log(makeCubeY.prototype, makeCube.prototype);
console.log(makeCube()); // 50
console.log(makeCube()); // 100
複製代碼

打印makeCubeY.prototype函數式編程

打印makeCube.prototype函數

經過這幾個實驗能夠從另外一個角度去理解Javascript中閉包,一個閉包是怎麼去查找不是本身做用域的變量呢?makeCube函數分別從[[Scopes]]中的Closure(wrapper)裏找到變量y、z,Closure(cube)裏找到變量x。至於全局let、const聲明的變量放在了Script裏,全局var聲明的變量放在了Global裏。工具

在學習FP前,理解閉包是尤其重要的~ 由於事實上大量的FP工具函數都使用了閉包這個特性。

工具函數

unary

const unary = fn => arg => fn(arg);
複製代碼

一元函數,應用於當只想在某個函數上傳遞一個參數狀況下使用。嘗試考慮如下場景:

console.log(['1', '2', '3'].map(parseInt)); // [1, NaN, NaN]
console.log(['1', '2', '3'].map(unary(parseInt))); // [1, 2, 3]
複製代碼

parseInt(string, radix)接收兩個參數,而map函數中接收的回調函數callback(currentValue[, index[, array]]),第二個參數是index,此時若是parseInt的使用就是錯誤的。固然除了Array.prototype.map,大量內置的數組方法中的回調函數中都不止傳遞一個參數,若是存在適用的只須要第一個參數的場景,unary函數就發揮了它的價值,無需修改函數,優雅簡潔地就接入了。(對於unary函數,fn就是閉包記憶的變量數據)

identity

const identity = v => v;
複製代碼

有同窗會看到identity函數會以爲莫名其妙?是幹嗎的?我第一眼看到也很迷惑?可是考慮如下場景:

console.log([false, 1, 2, 0, '5', true].filter( identity )); // [1, 2, "5", true]
console.log([false, 0].some( identity )); // false
console.log([-2, 1, '3'].every( identity )); // true
複製代碼

怎麼樣?眼前一亮吧,沒想到identity函數原來深藏不露,事實上雖然identity返回了原值,可是在這些函數中Javascript會對返回的值進行類型裝換,變成了布爾值。好比filter函數。咱們能夠看MDN定義filter描述以下(看標粗的那一句)。

filter() calls a provided callback function once for each element in an array, and constructs a new array of all the values for which callback returns a value that coerces to true.

constant

const constant = v => () => v;
複製代碼

一樣,這個函數...乍一看,也不知道具體有什麼用。可是考慮場景以下:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Hello!');
  }, 200);
});
p1.then(() => 'Hi').then(console.log); // Hi!
p1.then(constant('Hi')).then(console.log); // Hi!
p1.then('Hi').then(console.log); // Hello!
複製代碼

因爲Promise.prototype.then只接受函數,若是我僅僅只須要傳遞一個值時,那麼constant便會提供這種便利。固然這個並無什麼功能上的提高,可是的確提升了可閱讀性,也是函數式編程的一個優勢。

spreadArgs & gatherArgs

const spreadArgs = fn => argsArr => fn( ...argsArr );
const gatherArgs = fn => (...argsArr) => fn( argsArr );
複製代碼

嗯這兩個函數見名知義。分別用於展開一個函數的全部參數和收集一個函數全部參數,這兩個函數明顯對立,那麼它們的應用場景又是什麼呢?

spreadArgs函數示例以下:

function cube(x, y, z) {
  return x * y * z;
}

function make(fn, points) {
  return fn(points);
}

console.log(make(cube, [3, 4, 5])); // NaN
console.log(make(spreadArgs(cube), [3, 4, 5])); // 60
複製代碼

gatherArgs函數示例以下:

function combineFirstTwo([v1, v2]) {
  return v1 + v2;
}

console.log([1, 2, 3, 4, 5].reduce(combineFirstTwo)); // Uncaught TypeError
console.log([1, 2, 3, 4, 5].reduce(gatherArgs(combineFirstTwo))); // 15
複製代碼

看完以上代碼,簡單的兩個工具函數,輕易的作到了對一個函數的轉換,從而使其適用於另外一個場景。若是今後應該能夠瞥見函數式編程的一點點魅力,那麼下面的兩個函數將給你們帶來更多的驚喜。

partial & curry

const partial = (fn, ...presetArgs) => (...laterArgs) =>
  fn(...presetArgs, ...laterArgs);
  
const curry = (fn, arity = fn.length, nextCurried) =>
  (nextCurried = prevArgs => nextArg => {
    const args = [...prevArgs, nextArg];

    if (args.length >= arity) {
      return fn(...args);
    } else {
      return nextCurried(args);
    }
  })([]);
複製代碼

相信你們對函數柯里化應該或多或少有點了解。維基百科定義:

在計算機科學中,柯里化(英語:Currying),又譯爲卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。

固然得益於閉包的強大威力,柯里化這個武器得以誕生於Javascript世界。請你們先精讀以上關於partiel、curry函數的代碼。

喝一杯咖啡~

先模擬一個ajax函數以下:

function ajax(url, params, callback) {
  setTimeout(() => {
    callback(
      `GET ${url} \nparams: ${params} \ndata: Hello! ${params} `
    );
  });
}
複製代碼

考慮partial使用場景以下:

const fetchPerson = partial( ajax, "http://some.api/person" );

fetchPerson('Teddy Bear', console.log);
/* GET http://some.api/person params: Teddy Bear data: Hello! Teddy Bear */
複製代碼

考慮curry使用場景以下:

const fetchPerson = curry(ajax)('http://some.api/person');
const fetchUncleBarney = fetchPerson('Uncle Barney');

fetchUncleBarney(console.log);
/* GET http://some.api/person params: Uncle Barney data: Hello! Uncle Barney */
複製代碼

partial和curry函數功能類似,但又有具體的不一樣應用場景,但整體來講curry會比partial更自動化一點。

可是!相信看完示例的同窗又會有一連串問號?爲何好好地參數不一次性傳入,而非要分開屢次傳入這麼麻煩?緣由以下:

  1. 最首要的緣由是partial和curry函數都容許咱們經過參數控制將一個函數的調用在時間和空間上分開了。傳統函數須要一次性將參數湊齊才能調用,可是有時候咱們能夠提早預置部分參數,在最終須要觸發此函數時,纔將剩餘參數傳入。這時候partial和curry就會變得十分有用。
  2. partial和curry的存在讓函數組合(compose)會更加便利。(函數組合也計劃以後和你們分享,這裏就不詳細說了)。
  3. 固然最重要是也提高了可閱讀性!一開始可能不這麼覺得,可是若是你實踐操做感覺以後,也許會改觀。

P.S. 關於函數式編程的實踐,你們可使用lodash/fp模塊進行入門實踐。

一些思考

由於我也是函數式編程的初學者,若有不正確的地方,歡迎你們糾正~

接下來仍是會繼續整理FP的學習資料,學習實踐,連載一些我對於函數式編程的學習與思考,但願和你們一塊兒進步~

謝謝你們(●´∀`●)~

相關文章
相關標籤/搜索