Compose & Pipe - 函數式編程

前言

今天主要想和你們分享一下函數組合Function Composition的概念以及一些實踐。函數組合應該是函數式編程中最重要的幾個概念之一了~ 因此如下的學習內容十分重要~git

工具函數

備註:若是對接下來使用的partial & curry的概念不熟悉,能夠回去看個人第一篇有介紹哦~函數式編程入門實踐 —— Partial/Curry。(由於接下來就會用到)github

compose

若是咱們想,對一個值執行一系列操做,並打印出來,考慮如下代碼:編程

import { partial, partialRight } from 'lodash';

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

function pow(x, y) {
  return Math.pow(x, y);
}

function double(x) {
  return x * 2;
}

const add10 = partial(add, 10);
const pow3 = partialRight(pow, 3);

console.log(add10(pow3(double(2)))); // 74
複製代碼

備註:partialRightpartial見名知意,至關因而彼此的鏡像函數。redux

_.partialRight: This method is like _.partial except that partially applied arguments are appended to the arguments it receives.數組

無需否定,這段示例代碼的確毫無心義。可是爲了達成這一系列操做,我最終執行了這一長串嵌套了四層的函數調用:console.log(add10(pow3(double(2))))。(說實話,個人確以爲有點難以閱讀了...),若是更長了,怎麼辦?可能有的同窗會給出如下答案:bash

function mixed(x) {
  return add10(pow3(double(2)))
}
console.log(mixed(2)); // 74
複製代碼

的確,看似好了點,可是也只是將這個冗長的調用封裝了一下而已。會不會有更好的作法?app

function compose(...args) {
  return result => {
    const funcs = [...args];
    while(funcs.length > 0) {
      result = funcs.pop()(result);
    }
    return result;
  };
}

compose(console.log, add10, pow3, double)(2) // 74
複製代碼

歐耶!咱們經過實現了一個簡單的compose函數,而後發現調用的過程compose(console.log, add10, pow3, double)(2)居然變得如此優雅!多個函數的調用從代碼閱讀上,多層嵌套被拍平變成了線性!(固然實際上本質上仍是嵌套的函數調用的)。koa

固然,關於compose的更加函數式的實現以下:函數式編程

function compose(...funcs) {
  return result => [...funcs]
    .reverse()
    .reduce((result, fn) => fn(result), result);
}
複製代碼

那麼有同窗可能也發現了,上述compose以後的函數是隻能夠傳遞一個參數的。這無疑顯得有點蠢?難道不能夠優化實現支持多個參數麼?函數

考慮如下代碼:

function compose(...funcs) {
  return funcs
    .reverse()
    .reduce((fn1, fn2) => (...args) => fn2(fn1(...args)));
}
複製代碼

細心觀察,經過將參數傳遞進行懶執行,從而巧妙的完成了這個任務!示例以下:

function multiply(x, y) {
  return x * y;
}

compose(
  console.log, add10, pow3, multiply
)(2, 5); // 1010
複製代碼

固然上述代碼最終也能夠這麼寫:

compose(
  console.log, 
  partial(add, 10),
  partialRight(pow, 3),
  partial(multiply, 5)
)(2); // 1010
複製代碼

pipe

那麼學習完了composepipe又是什麼呢?首先在剛剛學習compose函數時,可能有同窗會以爲有點小別扭,由於compose從左到右傳遞參數的順序恰好和調用順序相反的!

(固然若是說幫助理解記憶的話,compose傳參的順序就是咱們書寫函數嵌套的順序,在腦海裏把console.log(add10(pow3(double(2))))這一長串裏的括號去了,是否是就是參數的順序了~)

回到話題,pipe是什麼?同窗們有沒有使用過命令行,好比我經常使用的一個命令,將當前工做路徑拷貝到剪切板,隨時ctrl + v就可使用了~

pwd | pbcopy
複製代碼

固然我木有走題!注意以上的管道符 |,這個其實就是pipe,能夠將數據流從左到右傳遞。

考慮示例代碼以下:

function pipe(...args) {
  return result => {
    const funcs = [...args];
    while(funcs.length > 0) {
      result = funcs.shift()(result);
    }
    return result;
  };
}

pipe(
  partial(multiply, 5),
  partialRight(pow, 3),
  partial(add, 10),
  console.log
)(2); // 1010
複製代碼

等等,從左到右?好像和compose恰好相反誒!並且這段代碼好眼熟啊!將pop方法換成了shift方法而已!

那麼實際上等價於:

const reverseArgs = func => (...args) => func(...args.reverse());
const pipe = reverseArgs(compose);
複製代碼

哈咱們避免了重複無心義的代碼!固然不管是compose仍是pipe,本質上咱們都將命令式的代碼轉換成了聲明式的代碼,對一個值的操做能夠理解爲值在函數之間流動

2 --> multiply --> pow --> add --> console.log

多麼優雅呀~

使用遞歸來實現compose!

遞歸版本的compose本質上更接近概念,可是可能也會讓人難以理解。瞭解一下也不錯~

代碼以下:

function compose(...funcs) {
  const [fn1, fn2, ...rest] = funcs.reverse();
  
  function composed(...args) {
    return fn2(fn1(...args));
  };

  if (rest.length === 0) return composed;

  return compose(
    ...rest.reverse(),
    composed
  );
}
複製代碼

redux & koa-compose

redux以及koa其實都是有中間件的思想,組合中間件的compose原理和上述代碼也相差不遠。你們能夠稍微閱讀如下兩個連接的源碼,代碼都很簡短,但都驗證了compose的概念只要在實際開發中運用得當的話,能夠發揮強大的魔力!

小結

因此,學習函數式編程並非讓本身看起來有多麼聰明,也不是爲了迷惑隊友(哈哈),也不是單純爲了學習而學習。它的實際意義在於,給函數調用穿上語義化的衣服,讓實際的應用代碼最終更可讀友好,利於維護~ 固然與此同時,也會訓練本身寫出聲明式的代碼。

(話說React Hooks和FP很搭配啊~ 過段時間也想在這個話題上分享一下)

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

相關文章
相關標籤/搜索