JavaScript函數式編程(一) html
在第二篇文章裏,咱們介紹了 Maybe、Either、IO 等幾種常見的 Functor,或許不少看完第二篇文章的人都會有疑惑:node
『這些東西有什麼卵用?』react
事實上,若是隻是爲了學習編寫函數式、反作用小的代碼的話,看完第一篇文章就足夠了。第二篇文章和這裏的第三篇着重於的是一些函數式理論的實踐,是的,這些很難(但並不是不可能)應用到實際的生產中,由於不少輪子都已經造好了而且很好用了。好比如今在前端大規模使用的 Promise 這種異步調用規範,其實就是一種 Monad(等下會講到);如今日趨成熟的 Redux 做爲一種 FLUX 的變種實現,核心理念也是狀態機和函數式編程。程序員
關於 Monad 的介紹和教程在網絡上已經層出不窮了好比這篇文章編程
,不少文章都寫得比我下面的更好,因此我在這裏只是用一種更簡單易懂的方式介紹 Monad,固然簡單易懂帶來的壞處就是不嚴謹,因此見諒/w\網絡
若是你對 Promise 這種規範有了解的話,應該記得 Promise 裏一個很驚豔的特性:架構
1 2 doSomething() 3 4 .then(result => { 5 6 // 你能夠return一個Promise鏈! 7 8 return fetch('url').then(result => parseBody(result)); 9 10 }) 11 12 .then(result => { 13 14 // 這裏的result是上面那個Promise的終值 15 16 }) 17 18 19 20 doSomething() 21 22 .then(result => { 23 24 // 也能夠直接return一個具體的值! 25 26 return 123; 27 28 }) 29 30 .then(result => { 31 32 // result === 123 33 34 })
對於 Promise 的一個回調函數來講,它既能夠直接返回一個值,也能夠返回一個新的 Promise,但對於他們後續的回調函數來講,這兩者都是等價的,這就很巧妙地解決了 nodejs 裏被詬病已久的嵌套地獄。異步
事實上,Promise 就是一種 Monad,是的,可能你每天要寫一大堆 Promise,可直到如今才知道每天用的這個東西居然是個聽起來很高大上的函數式概念。
下面咱們來實際實現一個 Monad,若是你不想看的話,只要記住 『Promise 就是一種 Monad』 這句話而後直接跳過這一章就行了。
咱們來寫一個函數 cat,這個函數的做用和 Linux 命令行下的 cat 同樣,讀取一個文件,而後打出這個文件的內容,這裏 IO 的實現請參考上一篇文章:
1 2 import fs from 'fs'; 3 4 import _ from 'lodash'; 5 6 7 8 var map = _.curry((f, x) => x.map(f)); 9 10 var compose = _.flowRight; 11 12 13 14 var readFile = function(filename) { 15 16 return new IO(_ => fs.readFileSync(filename, 'utf-8')); 17 18 }; 19 20 21 22 var print = function(x) { 23 24 return new IO(_ => { 25 26 console.log(x); 27 28 return x; 29 30 }); 31 32 } 33 34 35 36 var cat = compose(map(print), readFile); 37 38 39 40 cat("file") 41 42 //=> IO(IO("file的內容"))
因爲這裏涉及到兩個 IO:讀取文件和打印,因此最後結果就是咱們獲得了兩層 IO,想要運行它,只能調用:
cat("file").__value().__value(); //=> 讀取文件並打印到控制檯
很尷尬對吧,若是咱們涉及到 100 個 IO 操做,那麼難道要連續寫 100 個 __value() 嗎?
固然不能這樣不優雅,咱們來實現一個 join 方法,它的做用就是剝開一層 Functor,把裏面的東西暴露給咱們:
1 2 var join = x => x.join(); 3 4 IO.prototype.join = function() { 5 6 return this.__value ? IO.of(null) : this.__value(); 7 8 } 9 10 11 12 // 試試看 13 14 var foo = IO.of(IO.of('123')); 15 16 17 18 foo.join(); 19 20 //=> IO('123')
有了 join 方法以後,就稍微優雅那麼一點兒了:
var cat = compose(join, map(print), readFile); cat("file").__value(); //=> 讀取文件並打印到控制檯
join 方法能夠把 Functor 拍平(flatten),咱們通常把具備這種能力的 Functor 稱之爲 Monad。
這裏只是很是簡單地移除了一層 Functor 的包裝,但做爲優雅的程序員,咱們不可能老是在 map 以後手動調用 join 來剝離多餘的包裝,不然代碼會長得像這樣:
var doSomething = compose(join, map(f), join, map(g), join, map(h));
因此咱們須要一個叫 chain 的方法來實現咱們指望的鏈式調用,它會在調用 map 以後自動調用 join 來去除多餘的包裝,這也是 Monad 的一大特性:
1 2 var chain = _.curry((f, functor) => functor.chain(f)); 3 4 IO.prototype.chain = function(f) { 5 6 return this.map(f).join(); 7 8 } 9 10 11 12 // 如今能夠這樣調用了 13 14 var doSomething = compose(chain(f), chain(g), chain(h)); 15 16 17 18 // 固然,也能夠這樣 19 20 someMonad.chain(f).chain(g).chain(h) 21 22 23 24 // 寫成這樣是否是很熟悉呢? 25 26 readFile('file') 27 28 .chain(x => new IO(_ => { 29 30 console.log(x); 31 32 return x; 33 34 })) 35 36 .chain(x => new IO(_ => { 37 38 // 對x作一些事情,而後返回 39 40 }))
哈哈,你可能看出來了,chain 不就相似 Promise 中的 then 嗎?是的,它們行爲上確實是一致的(then 會稍微多一些邏輯,它會記錄嵌套的層數以及區別 Promise 和普通返回值),Promise 也確實是一種函數式的思想。
(我原本想在下面用 Promise 爲例寫一些例子,但估計能看到這裏的人應該都能熟練地寫各類 Promise 鏈了,因此就不寫了0w0)
總之就是,Monad 讓咱們避開了嵌套地獄,能夠輕鬆地進行深度嵌套的函數式編程,好比IO和其它異步任務。
好了,關於函數式編程的一些基礎理論的介紹就到此爲止了,若是想了解更多的話其實建議去學習 Haskell 或者 Lisp 這樣比較正統的函數式語言。下面咱們來回答一個問題:函數式編程在實際應用中到底有啥用咧?
一、React
React 如今已經隨處可見了,要問它爲何流行,可能有人會說它『性能好』、『酷炫』、『第三方組件豐富』、『新穎』等等,但這些都不是最關鍵的,最關鍵是 React 給前端開發帶來了全新的理念:函數式和狀態機。
咱們來看看 React 怎麼寫一個『純組件』吧:
var Text = props => ( <div style={props.style}>{props.text}</div> )
咦這不就是純函數嗎?對於任意的 text 輸入,都會產生惟一的固定輸出,只不過這個輸出是一個 virtual DOM 的元素罷了。配合狀態機,就大大簡化了前端開發的複雜度:
state => virtual DOM => 真實 DOM
在 Redux 中更是能夠把核心邏輯抽象成一個純函數 reducer:
reducer(currentState, action) => newState
關於 React+Redux(或者其它FLUX架構)就不在這裏介紹太多了,有興趣的能夠參考相關的教程。
二、Rxjs
Rxjs 從誕生以來一直都不溫不火,但它函數響應式編程(Functional Reactive Programming,FRP)的理念很是先進,雖然或許對於大部分應用環境來講,外部輸入事件並非太頻繁,並不須要引入一個如此龐大的 FRP 體系,但咱們也能夠了解一下它有哪些優秀的特性。
在 Rxjs 中,全部的外部輸入(用戶輸入、網絡請求等等)都被視做一種 『事件流』:
--- 用戶點擊了按鈕 --> 網絡請求成功 --> 用戶鍵盤輸入 --> 某個定時事件發生 --> ......
舉個最簡單的例子,下面這段代碼會監聽點擊事件,每 2 次點擊事件產生一次事件響應:
1 2 var clicks = Rx.Observable 3 4 .fromEvent(document, 'click') 5 6 .bufferCount(2) 7 8 .subscribe(x => console.log(x)); // 打印出前2次點擊事件
其中 bufferCount 對於事件流的做用是這樣的:
是否是很神奇呢?Rxjs 很是適合遊戲、編輯器這種外部輸入極多的應用,好比有的遊戲可能有『搓大招』這個功能,即監聽用戶一系列連續的鍵盤、鼠標輸入,好比上上下下左右左右BABA,不用事件流的思想的話,實現會很是困難且不優雅,但用 Rxjs 的話,就只是維護一個定長隊列的問題而已:
1 2 var inputs = []; 3 4 var clicks = Rx.Observable 5 6 .fromEvent(document, 'keydown') 7 8 .scan((acc, cur) => { 9 10 acc.push(cur.keyCode); 11 12 var start = acc.length - 12 < 0 ? 0 : acc.length - 12; 13 14 return acc.slice(start); 15 16 }, inputs) 17 18 .filter(x => x.join(',') == [38, 38, 40, 40, 37, 39, 37, 39, 66, 65, 66, 65].join(','))// 上上下下左右左右BABA,這裏用了比較奇技淫巧的數組對比方法 19 20 .subscribe(x => console.log('!!!!!!ACE!!!!!!'));
固然,Rxjs 的做用遠不止於此,但能夠從這個範例裏看出函數響應式編程的一些優良的特性。
既然是完結篇,那咱們來總結一下這三篇文章究竟講了些啥?
第一篇文章裏,介紹了純函數、柯里化、Point Free、聲明式代碼和命令式代碼的區別,你可能忘記得差很少了,但只要記住『函數對於外部狀態的依賴是形成系統複雜性大大提升的主要緣由』以及『讓函數儘量地純淨』就好了。
第二篇文章,或許是最沒有也或許是最有乾貨的一篇,裏面介紹了『容器』的概念和 Maybe、Either、IO 這三個強大的 Functor。是的,大多數人或許都沒有機會在生產環境中本身去實現這樣的玩具級 Functor,但經過了解它們的特性會讓你產生對於函數式編程的意識。
軟件工程上講『沒有銀彈』,函數式編程一樣也不是萬能的,它與爛大街的 OOP 同樣,只是一種編程範式而已。不少實際應用中是很難用函數式去表達的,選擇 OOP 亦或是其它編程範式或許會更簡單。但咱們要注意到函數式編程的核心理念,若是說 OOP 下降複雜度是靠良好的封裝、繼承、多態以及接口定義的話,那麼函數式編程就是經過純函數以及它們的組合、柯里化、Functor 等技術來下降系統複雜度,而 React、Rxjs、Cycle.js 正是這種理念的代言人,這多是大勢所趨,也或許是曇花一現,但不妨礙咱們去多掌握一種編程範式嘛