原文:How to use powerful function composition in Javascriptjavascript
聲明:翻譯原文從國外知名博客網站上獲取,並利用業餘時間進行翻譯。若是發現網絡上有其餘譯文,多是由於開始翻譯時沒有發現已存在譯文或是感受原譯文翻譯質量不佳而從新翻譯。不論出於哪類緣由,本譯文不會包含任何抄襲行爲。java
複合函數(Function composition) 是 JavaScript 編程中在面向對象和函數式編程兩者之間至關大的一個差別。程序員
本文會解釋類層級(Class Hierarchy)與複合函數之間的區別,以及在代碼中利用複合函數和函數式編程優勢的示例。es6
在面對對象編程中,定義 Class。編程
例如,你定義了父類 Animal
並擁有一個 move
方法,並繼續建立 Cat
和 Dog
類從 Animal
繼承 move
方法,並添加本身的方法 bark
(狗叫)和 meow
(貓叫)。數組
而後,你又定義了一個 Robot
類擁有方法 chargeBattery
。網絡
如今,若是你想建立一個須要 move
和 chargeBattery
方法的 RoboDog
類,以及一個爲 Dog
加強 bark
的 roboBark
,那麼要怎麼辦呢? 這個類須要從 Dog
和 Robot
同時繼承,但 JavaScript 卻不容許這樣作。架構
爲了解決這個問題以及其餘一些問題,在面向對象編程中再也不推薦使用繼承。 相反,咱們須要爲類定義一個接口(當前不存在於 JavaScript 中),並實例化繼承的類並將它們用做依賴項。app
此外,依賴項應該經過依賴注入來處理,以提升可測試性和靈活性,詳情可參閱: JavaScript Pure Functions for OOP developers。less
RoboDog
類看起來像下面這樣:
import {Animal, Dog} from './animals';
import {Robot} from './robots';
class RoboDog {
constructor(animal, dog, robot) {
this.animal = new animal();
this.dog = new dog();
this.robot = new robot();
}
move() {
return this.animal.move();
}
bark() {
return this.dog.bark();
}
chargeBattery() {
return this.robot.chargeBattery();
}
roboBark() {
return 2 * this.dog.bark();
}
}
const roboDog = new RoboDog(Animal, Dog, Robot);
roboDog.roboBark();
複製代碼
複合函數基於一元柯西化(Monadic Curried)的使用和優選純函數(Pure Function)。
// 一元函數只接受一個參數
const monadic = one => one + 1;
// 這不是一元函數
const notMonadic = (one, two) => one + two;
// 這是柯西、一元、高階函數
const curry = one => two => one + two;
複製代碼
複合函數很是簡單,它使用多個函數,而且每一個函數接收輸入,並將其輸出移交給下一個函數:
const plusOne = a => a + 1;
const plusTwo = a => a + 2;
const composedPlusThree = a => plusTwo(plusOne(a));
composedPlusThree(3); // 6
複製代碼
在函數式編程中,你定義的是表達式而不是語句,函數也只是一個表達式。所以,JavaScript 支持將函數做爲參數,或把返回的函數做爲其輸出的高階函數。
爲了讓其變得容易,你能夠定義高階函數 compose
和 composePipe
:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const composePipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
複製代碼
compose 和 composePipe 在組合函數的順序上有所不一樣:
const plusA = s => s + 'A';
const plusB = s => s + 'B';
const composed1 = s => compose(plusA, plusB)(s);
const composed2 = s => composePipe(plusA, plusB)(s);
composed1(''); // BA
composed2(''); // AB
複製代碼
請注意,在這裏可使用無參數風格代碼(tacit programming 隱式編程 ):
const composedPointFree = compose(plusA, plusB);
console.log(composedPointFree('') === composed1('')); // true
複製代碼
顯然,這是能夠的。由於 compose(plusA,plusB)
是一個高階函數,而 compose
返回一個用於定義新表達式的函數。
若是你使用過Unix,你還能夠將函數組合與 Unix 管道相關聯,該管道的工做原理相同:$ ls -l | grep key | less
。
查看上圖,你能夠看到三個不一樣顏色的編號組,它們經過函數 g
和 f
鏈接。 函數 g
接受參數 Horse
並返回 Horn
。 而後函數 f
接受參數 Horn
並返回 Unicorn
。這兩個函數的組成是一個函數,而這個函數須要一個 Horse
做爲參數,並直接返回一個 Unicorn
做爲輸出。
由於咱們使用的是純函數,而且其始終爲相同輸入返回相同值,因此咱們能夠經過一個簡單的函數替換組合函數,該函數只須要 Horse
並返回 Unicorn
。 這是 Memoization(記憶化) 中使用的原則。
函數式編程並不能很好地優化並行處理。正如你所看到的那樣,它還擁有容許咱們徹底跳過處理的魔力,並經過跳過它們之間的全部內容來返回問題的答案。
複合函數的使用,實際上與前文中的 RoboDog
面向對象編程實例中所作的,看起來類似。可是,使用複合函數,其函數的構成要優雅得多。
你沒有使用類來模擬整個邏輯,而只是定義了表明所需功能的方法。 最終JavaScript 模塊的表達以下:
import {bark} from './dog';
import {compose} from './functional';
const doubleIt = a => 2 * a;
export const roboBark = composePipe(bark, doubleIt);
複製代碼
請注意,上面的代碼沒有引用它不須要的任何內容,這意味着沒有提到 Animal
或 Robot
的功能。 這些並非 RoboDog
獨有的,而咱們只想關注一個全新的獨特代碼。
要使用代碼中的全部功能, 你能夠自由使用 Animal
、Dog
、Cat
、Robot
和 RoboDog
中的功能。
複合函數和對象之間還有另外一個顯着差別。 對象保存內部數據和狀態,它們是有狀態的。 然而,函數式編程中的函數應該是純粹的和無狀態的。
純函數僅由其輸入驅動以提供其輸出,它不會改變(變異)任何其餘數據,也不會觸發任何反作用。 這使得它很是簡單、可預測、易於測試,而且易於遵循通用編程的最佳實踐。這些都是優秀的程序員應該關心的事情。
在函數式編程中,你應該遵循關注點分離,經過使用控制反轉(IOC)的原理和函數式單子(Monads)的方式將任務的執行與其實現分離(IOC 是 AOP 中經常使用概念,Monads 是函數式編程中的概念)。
甚至,若是不使用單子(由於它們的定義會嚇到你:A monad is just a monoid in the category ofendofunctors,自函子範疇上的幺半羣
),你仍然能夠解耦代碼。只需將功能的定義移動到一個能夠集成和提供數據的位置並執行,而後移動到另外一個位置。理想狀況下,能夠在徹底不一樣的模塊級別上執行此操做。
作完這些工做,你能夠經過單元測試和集成測試來覆蓋代碼功能。自此,你就能夠過上快樂的程序員生活。
你有可能正在使用函數做爲可重複的語句序列的盒子,以下所示:
function simonSays(arg) {
let result = arg.trim();
result = `Simon Says: ${result}`;
return result;
}
simonSays(' Jump! '); // Simon Says: Jump!
複製代碼
上面的函數修剪(trim)字符串參數,修飾它而後返回。 示例上的函數雖然只有五行,但實際上,咱們常常看到由幾十行代碼表示的函數。
單一職責原則(Single Responsibility Principle)規定:每一個函數都應對功能的一部分負責。 這是開放的解釋,但咱們能夠很容易地發現,上述功能中「修剪」和「裝飾」作的是兩件事而不是一件事。
讓咱們嘗試使用 JavaScript 中的複合函數:
const trim = a => a.trim();
const add = a => b => a + b;
const simonSays = composePipe(trim, add('Simon Says: '));
simonSays(' Jump! '); // Simon Says: Jump!
複製代碼
使用複合函數,意味着對於程序邏輯的每一步都會有一個可測試且可重用的函數。
測試驅動開發(TDD)要求你,首先爲要實現功能的任何部分編寫測試用例,而後實現邏輯,並所有經過測試用例的測試。這部分是爲了確保程序不會有任何隱藏的、未經測試的邏輯。
經過使用複合函數,你老是能夠用一種暴露邏輯並容許輕鬆測試的方式去編寫代碼。 更多內容能夠查看:Making testable JavaScript code。
經過局部應用的柯西化函數來完善上述的 simonSays
函數。局部應用程序意味着你將提供暴露高階函數中做爲基礎函數的參數:
const add = a => b => a + b;
const partialSimonSays = add('Simon Says:'); // partial application
const simonSays = composePipe(trim, partialSimonSays);
partialSimonSays('Jump!'); // Simon Says: Jump!
simonSays(' Jump! '); // Simon Says: Jump!
複製代碼
這容許你建立更多可重用的代碼。更多內容能夠查看:JavaScript ES6 curry functions with practical examples。
由於咱們一直在使用純函數,因此在組合中插入其餘函數會很是容易。請參閱下面的示例:
// console.log is impure and does not provide any return value
// so we have to improve it
const investigate = a => console.log(a) || a;
const simonSays = composePipe(
investigate,
trim,
investigate,
partialSimonSays,
investigate
);
simonSays(' Jump! ');
// Jump!
// Jump!
// Simon Says: Jump!
複製代碼
若是你正在建立純函數,你將始終可以很是輕鬆地編寫代碼,而無需重構之前的代碼來支持新的用例。
複合函數要求你對編寫代碼的方式進行不一樣層次的思考,這樣將會爲你帶來不少好處。
由複合函數替換類層級容許你專一於,基於功能的思考去開發惟一代碼,而不是基於類的思考。
隱式編程容許你經過利用柯西化和高階函數來簡化代碼。
你須要構建分解後的原子函數,以便爲單一責任原則和測試驅動開發建立更多可重用、可組合的代碼。
純函數和局部應用函數容許經過建立功能強大、簡單、可預測、可輕鬆測試的代碼,來提高你的架構,並輕鬆應用到編程的最佳實踐中。