《JavaScript函數式編程》4~8章講了函數式編程通用的幾種模式,以及在實際業務場景測試、異步操作的環境下的運用方式。
函數的柯里化與管道模式
在《JavaScript函數式編程》1~3 讀後感中曾經闡述過:在函數式編程思想中,需要把每一個函數功能拆分爲最小單元的功能塊。即:函數的設計要精簡,每個函數實現的功能需要專一,以組合的方式來實現所需要實現的業務邏輯。函數柯里化就是這樣思想的一個實現。它利用閉包的特性,把函數拆分爲最小單元,讓拆分函數能夠共享入參,共同操作入參,實現預期的效果。
來看一個最簡單的實例,我們現在需要實現一個函數composeName
,放入姓
和名
,輸出姓 名
。比如輸入Curry
,Haskell
,輸出Curry Haskell
。這也太簡單了,不費吹灰之力就能寫出來:
const composeName = (firstName, lastName) => `${firstName} ${lastName}`;
// 使用
composeName('Curry', 'Haskell'); // 'Curry Haskell'
而根據函數柯里化的思想,構成名字的firstName
和lastName
是兩個概念的入參,應該拆開來進行輸入的構成。就像這樣:
function curry2(fn) {
return function(first) {
return function(last) {
return fn(first, last);
}
}
}
// 這裏借用 上面實現的composeName
const curryedComposeName = curry2(composeName)
// 使用
curryedComposeName('Curry')('Haskell'); // 'Curry Haskell'
這裏書寫了一個最簡單的二階入參柯里化函數,來用來便於理解什麼是柯里化。curry2
做的事情很簡單,接收一個函數fn
進行包裝,讓這個函數接收兩個參數的過程拆分開來,最後執行函數fn
。乍一看非常脫褲子放屁的事情,其實柯里化做了最重要的事情就是讓函數的每一步執行都可控制,可方便擴充。
在實際業務場景中,用戶的輸入是不可控的。就上面的例子場景,假如存儲進後端的名字必須符合首字母大小,剩餘部分小寫的規範,但是用戶有可能輸入cuRry
。我們需要給用戶輸入加一個報錯機制,當用戶輸入姓或者名不符合規範的時候,返回一個姓或者名違規的報錯,不去執行composeName
的拼接。這段邏輯在composeName
中就需要進行一個邏輯分支的接入。
// isNameError 是用來檢測輸入的英文單詞是否符合首字母大寫,剩餘部分小寫規則的函數,此處省略
const composeName = (firstName, lastName) => {
if (isNameError(firstName)) {
throw new Error('firstName error');
} else if (isNameError(firstName)) {
throw new Error('lastName error');
}
return `${firstName} ${lastName}`;
};
柯里化狀態下,已經分層的函數結構能夠非常方便的擴充邏輯:
function curry2(fn) {
return function(first) {
isNameError(first) ?
throw new Error('firstName error')
:
return function(last) {
isNameError(last) ?
throw new Error('firstName error')
:
return fn(first, last);
}
}
}
柯里化能夠便捷的對函數每一步執行進行控制和包裝。比如可以綁定函數執行的作用域,加入一些容錯機制,再比如在很多大型庫中,對函數執行時間進行一個追蹤優化,都能利用到柯里化進行處理。柯里化使用高階函數的思路,對拆分的每一步函數進行一個包裝,可以形成各種通用的工廠模板,比如trackTimeCurry
,catchErrorCurry
,進行一個通用函數模板的複用。實現代碼的精簡化。
柯里化本質上還是使用了拆分功能快以及組合的模式。這種編程模式,還在函數式編程的另一個思路管道模式模式中予以體現。
管道模式和shell上面的管道指令很相似:ls -al /etc | less
。這段指令中,使用|
,讓ls -al /etc
列出的etc
文件夾下的所有文件列表,以less
的方式展示出來。|
就像一根管道
,讓ls -al /etc
得到的結果,接入到less
指令中。那在管道模式中應該存在compose的函數,讓入參的函數前後銜接,先後處理。
比如我們實現函數countWords
,輸入任意一段文字,去掉所有的空格,得到段落字數的功能。
const explode = str => str.split(/\s+/);
const count = arr => arr.length;
/**
* @param {string} str any string
* @return {number} str length without space
*/
const countWords = compose(explode, count);
// 使用
countWords(anyStr);
compose就像一個管道,通過組合explode
和count
函數,組成了一個countWords
的功能,把countWords
的入參像流水一樣注入到前後的函數運行參數中countWords | explode | count
。而完成了countWords功能之後,explode
和count
還可以作爲單一的功能函數,嵌入到其它需要實現的功能當中去,實現一個函數最大程度的複用。
結語與其它
書本在後續的章節還提出了幾種函數式編程通用的處理模式,函數式編程爲測試帶來的便利,以及使用Promise的鏈式優雅的處理異步。過於細節和社區的通用模式這裏就不再詳細贅述了,這裏剩下的部分總結一下閱讀下來個人感覺的函數式編程的優缺點吧。
-
優點:
- 最小功能單元的函數設計思想讓代碼的維護和複用變得非常方便,在迭代開發的背景下,功能的新增和修改也非常清晰。
- 函數純度(與變量非交互)的概念,讓編寫代碼單元測試變得非常的方便和清晰。
- 函數流的概念能夠保證代碼執行順序的準確(諸如Promise的設計)。
-
缺點:
- 純度的概念,在實際的業務場景很難實踐保持,最多實行在一些歸併到
utils
中的方法函數。 - 最小功能單元設計,在大的項目背景下,很容易分散在各個層級的文件中。在前端還是以
view
爲主核心的情況下,很難進行實行。
- 純度的概念,在實際的業務場景很難實踐保持,最多實行在一些歸併到