在第二篇文章裡,我們介紹了Maybe、Either、IO 等幾種常見的Functor,或許很多看完第二篇文章的人都會有疑惑:
『這些東西有什麼卵用? ’
事實上,如果只是為了學習寫函數式、副作用小的程式碼的話,看完第一篇文章就夠了。第二篇文章和這裡的第三篇著重於的是一些函數式理論的實踐,是的,這些很難(但並非不可能)應用到實際的生產中,因為很多輪子都已經造好了並且很好用了。例如現在在前端大規模使用的Promise 這種非同步呼叫規範,其實就是一種Monad(等下會講到);現在日趨成熟的Redux 作為一種Flux 的變種實現,核心理念也是狀態機和函數式程式設計。
關於Monad 的介紹和教程在網路上已經層出不窮了,很多文章都寫得比我下面的更好,所以我在這裡只是用一種更簡單易懂的方式介紹Monad,當然簡單易懂帶來的壞處就是不嚴謹,所以見諒/w\
如果你對Promise 這種規範有了解的話,應該記得Promise 裡一個很驚豔的特性:
doSomething() .then(result => { // 你可以return一个Promise链! return fetch('url').then(result => parseBody(result)); }) .then(result => { // 这里的result是上面那个Promise的终值 }) doSomething() .then(result => { // 也可以直接return一个具体的值! return 123; }) .then(result => { // result === 123 })
對於Promise 的一個回呼函數來說,它既可以直接傳回一個值,也可以傳回一個新的Promise,但對於他們後續的回呼函數來說,這二者都是等價的,這就很巧妙地解決了nodejs 裡被詬病已久的嵌套地獄。
事實上,Promise 就是一種 Monad,是的,可能你天天要寫一大堆 Promise,可直到現在才知道天天用的這個東西竟然是個聽起來很高大上的函數式概念。
下面我們來實際實作一個 Monad,如果你不想看的話,只要記住 『Promise 就是一種 Monad』 這句話然後直接跳過這一章就好了。
我們來寫一個函數cat,這個函數的作用和Linux 命令列下的cat 一樣,讀取一個文件,然後打出這個文件的內容,這裡IO 的實作請參考上一篇文章:
import fs from 'fs'; import _ from 'lodash'; var map = _.curry((f, x) => x.map(f)); var compose = _.flowRight; var readFile = function(filename) { return new IO(_ => fs.readFileSync(filename, 'utf-8')); }; var print = function(x) { return new IO(_ => { console.log(x); return x; }); } var cat = compose(map(print), readFile); cat("file") //=> IO(IO("file的内容"))
由於這裡牽涉到兩個 IO:讀取檔案和列印,所以最後結果就是我們得到了兩層 IO,想要運作它,只能呼叫:
cat("file").__value().__value(); //=> 读取文件并打印到控制台
很尷尬對吧,如果我們牽涉到100 個 IO 操作,那麼難道要連續寫100 個 __value( ) 嗎?
當然不能這樣不優雅,我們來實現一個 join 方法,它的功能就是剝開一層Functor,把裡面的東西暴露給我們:
var join = x => x.join(); IO.prototype.join = function() { return this.__value ? IO.of(null) : this.__value(); } // 试试看 var foo = IO.of(IO.of('123')); foo.join(); //=> 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 的一大功能:
var chain = _.curry((f, functor) => functor.chain(f)); IO.prototype.chain = function(f) { return this.map(f).join(); } // 现在可以这样调用了 var doSomething = compose(chain(f), chain(g), chain(h)); // 当然,也可以这样 someMonad.chain(f).chain(g).chain(h) // 写成这样是不是很熟悉呢? readFile('file') .chain(x => new IO(_ => { console.log(x); return x; })) .chain(x => new IO(_ => { // 对x做一些事情,然后返回 }))
哈哈,你可能看出來了,chain 不就類似Promise 中的then 嗎?是的,它們行為上確實是一致的(then 會稍微多一些邏輯,它會記錄嵌套的層數以及區別Promise 和普通返回值),Promise 也確實是一種函數式的思想。
(我本來想在下面用Promise 為例寫一些例子,但估計能看到這裡的人應該都能熟練地寫各種Promise 鏈了,所以就不寫了0w0)
總之就是,Monad 讓我們避開了嵌套地獄,可以輕鬆地進行深度嵌套的函數式編程,例如IO和其它非同步任務。
好了,關於函數式程式設計的一些基礎理論的介紹就到此為止了,如果想了解更多的話其實建議去學習Haskell 或者Lisp 這樣比較正統的函數式語言。下面我們來回答一個問題:函數式程式設計在實際應用上到底有啥用咧?
1、React
React 現在已經隨處可見了,要問它為什麼流行,可能有人會說它『性能好』、『酷炫』、‘第三方組件豐富’、『新穎』等等,但這些都不是最關鍵的,最關鍵是React 為前端開發帶來了全新的理念:函數式和狀態機。
我們來看看 React 怎麼寫一個『純元件』吧:
var Text = props => ( <p style={props.style}>{props.text}</p> )
咦这不就是纯函数吗?对于任意的 text 输入,都会产生唯一的固定输出,只不过这个输出是一个 virtual DOM 的元素罢了。配合状态机,就大大简化了前端开发的复杂度:
state => virtual DOM => 真实 DOM
在 Redux 中更是可以把核心逻辑抽象成一个纯函数 reducer:
reducer(currentState, action) => newState
关于 React+Redux(或者其它FLUX架构)就不在这里介绍太多了,有兴趣的可以参考相关的教程。
2、Rxjs
Rxjs 从诞生以来一直都不温不火,但它函数响应式编程(Functional Reactive Programming,FRP)的理念非常先进,虽然或许对于大部分应用环境来说,外部输入事件并不是太频繁,并不需要引入一个如此庞大的 FRP 体系,但我们也可以了解一下它有哪些优秀的特性。
在 Rxjs 中,所有的外部输入(用户输入、网络请求等等)都被视作一种 『事件流』:
--- 用户点击了按钮 --> 网络请求成功 --> 用户键盘输入 --> 某个定时事件发生 --> ......
举个最简单的例子,下面这段代码会监听点击事件,每 2 次点击事件产生一次事件响应:
var clicks = Rx.Observable .fromEvent(document, 'click') .bufferCount(2) .subscribe(x => console.log(x)); // 打印出前2次点击事件
其中 bufferCount 对于事件流的作用是这样的:
是不是很神奇呢?Rxjs 非常适合游戏、编辑器这种外部输入极多的应用,比如有的游戏可能有『搓大招』这个功能,即监听用户一系列连续的键盘、鼠标输入,比如上上下下左右左右BABA,不用事件流的思想的话,实现会非常困难且不优雅,但用 Rxjs 的话,就只是维护一个定长队列的问题而已:
var inputs = []; var clicks = Rx.Observable .fromEvent(document, 'keydown') .scan((acc, cur) => { acc.push(cur.keyCode); var start = acc.length - 12 < 0 ? 0 : acc.length - 12; return acc.slice(start); }, inputs) .filter(x => x.join(',') == [38, 38, 40, 40, 37, 39, 37, 39, 66, 65, 66, 65].join(','))// 上上下下左右左右BABA,这里用了比较奇技淫巧的数组对比方法 .subscribe(x => console.log('!!!!!!ACE!!!!!!'));
当然,Rxjs 的作用远不止于此,但可以从这个范例里看出函数响应式编程的一些优良的特性。
3、Cycle.js
Cycle.js 是一个基于 Rxjs 的框架,它是一个彻彻底底的 FRP 理念的框架,和 React 一样支持 virtual DOM、JSX 语法,但现在似乎还没有看到大型的应用经验。
本质的讲,它就是在 Rxjs 的基础上加入了对 virtual DOM、容器和组件的支持,比如下面就是一个简单的『开关』按钮:
import xs from 'xstream'; import {run} from '@cycle/xstream-run'; import {makeDOMDriver} from '@cycle/dom'; import {html} from 'snabbdom-jsx'; function main(sources) { const sinks = { DOM: sources.DOM.select('input').events('click') .map(ev => ev.target.checked) .startWith(false) .map(toggled => <p> <input type="checkbox" /> Toggle me <p>{toggled ? 'ON' : 'off'}</p> </p> ) }; return sinks; } const drivers = { DOM: makeDOMDriver('#app') }; run(main, drivers);
当然,Cycle.js 这种『侵入式』的框架适用性不是太广,因为使用它就意味着应用中必须全部或者大部分都要围绕它的理念设计,这对于大规模应用来说反而是负担。
既然是完结篇,那我们来总结一下这三篇文章究竟讲了些啥?
第一篇文章里,介绍了纯函数、柯里化、Point Free、声明式代码和命令式代码的区别,你可能忘记得差不多了,但只要记住『函数对于外部状态的依赖是造成系统复杂性大大提高的主要原因』以及『让函数尽可能地纯净』就行了。
第二篇文章,或许是最没有也或许是最有干货的一篇,里面介绍了『容器』的概念和 Maybe、Either、IO 这三个强大的 Functor。是的,大多数人或许都没有机会在生产环境中自己去实现这样的玩具级 Functor,但通过了解它们的特性会让你产生对于函数式编程的意识。
软件工程上讲『没有银弹』,函数式编程同样也不是万能的,它与烂大街的 OOP 一样,只是一种编程范式而已。很多实际应用中是很难用函数式去表达的,选择 OOP 亦或是其它编程范式或许会更简单。但我们要注意到函数式编程的核心理念,如果说 OOP 降低复杂度是靠良好的封装、继承、多态以及接口定义的话,那么函数式编程就是通过纯函数以及它们的组合、柯里化、Functor 等技术来降低系统复杂度,而 React、Rxjs、Cycle.js 正是这种理念的代言人,这可能是大势所趋,也或许是昙花一现,但不妨碍我们去多掌握一种编程范式嘛0w0
以上就是JavaScript函数式编程(三)的内容,更多相关内容请关注PHP中文网(www.php.cn)!