上一篇文章中,咱们讨论了经常使用的函数式编程案例,一些同窗反馈没有讲到底层概念,想了解一下什么是 Monad?基于这个问题,咱们来探究一下。javascript
在函数式编程中,Monad 是一种结构化程序的抽象,咱们经过三个部分来理解一下。前端
根据维基百科的定义,Monad 由如下三个部分组成:java
M<T>
。一个类型转换函数(return or unit),可以把一个原始值装进 M 中。ajax
T -> M T
一个组合函数 bind,可以把 M 实例中的值取出来,放入一个函数中去执行,最终获得一个新的 M 实例。编程
M<T>
执行 T-> M<U>
生成 M<U>
除此以外,它还遵照一些规则:promise
单位元:是 集合里的一种特别的元素,与该集合里的 二元运算有关。当单位元和其余元素结合时,并不会改变那些元素。乘法的单位元就是 1,任何数 x 1 = 任何数自己、1 x 任何数 = 任何数自己。安全
加法的单位元就是 0,任何数 + 0 = 任何数自己、0 + 任何数 = 任何数自己。网络
这些定义很抽象,咱们用一段 js 代码来模拟一下。异步
class Monad { value = ""; // 构造函数 constructor(value) { this.value = value; } // unit,把值装入 Monad 构造函数中 unit(value) { this.value = value; } // bind,把值转换成一个新的 Monad bind(fn) { return fn(this.value); } } // 知足 x-> M(x) 格式的函数 function add1(x) { return new Monad(x + 1); } // 知足 x-> M(x) 格式的函数 function square(x) { return new Monad(x * x); } // 接下来,咱们就能进行链式调用了 const a = new Monad(2) .bind(square) .bind(add1); //... console.log(a.value === 5); // true
上述代码就是一个最基本的 Monad,它将程序的多个步骤抽离成线性的流,经过 bind 方法对数据流进行加工处理,最终获得咱们想要的结果。函数式编程
Ok,咱们已经明白了 Monad 的内部结构,接下来,咱们再看一下 Monad 的使用场景。
经过 Monad 的规则,衍生出了许多使用场景。
组装多个函数,实现链式操做。
fn1(fn2(fn3()))
。处理反作用。
还记得 Jquery 时代的 ajax 操做吗?
$.ajax({ type: "get", url: "request1", success: function (response1) { $.ajax({ type: "get", url: "request2", success: function (response2) { $.ajax({ type: "get", url: "request3", success: function (response3) { console.log(response3); // 获得最终结果 }, }); }, }); }, });
上述代码中,咱们经过回调函数,串行执行了 3 个 ajax 操做,但一样也生成了 3 层代码嵌套,这样的代码不只难以阅读,也不利于往后维护。
Promise 的出现,解决了上述问题。
fetch("request1") .then((response1) => { return fetch("request2"); }) .then((response2) => { return fetch("request3"); }) .then((response3) => { console.log(response3); // 获得最终结果 });
咱们经过 Promise,将多个步骤封装到多个 then 方法中去执行,不只消除了多层代码嵌套问题,并且也让代码划分更加天然,大大提升了代码的可维护性。
想想,为何 Promise 能够不断执行 then 方法?
其实,Promise 和 Monad 很相似,它知足了多条 Monad 规则。
x => Promise.resolve(x)
Promise.prototype.then
咱们用代码来验证一下。
// 首先定义 2 个异步处理函数。 // 延迟 1s 而后 加一 function delayAdd1(x) { return new Promise((resolve) => { setTimeout(() => { resolve(x + 1); }); }, 1000); } // 延迟 1s 而后 求平方 function delaySquare(x) { return new Promise((resolve) => { setTimeout(() => { resolve(x * x); }); }, 1000); } /****************************************************************************************/ // 单位元 e 规则,知足:e*a = a*e = a const promiseA = Promise.resolve(2).then(delayAdd1); const promiseB = delayAdd1(2); // promiseA === promiseB,故 promise 知足左单位元。 const promiseC = Promise.resolve(2); const promiseD = a.then(Promise.resolve); // promiseC === promiseD,故 promise 知足右单位元。 // promise 既知足左单位元,又知足右单位元,故 Promise 知足单位元。 // ps:但一些特殊的状况不知足该定义,下文中会讲到 /****************************************************************************************/ // 结合律规则:(a * b)* c = a *(b * c) const promiseE = Promise.resolve(2); const promiseF = promiseE.then(delayAdd1).then(delaySquare); const promiseG = promiseE.then(function (x) { return delayAdd1(x).then(g); }); // promiseF === promiseG,故 Promise 是知足结合律。 // ps:但一些特殊的状况不知足该定义,下文中会讲到
看完上面的代码,不由感受很惊讶,Promise 和 Monad 也太像了吧,不只能够实现链式操做,也知足单位元和结合律,难道 Promise 就是一个 Monad?
其实否则,Promise 并不彻底知足 Monad:
若是是这两种状况,那就没法知足 Monad 规则。
// Promise.resolve 传入一个 Promise 对象 const functionA = function (p) { // 这时 p === 1 return p.then((n) => n * 2); }; const promiseA = Promise.resolve(1); Promise.resolve(promiseA).then(functionA); // RejectedPromise TypeError: p.then is not a function // 因为 Promise.resolve 对传入的 Promise 进行了处理,致使直接运行报错。违背了单位元和结合律。 // Promise.resolve 传入一个 thenable 对象 const functionB = function (p) { // 这时 p === 1 alert(p); return p.then((n) => n * 2); }; const obj = { then(r) { r(1); }, }; const promiseB = Promise.resolve(obj); Promise.resolve(promiseB).then(functionB); // RejectedPromise TypeError: p.then is not a function // 因为 Promise.resolve 对传入的 thenable 进行了处理,致使直接运行报错。违背了单位元和结合律。
看到这里,相信你们对 Promise 也有了一层新的了解,正是借助了 Monad 同样的链式操做,才使 Promise 普遍应用在了前端异步代码中,你是否也和我同样,对 Monad 充满了好感?
接下来,咱们再看一个常见的问题:为何 Monad 适合处理反作用?
ps:这里说的反作用,指的是违反 纯函数原则的操做,咱们应该尽量避免这些操做,或者把这些操做放在最后去执行。
例如:
var fs = require("fs"); // 纯函数,传入 filename,返回 Monad 对象 var readFile = function (filename) { // 反作用函数:读取文件 const readFileFn = () => { return fs.readFileSync(filename, "utf-8"); }; return new Monad(readFileFn); }; // 纯函数,传入 x,返回 Monad 对象 var print = function (x) { // 反作用函数:打印日志 const logFn = () => { console.log(x); return x; }; return new Monad(logFn); }; // 纯函数,传入 x,返回 Monad 对象 var tail = function (x) { // 反作用函数:返回最后一行的数据 const tailFn = () => { return x[x.length - 1]; }; return new Monad(tailFn); }; // 链式操做文件 const monad = readFile("./xxx.txt").bind(tail).bind(print); // 执行到这里,整个操做都是纯的,由于反作用函数一直被包裹在 Monad 里,并无执行 monad.value(); // 执行反作用函数
上面代码中,咱们将反作用函数封装到 Monad 里,以保证纯函数的优良特性,巧妙地化解了反作用存在的安全隐患。
Ok,到这里为止,本文的主要内容就已经分享完了,但在学习 Monad 中的某一天,忽然发现有人用一句话就解释清楚了 Monad,自叹不如,简直太厉害了,咱们一块儿来看一下吧!
Warning:下文的内容偏数学理论,不感兴趣的同窗跳过便可。
早在 10 多年前,Philip Wadler 就对 Monad 作了一句话的总结。
原文:_A monad is a monoid in the category of endofunctors_。翻译:Monad 是一个 自函子 范畴 上的 幺半群” 。
这里标注了 3 个重要的概念:自函子、范畴、幺半群,这些都是数学知识,咱们分开理解一下。
任何事物都是对象,大量的对象结合起来就造成了集合,对象和对象之间存在一个或多个联系,任何一个联系就叫作态射。
一堆对象,以及对象之间的全部态射所构成的一种代数结构,便称之为 范畴。
咱们将范畴与范畴之间的映射称之为 函子。映射是一种特殊的态射,因此函子也是一种态射。
自函子就是一个将范畴映射到自身的函子。
若是一个集合,知足结合律,那么就是一个半群。
单位元是集合里的一种特别的元素,与该集合里的二元运算有关。当单位元和其余元素结合时,并不会改变那些元素。
如: 任何一个数 + 0 = 这个数自己。 那么 0 就是单位元(加法单位元) 任何一个数 * 1 = 这个数自己。那么 1 就是单位元(乘法单位元)
Ok,咱们已经了解了全部应该掌握的专业术语,那就简单串解一下这段解释吧:
一个 自函子 范畴 上的 幺半群 ,能够理解为,在一个知足结合律和单位元规则的集合中,存在一个映射关系,这个映射关系能够把集合中的元素映射成当前集合自身的元素。
相信掌握了这些理论知识,确定会对 Monad 有一个更加深刻的理解。
本文从 Monad 的维基百科开始,逐步介绍了 Monad 的内部结构以及实现原理,并经过 Promise 验证了 Monad 在实战中发挥的重大做用。
文中包含了许多数学定义、函数式编程的理论等知识,大可能是参考网络资料和自我经验得出的,若是有错误的地方,还望你们多多指点 🙏
最后,若是你对此有任何想法,欢迎留言评论!