共计 5399 个字符,预计需要花费 14 分钟才能阅读完成。
上一篇文章中,咱们探讨了罕用的函数式编程案例,一些同学反馈没有讲到底层概念,想理解一下什么是 Monad?基于这个问题,咱们来探索一下。
在函数式编程中,Monad 是一种结构化程序的形象,咱们通过三个局部来了解一下。
- Monad 定义
- Monad 应用场景
- Monad 一句话解释
Monad 定义
依据维基百科的定义,Monad 由以下三个局部组成:
- 一个类型构造函数(M),能够构建出一元类型
M<T>
。 -
一个类型转换函数(return or unit),可能把一个原始值装进 M 中。
- unit(x) :
T -> M T
- unit(x) :
-
一个组合函数 bind,可能把 M 实例中的值取出来,放入一个函数中去执行,最终失去一个新的 M 实例。
M<T>
执行T-> M<U>
生成M<U>
除此之外,它还恪守一些规定:
- 单位元规则,通常由 unit 函数去实现。
- 结合律规定,通常由 bind 函数去实现。
单位元:是汇合里的一种特地的元素,与该汇合里的二元运算无关。当单位元和其余元素联合时,并不会扭转那些元素。
乘法的单位元就是 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 应用场景
通过 Monad 的规定,衍生出了许多应用场景。
-
组装多个函数,实现链式操作。
- 链式操作能够打消中间状态,实现 Pointfree 格调。
- 链式操作也能防止多层函数嵌套问题
fn1(fn2(fn3()))
。 - 如果你用过 rxjs,就能领会到链式操作带来的高兴。
-
解决副作用。
- 包裹异步 IO 等副作用函数,放在最初一步执行。
还记得 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 规定。
- Promise 自身就是一个构造函数。
- Monad 中的 unit,在 Promise 中能够看为:
x => Promise.resolve(x)
- Monad 中的 bind,在 Promise 中能够看为:
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:
- Promise.resolve 如果传入一个 Promise 对象,会期待传入的 Promise 执行,并将执行后果作为外层 Promise 的值。
- Promise.resolve 在解决 thenable 对象时,同样不会间接返回该对象,会将对象中的 then 办法当做一个 Promise 期待后果,并作为外层 Promise 的值。
如果是这两种状况,那就无奈满足 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 解决副作用
接下来,咱们再看一个常见的问题:为什么 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:下文的内容偏数学实践,不感兴趣的同学跳过即可。
Monad 一句话解释
早在 10 多年前,Philip Wadler 就对 Monad 做了一句话的总结。
原文:_A monad is a monoid in the category of endofunctors_。
翻译:Monad 是一个 自函子 领域 上的 幺半群”。
这里标注了 3 个重要的概念:自函子、领域、幺半群,这些都是数学知识,咱们离开了解一下。
- 什么是领域?
任何事物都是对象,大量的对象联合起来就造成了汇合,对象和对象之间存在一个或多个分割,任何一个分割就叫做态射。
一堆对象,以及对象之间的所有态射所形成的一种代数构造,便称之为 领域。
- 什么是函子?
咱们将领域与领域之间的映射称之为 函子。映射是一种非凡的态射,所以函子也是一种态射。
- 什么是自函子?
自函子 就是一个将领域映射到本身的函子。
- 什么是幺半群 Monoid?
幺半群 是一个存在 单位元 的半群。
- 什么是半群?
如果一个汇合,满足结合律,那么就是一个 半群。
- 什么是单位元?
单位元 是汇合里的一种特地的元素,与该汇合里的二元运算无关。当单位元和其余元素联合时,并不会扭转那些元素。
如:任何一个数 + 0 = 这个数自身。那么 0 就是单位元(加法单位元)任何一个数 * 1 = 这个数自身。那么 1 就是单位元(乘法单位元)
Ok,咱们曾经理解了所有应该把握的专业术语,那就简略串解一下这段解释吧:
一个 自函子 领域 上的 幺半群,能够了解为,在一个满足结合律和单位元规则的汇合中,存在一个映射关系,这个映射关系能够把汇合中的元素映射成以后汇合本身的元素。
置信把握了这些理论知识,必定会对 Monad 有一个更加深刻的了解。
总结
本文从 Monad 的维基百科开始,逐渐介绍了 Monad 的内部结构以及实现原理,并通过 Promise 验证了 Monad 在实战中施展的重大作用。
文中蕴含了许多数学定义、函数式编程的实践等常识,大多是参考网络材料和自我教训得出的,如果有谬误的中央,还望大家多多指导 ????
最初,如果你对此有任何想法,欢送留言评论!