咱們都知道單一職責原則,其實面向對象的SOLID中的S(SRP, Single responsibility principle)。在函數式當中每個函數就是一個單元,一樣應該只作一件事。可是現實世界老是複雜的,當把現實世界映射到編程時,單一的函數就沒有太大的意義。這個時候就須要函數組合和柯里化了。javascript
若是用過jQuery的都曉得啥是鏈式調用,好比$('.post').eq(1).attr('data-test', 'test')
.javascript原生的一些字符串和數組的方法也能寫出鏈式調用的風格:java
'Hello, world!'.split('').reverse().join('') // "!dlrow ,olleH"
首先鏈式調用是基於對象的,上面的一個一個方法split
, reverse
, join
若是脫離的前面的對象"Hello, world!"是玩不起來的。git
而在函數式編程中方法是獨立於數據的,咱們能夠把上面以函數式的方式在寫一遍:github
const split = (tag, xs) => xs.split(tag) const reverse = xs => xs.reverse() const join = (tag, xs) => xs.join(tag) join('',reverse(split('','Hello, world!'))) // "!dlrow ,olleH"
你確定會說,你是在逗我。這比鏈式調用好在哪兒了?這裏仍是依賴於數據的啊,沒有傳遞`'Hello, world!',你這一串一串的函數組合也轉不起來啊。這裏惟一的好處也就是那幾個單獨的方法能夠複用了。莫慌,後面還有那麼多內容我怎麼也會給你優化(忽悠)好的。再進行改造前,咱們先介紹兩個概念,部分應用和柯里化。編程
部分應用是一種處理函數參數的流程,他會接收部分參數,而後返回一個函數接收更少的參數。這個就是部分應用。咱們用bind
來實現一把:segmentfault
const addThreeArg = (x, y, z) => x + y + z; const addTwoArg = addThreeNumber.bind(null, 1) const addOneArg = addThreeNumber.bind(null, 1, 2) addTwoArg(2, 3) // 6 addOneArg(7) // 10
上面利用bind
生成了另外兩個函數,分別接受剩下的參數,這就是部分應用。固然你也能夠經過其餘方式實現。數組
部分應用主要的問題在於,它返回的函數類型沒法直接推斷。正如前面所說,部分應用返回一個函數接收更少的參數,而沒有規定返回的參數具體是多少個。這也就是一些隱式的東西,你須要去查看代碼。才知道返回的函數接收多少個參數。緩存
柯里化定義:你能夠調一個函數,可是不一次將全部參數傳給它。這個函數會返回一個函數去接收下一個參數。函數式編程
const add = x => y => x + y const plusOne = add(1) plusOne(10) // 11
柯里化的函數返回一個只接收一個參數的函數,返回的函數類型能夠預測。函數
固然在實際開發中,有不少的函數都不是柯里化的,咱們可使用一些工具函數來轉化:
const curry = (fn) => { // fn能夠是任何參數的函數 const arity = fn.length; return function $curry(...args) { if (args.length < arity) { return $curry.bind(null, ...args); } return fn.call(null, ...args); }; };
也能夠用開源庫Ramda裏提供的curry方法。
舉個例子
const currySplit = curry((tag, xs) => xs.split(tag)) const split = (tag, xs) => xs.split(tag) // 我如今須要一個函數去split "," const splitComma = currySplit(',') //by curry const splitComma = string => split(',', string)
能夠看到柯里化的函數生成新函數時,和數據徹底沒有關係。對比兩個生成新函數的過程,沒有柯里化的相對而言就有一點囉嗦了。
先給代碼:
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];
其實compose作的事情一共兩件:
可能有同窗對上面的reduceRight不是很熟悉,我給個2元和3元的例子:
const compose = (f, g) => (...args) => f(g(...args)) const compose3 = (f, g, z) => (...args) => f(g(z(...args)))
函數調用是從左到右,數據流也是同樣的從左到右。固然你能夠定義從右到左的,不過從語義上來講就不那麼表意了。
好,如今讓咱們來優化一下最開始的例子:
const split = curry((tag, xs) => xs.split(tag)) const reverse = xs => xs.reverse() const join = curry((tag, xs) => xs.join(tag)) const reverseWords = compose(join(''), reverse, split('')) reverseWords('Hello,world!');
是否是簡潔易於理解多了。這裏的reverseWords
也是咱們以前講過的Pointfree的代碼風格。不依賴數據和外部狀態,就是組合在一塊兒的一個函數。
Pointfree我在上一篇介紹過JS函數式編程 - 概念,也闡述了其優缺點,有興趣的小夥伴能夠看看。
先回顧一下小學知識加法結合律:a+(b+c)=(a+b)+c
。我就不解釋了,大家應該能理解。
回過來看函數組合其實也存在結合律的:
compose(f, compose(g, h)) === compose(compose(f, g), h);
這個對於咱們編程有一個好處,咱們的函數組合能夠隨意組合而且緩存:
const split = curry((tag, xs) => xs.split(tag)) const reverse = xs => xs.reverse() const join = curry((tag, xs) => xs.join(tag)) const getReverseArray = compose(reverse, split('')) const reverseWords = compose(join(''), getReverseArray) reverseWords('Hello,world!');
腦圖補充:
OK,下一篇介紹一下範疇輪,和函子。