之前文章已经了解了函数式编程的一些基础,但是还没有演示在函数编程中如何把副作用控制在可控的范围内、异常处理、异常操作等。
函子是一个普通的对象,这个对象里面应该维护一个值并且要对外公布一个map方法,所以可以通过一个类来描述函子
- 容器:包含值和值的变形关系(这个变形关系就是函数)。
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)。
class Container {
constructor(value) {
this._value = value;
}
map(fn) {
return new Container(fn(this._value));
}
}
map方法要去接收一个处理值的函数,当我们调用map方法 的时候,调 用fn去处理这个方法的值,并且把处理的结果给这个新的函子,由新的函子来保存。
创建一个函子的对象。传一个5,调这个对象的map方法来处理这个值加1再求平方。
let r = new Container(5).map((x) => x + 1).map((x) => x * x);
console.log(r);
返回的是一个container对象。所以map方法返回的不是一个值,而是一个新的函子对象,在这个函子对象里再保存新的值,始终不把值对外公布,想要处理值的话只能给这个map对象传递方法 。
每次调用这个函子的时候都要通过new来处理有点不方便,可以把这个new封装一下,因为我看到new就想到面象对象,但我们写的是函数式编程,所以还是避免new这个关键词吧。
class Container {
static of(value) {
return new Container(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return new Container(fn(this._value));
}
}
let r = Container.of(5)
.map((x) => x + 1)
.map((x) => x * x);
console.log(r);
可以在container方法创建一个静态的方法of,of的作用就是给我们返回一个函子对象。
调用的时候因为是一个静态方法所以可以直接通过类名来调用container.of(),
我们想改变值的时候可以不停的.map,有点像链式编程。
总结
- 函数式编程的运算不直接操作值,而是由函子完成。
- 函子就是一个实现了map契约的对象。
- 我们可以把函子想像成一个盒子,这个盒子里封装了一个值。
- 想要处理盒子中的值,我们需要给盒子的 map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理。
- 最终map方法返回一个包含新值的盒子(函子)。
回到上一个例子,如果那个5我们不小心传值变成了null,undefined会发生什么呢?
let r = Container.of(null).map((x) => x.toUpperCase());
console.log(r);
这时发生了一个异常。因为给函子传了一个null,而在map方法里要去执行toUpperCase来处理null。此时这个函数就变的不纯了,因为纯函数始终是相同的输入返回相同的结果,所有这个传入的null就是之前说的副作用。再学一个maybe函子。
MayBe函子
maybe是可能的意思,可能会是空值的情况,就可以用maybe来处理。
- 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理。
- MayBe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)。
class MayBe {
static of(value) {
return new MayBe(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value));
}
isNothing() {
return this._value === null || this._value === undefined;
}
}
let r = MayBe.of(null).map((x) => x.toUpperCase());
console.log(r);
这是maybe函数运行的一个过程,再看看它有什么 问题
let r = MayBe.of("hello world")
.map((x) => x.toUpperCase())
.map((x) => null)
.map((x) => x.split(" "));
console.log(r);
返回结果是还是null,但是在什么 时候出现的null,打印结果是看不出来的,所以maybe函子的问题就是虽然我们可以去处理空值的问题,但是如果我们多次调 用map方法的时候,哪一次出现空值是不太明确的。所以接下去我们再看 Either函子
Either函子
- Either两者中的任何一个,类似于if…else…的处理
- 异常会让函数变的不纯, Either函子可以用来做异常处理
当出现异常的时候,Either函子会出现有效的提示信息,而且可以用来处理异常。
因为是二选一,所以我们要定义两种类型,一个left,一个right。
class Left {
static of(value) {
return new Left(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return this;
}
}
class Right {
static of(value) {
return new Right(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Right.of(fn(this._value));
}
}
Left,和right类和之前相差不大,其实可以去继承Container 这个类的,为了方便理解重新写一遍,看一下right.left有什么 区别。
let r1 = Right.of(12).map((x) => x + 2);
let r2 = Left.of(12).map((x) => x + 2);
console.log(r1);
console.log(r2);
可以看出left是原样输出的,那使用的时候就可以这样了。在可能异常的地方使用left。
function parseJSON(str) {
try {
return Right.of(JSON.parse(str));
} catch (e) {
return Left.of({ error: e.message });
}
}
let r = parseJSON("{name:zs}");
let l = parseJSON('{"name":"zs"}');
console.log(r);
console.log(l);