前端领域中许多老生常谈的话题背后,其实都蕴含着经典的计算机科学基础知识。在今天,只要你使用 JS 发起过网络请求,那其实你基本就使用过了函数式编程中的 Monad。这是怎么一回事呢?让咱们从回调地狱提及吧……前端
熟悉 JS 的同窗对于回调函数必定不会陌生,这是这门语言中处理异步事件最经常使用的手法。然而正如咱们所熟知的那样,顺序处理多个异步任务的工做流很容易形成回调的嵌套,使得代码难以维护:ios
$.get(a, (b) => {
$.get(b, (c) => {
$.get(c, (d) => {
console.log(d)
})
})
})
复制代码
长久以来这个问题一直困扰着广大 JSer,社区的解决方案也是百花齐放。其中一种已经成为标准的方案叫作 Promise,你能够将异步回调包在 Promise 里,由 Promise.then
方法链式组合异步工做:git
const getB = a =>
new Promise((resolve, reject) => $.get(a, resolve))
const getC = b =>
new Promise((resolve, reject) => $.get(b, resolve))
const getD = c =>
new Promise((resolve, reject) => $.get(c, resolve))
getB(a)
.then(getC)
.then(getD)
.then(console.log)
复制代码
虽然 ES7 里已经有了更简练的 async/await 语法,但 Promise 已经有了很是普遍的应用。好比,网络请求的新标准 fetch 会将返回内容封装为 Promise,目前最流行的 Ajax 库 axios 也是这么作的。至于一度占领 70% 网页的元老基础库 jQuery,早在 1.5 版本中就支持了 Promise。这就意味着,只要你在前端发起过网络请求,你基本上就和 Promise 打过交道。而 Promise 自己,就是一种 Monad。github
不过,各种对 Promise 的介绍多半集中在它的各类状态迁移和 API 使用上,这和 Monad 听起来彷佛彻底八竿子打不着,这两个概念之间有什么联系呢?要讲清楚这个问题,咱们至少得搞懂 Monad 是什么。算法
不少原本有兴趣学习 Haskell 等函数式语言的同窗,均可能被一句名言震慑到打退堂鼓——【Monad 不就是自函子上的幺半群吗,有什么难以理解的】。其实这句话和白学家说的【冬马小三,雪菜碧池】没有什么差异,不过是一句正确的废话而已,听完懂的人仍是懂,不懂的人仍是不懂。因此若是再有人和你这么介绍 Monad,请放心地打死他吧——喂等等,谁说冬三雪碧是正确的了!编程
回归正题,Monad 究竟是什么呢?咱们大可没必要拿出 PLT 或 Haskell 那一套,而是在 JS 的语境里好好考虑一下这个问题:既然 Promise 在 JS 里是一个对象,相似地,你也能够把 Monad 当作一个特殊的对象。axios
既然是对象,那么它的黑魔法也不外乎存在于属性和方法两个地方里了。下面咱们要回答一个相当重要的问题:Monad 有什么特殊的属性和方法,来帮助咱们逃离回调地狱呢?promise
咱们能够用很是简单的伪代码来澄清这个问题。假如咱们有 A B C D 四件事要作,那么基于回调嵌套,你能够写出最简单的函数表达式形如:网络
A(B(C(D)))
复制代码
看到嵌套回调的噩梦了吧?不过,咱们能够抽丝剥茧地简化这个场景。首先,咱们把问题简化到最普通的回调嵌套:异步
A(B)
复制代码
基于添加中间层和控制反转的理念,咱们只需十几行代码,就可以实现一个简单的中间对象 P,把 A 和 B 分开传给这个对象,从而把回调拆分开:
P(A).then(B)
复制代码
如今,A 被咱们包装了一层,P 这个容器就是 Promise 的雏形了!在笔者的博文 从源码看 Promise 概念与实现 中,已经解释了这样将回调嵌套解除的基本机制了,相应的代码实如今此再也不赘述。
可是,这个解决方案只适用于 A B 两个函数之间发生嵌套的场景。只要你尝试去实现过这个版本的 P,你必定会发现,咱们如今没有这种能力:
P(A).then(B).then(C).then(D)
复制代码
也没有这种能力:
P(P(P(A))).then(B)
复制代码
这就是 Monad 大展身手的时候了!咱们首先给出答案: Monad 对象是这个简陋版 P
的强化,它的 then
能支持这种嵌套和链式调用的场景。 固然,正统的 Monad 里这个 API 不是这个名字,但做为参照,咱们能够先看看 Promise/A+ 规范中的一个关键细节:
在每次 Resolve 一个 Promise 时,咱们须要判断两种状况:
thenable
),那么递归 Resolve 这个 Promise。fulfill
或 reject
当前 Promise。直观地说,这个细节可以保证下面两种调用方式彻底等效:
// 1
Promise.resolve(1).then(console.log)
// 1
Promise.resolve(
Promise.resolve(
Promise.resolve(
Promise.resolve(1)
)
)
).then(console.log)
复制代码
这里的嵌套是否似曾相识?这实际上就是披着 Promise 外衣的 Monad 核心能力:对于一个 P 这样装着某种内容的容器,咱们可以递归地把容器一层层拆开,直接取出最里面装着的值。只要实现了这个能力,经过一些技巧,咱们就可以实现下面这个优雅的链式调用 API:
Promise(A).then(B).then(C).then(D)
复制代码
这更带来了额外的好处:无论这里面的 B C D 函数返回的是同步执行的值仍是异步解析的 Promise,咱们都能彻底一致地处理。好比这个同步的加法:
const add = x => x + 1
Promise
.resolve(0)
.then(add)
.then(add)
.then(console.log)
// 2
复制代码
和这个略显拧巴的异步加法:
const add = x =>
new Promise((resolve, reject) => setTimeout(() => resolve(x + 1), 1000))
Promise
.resolve(0)
.then(add)
.then(add)
.then(console.log)
// 2
复制代码
不分同步与异步,它们的调用方式与最终结果彻底一致!
做为一个总结,让咱们看看从回调地狱到 Promise 的过程当中,背后运用了哪些函数式编程中的概念呢?
P(A).then(B)
实现里,它的 P(A)
至关于 Monad 中的 unit
接口,可以把任意值包装到 Monad 容器里。then
背后实际上是 FP 中的 join
概念,在容器里还装着容器的时候,递归地把内层容器拆开,返回最底层装着的值。bind
概念。你能够扁平地串联一堆 .then()
,往里传入各类函数,Promise 可以帮你抹平同步和异步的差别,把这些函数逐个应用到容器里的值上。回归这节中最原始的问题,Monad 是什么呢?只要一个对象具有了下面两个方法,咱们就能够认为它是 Monad 了:
unit
。bind
(这和 JS 里的 bind
彻底是两个概念,请不要混淆了)。正如咱们已经看到的,Promise.resolve()
可以把任意值包装到 Promise 里,而 Promise/A+ 规范里的 Resolve 算法则实际上实现了 bind
。所以,咱们能够认为:Promise 就是一个 Monad。其实这并非一个新奇的结论,在 Github 上早有人从代码角度给出了证实,有兴趣的同窗能够去感觉一下 :-)
做为总结,最后考虑这个问题:咱们是怎么把 Promise 和 Monad 联系起来呢?Promise 消除回调地狱的关键在于:
A(B)
为 P(A).then(B)
的形式。这其实就是 Monad 用来构建容器的 unit
。P(A).then(B).then(C)...
,这实际上是 Monad 里的 bind
。到这里,咱们就可以从 Promise 的功能来理解 Monad 的做用,并用 Monad 的概念来解释 Promise 的设计啦 😉
到了这里,只要你理解了 Promise,那么你应该就已经能够理解 Monad 了。不过,Monad 传说中【自函子上的幺半群】又是怎么一回事呢?其实只要你读到了这里,你就已经见识过自函子和幺半群了(这里的理解未必准确,权当抛砖引玉之用,但愿 dalao 指正)。
函子即所谓的 Functor,是一个能把值装在里面,经过传入函数来变换容器内容的容器:简化的理解里,前文中的 Promise.resolve
就至关于这样的映射,能把任意值装进 Promise 容器里。而自函子则是【能把范畴映射到自己】的 Functor,能够对应于 Promise(A).then()
里仍然返回 Promise 自己。
幺半群即所谓的 Monadic,知足两个条件:单位元与结合律。
单位元是这样的两个条件:
首先,做用到单位元 unit(a)
上的 f
,结果和 f(a)
一致:
const value = 6
const f = x => Promise.resolve(x + 6)
// 下面两个值相等
const left = Promise.resolve(value).then(f)
const right = f(value)
复制代码
其次,做用到非单位元 m
上的 unit
,结果仍是 m
自己:
const value = 6
// 下面两个值相等
const left = Promise.resolve(value)
const right = Promise.resolve(value).then(x=> Promise.resolve(x))
复制代码
至于结合律则是这样的条件:(a • b) • c
等于 a • (b • c)
:
const f = a => Promise.resolve(a * a)
const g = a => Promise.resolve(a - 6)
const m = Promise.resolve(7)
// 下面两个值相等
const left = m.then(f).then(g)
const right = m.then(x => f(x).then(g))
复制代码
上面短短的几行代码,其实就是对【Promise 是 Monad】的一个证实了。到这里,咱们能够发现,平常对接接口编写 Promise 的时候,咱们写的东西均可以先提高到函数式编程的 Monad 层面,而后用抽象代数和范畴论来解释,逼格是否是瞬间提升了呢 XD
上面全部的论证都没有牵扯到 >>==
这样的 Haskell 内容,咱们能够彻底用 JS 这样低门槛的语言来介绍 Monad 是什么,又有什么用。某种程度上笔者认同王垠的观点:函数式编程的门槛被人为地拔高或神话了,明明是实际开发中很是实用且易于理解的东西,却要使用更难以懂的一套概念去形式化地定义和解释,这恐怕并不利于优秀工具和理念的普及。
固然了,为了体现逼格,若是下次再有同窗问你 Promise 是什么,请这么回复:
Promise 不就是自函子上的幺半群吗,有什么难以理解的 🙂
最后插播广告:笔者写这篇文章的动机,是源自实现一个彻底 Promise 化的异步数据转换轮子 Bumpover 时对 Promise 的一些新理解。有兴趣的同窗欢迎关注哦 XD