前言
程序中出现的很多bug往往是由于数据不经意间变成了null
或者undefined
造成的,比如说与服务器通信,第三方库抛出异常来表示某些错误等。因此,编写代码时也往往会写大量防御代码来确保所有抛出的错误都能被适当的捕获。最后导致程序越来越复杂,又不能去扩展和重用。
JavaScript
的异常处理机制通常是使用try-catch
语句实现的:
try {
...
} catch {
...
}
复制代码
显然,使用try-catch
后的代码不能组合或重用,这会严重影响代码的设计。这也与函数式的设计相违背,其中之一的原因就是违反了引用透明性,因为抛出异常会导致函数调用出现另一个出口,不能保证单一的可预测的返回值。
但这不意味着函数式编程不需要抛出异常,这是不可能的。函数式的做法是确保异常应该由一个地方抛出,而不是处处可见。
另一种和抛出异常一样讨人厌的错误就是返回值为null
。虽然遵循了函数的返回值只有一个,但也好不了哪去,因为这也需要去写额外的代码去判断是否为null
。
函数式的解决方案–Functor
说起来与命令式编程差不多,函数式编程应对抛出异常的解决方案就是创建一个安全的容器,来存放可能出抛出异常的代码。try{}
也可以看作是存放危险代码的容器。
所以,Functor
是一个值的容器,而且它实现了map
函数,在遍历每个对象值的时候生成一个新的容器。
将值包裹起来是函数式编程的一个基本设计模式,因为它直接地保证了值不会被人篡改,只能使用特定的方法才能访问容器的值。比如说数组的map
、filter
和reduce
,数组就是值的容器。对于数组来说可以通过map
转换值,返回包含新值的一个新的数组。
创建一个Container
构造函数:
class Container () {
contructor (val) {
this._value = val;
}
static of (val) {
return new Container(val);
}
map (fn) {
return Containe.of(fn(this._value));
}
}
复制代码
Container
只是存储值的容器,而map
函数允许调用任何函数使用当前容器的值。要访问容器的val
,唯一的办法就是通过map
操作去完成,比如map(R.identity)
:
const contName = Container.of('zhang');
contName.map(R.identity); // Container{ _value: 'zhang'}
复制代码
还能映射任何函数到该容器,比如变换该值:
contName.map(R.toUpper); // 'Container{ _value: 'ZHANG'}
复制代码
map
会先打开该容器,应用函数到值,最后把返回的值包裹在一个新的同类型容器中。这也是Functor
的含义所在。
Functor and more
从本质上讲,Functor
只是一个可以将函数应用到它包裹的值上,并将结果再包裹起来的数据结构:
map::(A -> B) -> Container(A) -> Container(B)
复制代码
map
函数接受一个从A->B
的函数,以及一个Container(A)
的Functor
,然后返回包裹着返回值的新Container(B)
。
const plus = R.curry((a, b) => a + b);
const plus1 = plus(1);
复制代码
把另一个参数放到Container
中,再调用map
把plus1
映射到容器中。
const two = Container.of(2); // Container(2)
const three = two.map(plus1); // Container(3)
复制代码
值一直都在容器中,所以可以用map
任意次映射函数来转换值。
two.map(plus(4)).map(plus(5)); // Container(11)
复制代码
Functor其实并不是什么新玩意,大家都一直在使用它,只不过没有意识到而已。比如Array的map、filter:
map::(A -> B) -> Array(A) -> Array(B)
filter::(A -> Boolean) -> Array(A) -> Array(A)
复制代码
map
和filter
都返回同样类型的Functor
,因此可以不断地链接。compose
组合函数也是一样,从一个函数到另一个函数的映射。
因为是属于函数式编程,Functor
有一些重要的约束:
- 无副作用
- 可组合的
遵守这些规则,可以免于抛出异常、篡改元素等。其实际目的只是创建一个上下文,以便可以安全地应用函数操作到值,而又不改变原始的值。这也是map
可以将一个数组转换到另一个数组,而不改变原数组的原因。而Functor
就是这个概念的推广。
Functor的不足
使用Functor
可以安全地应用函数到内部的值,并返回一个新的Functor
。这是一个很好的模式,但如果它遍布在代码中,就会有一些让人不那么顺心的地方。比如说,有两个函数findStuden
和getName
,这两个函数都给值包裹一个上下文Container
:
const findStudent = R.curry((storage, id) => {
return Container.of(find(storage, id));
})
const getName = (student) => {
return Container.of(student.map(R.prop('name')));
}
const studentName = R.compose(getName, findStudent(localStorage('studentList')));
studentName('440***'); // Container(Container('zhang')); ugh!
复制代码
可以看到把这两个函数组合在一起时,返回的值时被包了两层的Container
对象。也许两层也还能接受,但出现三层、四层那就头都大了,在JavaScript
中,通常我们管这样的代码叫callback hell。
Monad
为了解决这种深层嵌套的问题,可以使用Monad
。Monad
也不是一个新的概念,遇到深层嵌套的问题,很自然而然的想到要将其拍平,我们只需要一层Container
。
所以在Functo
r中加上join
函数.
Container.prototype.join = function () {
if (this._value instanceof Container) {
return this._value.join();
}
return this;
}
复制代码
所以在studentName
中,调用join
就能把嵌套的结构给扁平化:
studentName('440***').join(); // Container('zhang')
复制代码
为了避免调用过map
过程中出现嵌套结构,所以一般在map
之后就要调用一次join
。所以将其抽象为chain
方法
Container.prototype.chain = function (fn) {
return this.map(fn).join();
}
复制代码
所以Monad
就是一个含有chain
方法的Functor
。
基础知识已经准备好了,下面进入主题:使用函数式编程来处理异常或错误。
创建Maybe来处理异常
函数式编程通常使用Maybe
和Either
处理:
- 隔离不纯
- 合并判空逻辑
- 避免异常
- 支持函数组合
- 中心化逻辑,用于提供默认值
Maybe合并判空
Maybe Monad
侧重于有效整合null
-判断逻辑.
class MayBe {
constructor(val) {
this._value = val;
}
static of (val) {
return new MayBe(val);
}
isNothing () {
return (this._value === null || this._value === undefined);
}
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value));
}
}
复制代码
MayBe
在应用传入的函数之前会检查null
和undefined
,这是一种对错误的抽象。
MayBe.of('zhang').map(R.toUpper); // MayBe('ZHANG');
复制代码
调用R.toUpper
时,是不需要在意输入值是否为null/undefined
,它已经被MayBe
抽象出来的。如果值为null
,就会得到MayBe(null)
;代码没有在null
或者undefined
值下崩溃,因为已经把值放到一个安全的容器中。
MayBe.of('zhang').map(R.toUpper).map(name => 'Mr.' + name); // MayBe('Mr.ZHANG‘)
复制代码
即使第一个map
返回null
或者undefined
,也是没有问题的。第二个map
仍然会被调用,它也会返回null
。该过程将持续到链条中的最后一个map
函数被调用完
MayBe.of('zhang').map(() => undefined).map(name => 'Mr.' + name); // MayBe(null)
复制代码
const fetchStudent = async (id) => {
const res = await Axios.get(`api/user/${id}`);
return MayBe.of(res).map(R.props('data')
.map(R.props('student')
.map((student) => {
return {
name: student.name,
id: student.id
}
})
}
复制代码
如果其中一个map
出错了,或者一开始的res
为空。我们只能得到一个MayBe(null)
结果,并不知道哪一步出错了,为了了解这一点,需要另一个Monad-Either
。
创建Either Monad来处理错误
Either
与MayBe
略有不同。Ether
代表的是两个逻辑分离的值a和b,它们永远不会同时出现。
- Left(a)-包含一个可能的错误或异常
- Right(b)-包含一个成功的值
class Either {
static left(a) {
return Left.of(a);
}
static right(b) {
return Right.of(b);
}
static of(val) {
return !!val ? Either.right(val) : Either.left(val);
}
}
class Left{
contructor (val) {
this._value = val;
}
static of (val) {
return new Left(val);
}
join () {
if (this._value instanceof Left) {
return this._value.join();
}
return this;
}
chain () {
return this.map(fn).join();
}
map (fn) {
return this
}
}
class Right{
constructor(val) {
this._value = val;
}
static of (val) {
return new Right(val)
}
map (fn) {
return Right.of(fn(this._value))
}
}
复制代码
Either
包含了两个class
,Right、Left
。有趣的是Left
,它的map
不执行给定的函数,而只是返回自身。也就是说在Right
上运行的函数,不能在Left
上运行。举例:
Right.of('test').map(R.toUpper); // Right('TEST')
Left.of('test').map(R.toUpper); // Left('test')
复制代码
使用Either
改写上面的例子,抽象请求api
的操作
const fetchFn (id) = {
return Axios.get(`api/user/${id}`).then(res => {
return Either.of(res)
}).catch(err => {
return Eigher.of(err)
})
}
const fetchStudent = async (id) => {
const res = await fetchFn(id);
return res.map(R.props('data')
.map(R.props('student')
.map((student) => {
return {
name: student.name,
id: student.id
}
})
}
复制代码
如果使用错误的id去调用api:
fetchStudent('xxx');
复制代码
将会返回:
Nothing({
message: 'id not invalid',
errorCode: 404
})
复制代码
使用Eihter
获得了分支失败的确切原因,在错误的情况下fetchStudent
返回了Nothing
,因此后续的map
映射并不会执行,而且Nothing
还保存了错误信息。
Monad
控制了充满副作用的世界,使得开发者可以在可组合的结构中使用它们。而且只需很少量的代码,就可以将Monad
变成可组合的,从而可以享受流畅,富有表现力的错误处理机制。
结尾
如果大家不了解什么是函数式编程,请看我上一篇文章深入浅出JavaScript函数式编程
更多文章请移步楼主github,如果喜欢请点一下star,对作者也是一种鼓励。