函數語言程式設計入門實踐(二)
今天主要想和大家分享一下函式組合Function Composition
的概念以及一些實踐。函式組合應該是函數語言程式設計中最重要的幾個概念之一了~ 所以以下的學習內容十分重要~
工具函式
備註:如果對接下來使用的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 複製程式碼
備註:partialRight
和partial
見名知意,相當於是彼此的映象函式。
_.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
那麼學習完了compose
,pipe
又是什麼呢?首先在剛剛學習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很搭配啊~ 過段時間也想在這個話題上分享一下)
謝謝大家(●´∀`●)~