这是完结篇了,前两篇文章在这里:
JavaScript函数式编程(二)
在第二篇文章里,我们介绍了 Maybe、Either、IO 等几种常见的 Functor,或许很多看完第二篇文章的人都会有疑惑:
『这些东西有什么卵用?』
事实上,如果只是为了学习编写函数式、副作用小的代码的话,看完第一篇文章就足够了。第二篇文章和这里的第三篇着重于的是一些函数式理论的实践,是的,这些很难(但并非不可能)应用到实际的生产中,因为很多轮子都已经造好了并且很好用了。比如现在在前端大规模使用的 Promise 这种异步调用规范,其实就是一种 Monad(等下会讲到);现在日趋成熟的 Redux 作为一种 FLUX 的变种实现,核心理念也是状态机和函数式编程。
一、Monad
关于 Monad 的介绍和教程在网络上已经层出不穷了,很多文章都写得比我下面的更好,所以我在这里只是用一种更简单易懂的方式介绍 Monad,当然简单易懂带来的坏处就是不严谨,所以见谅/w\
如果你对 Promise 这种规范有了解的话,应该记得 Promise 里一个很惊艳的特性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 的实现请参考上一篇文章:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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,想要运行它,只能调用:
1 2 |
cat("file").__value().__value(); //=> 读取文件并打印到控制台 |
很尴尬对吧,如果我们涉及到 100 个 IO 操作,那么难道要连续写 100 个 __value() 吗?
当然不能这样不优雅,我们来实现一个 join 方法,它的作用就是剥开一层 Functor,把里面的东西暴露给我们:
1 2 3 4 5 6 7 8 9 10 |
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 方法之后,就稍微优雅那么一点儿了:
1 2 3 |
var cat = compose(join, map(print), readFile); cat("file").__value(); //=> 读取文件并打印到控制台 |
join 方法可以把 Functor 拍平(flatten),我们一般把具有这种能力的 Functor 称之为 Monad。
这里只是非常简单地移除了一层 Functor 的包装,但作为优雅的程序员,我们不可能总是在 map 之后手动调用 join 来剥离多余的包装,否则代码会长得像这样:
1 |
var doSomething = compose(join, map(f), join, map(g), join, map(h)); |
所以我们需要一个叫 chain 的方法来实现我们期望的链式调用,它会在调用 map 之后自动调用 join 来去除多余的包装,这也是 Monad 的一大特性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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 怎么写一个『纯组件』吧:
1 2 3 |
var Text = props => ( <div style={props.style}>{props.text}</div> ) |
咦这不就是纯函数吗?对于任意的 text 输入,都会产生唯一的固定输出,只不过这个输出是一个 virtual DOM 的元素罢了。配合状态机,就大大简化了前端开发的复杂度:
1 |
state => virtual DOM => 真实 DOM |
在 Redux 中更是可以把核心逻辑抽象成一个纯函数 reducer:
1 |
reducer(currentState, action) => newState |
关于 React+Redux(或者其它FLUX架构)就不在这里介绍太多了,有兴趣的可以参考相关的教程。
2、Rxjs
Rxjs 从诞生以来一直都不温不火,但它函数响应式编程(Functional Reactive Programming,FRP)的理念非常先进,虽然或许对于大部分应用环境来说,外部输入事件并不是太频繁,并不需要引入一个如此庞大的 FRP 体系,但我们也可以了解一下它有哪些优秀的特性。
在 Rxjs 中,所有的外部输入(用户输入、网络请求等等)都被视作一种 『事件流』:
1 |
--- 用户点击了按钮 --> 网络请求成功 --> 用户键盘输入 --> 某个定时事件发生 --> ...... |
举个最简单的例子,下面这段代码会监听点击事件,每 2 次点击事件产生一次事件响应: