函數語言程式設計入門實踐 —— Compose/Pipe

蘇里發表於2019-04-24

前言

今天主要想和大家分享一下函式組合Function Composition的概念以及一些實踐。函式組合應該是函數語言程式設計中最重要的幾個概念之一了~ 所以以下的學習內容十分重要~

工具函式

備註:如果對接下來使用的partial & curry的概念不熟悉,可以回去看我的第一篇有介紹哦~函數語言程式設計入門實踐 —— Partial/Curry。(因為接下來就會用到)

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見名知意,相當於是彼此的映象函式。

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

無需否認,這段示例程式碼的確毫無意義。但是為了達成這一系列操作,我最終執行了這一長串巢狀了四層的函式呼叫:console.log(add10(pow3(double(2))))。(說實話,我的確覺得有點難以閱讀了...),如果更長了,怎麼辦?可能有的同學會給出以下答案:

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

的確,看似好了點,但是也只是將這個冗長的呼叫封裝了一下而已。會不會有更好的做法?

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)竟然變得如此優雅!多個函式的呼叫從程式碼閱讀上,多層巢狀被拍平變成了線性!(當然實際上本質上還是巢狀的函式呼叫的)。

當然,關於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很搭配啊~ 過段時間也想在這個話題上分享一下)

謝謝大家(●´∀`●)~

相關文章