前言
如果对函数式编程不了解,或者有兴趣,可以先阅读前面的文章 函数式编程-入门,会帮助理解本篇文章。
本文旨在理论,有些枯燥乏味,但非常重要,说到函数式编程,那一定逃不过范畴论,那么什么是范畴论
范畴论
- 函数式编程是范畴论的数学分支,范畴论认为世界上的所有概念体系都可以抽象出一个个范畴
- 彼此之间存在某种关系概念、事物、对象等,都构成范畴,说白了,任何事物只要找到他们之间的关系,就能定义
- 用函数表达范畴之间的关系,叫“态射”,同一个范畴所有成员,就是不同状态的“变形”(tranformation),通过“态射”,一个成员可以变形成另一个成员
- 本质上,函数式编程只是范畴论的运算方法,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了
比如:前端团队就是一个范畴,团队中每个成员都有共性(能够胜任前端开发工作),后端团队是另一个范畴,整体研发团队是一个大的范畴,那么我们通过学习、转岗(变形关系),可以由前端转后端
容器(Container)
- 一个范畴代表一个容器,容器内部有两种因素组成:value 和 值的变形关系(函数)
- 容器里面包含__value,如果不包含变形关系,那么它只是个容器
- 函数(变形关系)不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴,这就涉及到到了函子(Functor)
函子(Functor)
- 函子是函数式编程中最重要的数据类型,也是基本的运算单位和功能单位,它首先是一个范畴,也就是说一个容器包含了值和变形关系
- 它的变形关系可以依次作用于每一个值,将当前容器变形到另一个容器
- 本来一个容器不能调用自身的函数,但函子可以,容器只留一个接口 map 可以运行容器内的函数
- 拥有了map方法的容器变成了函子
- 函子是变形函数,它能作用于当前容器的每一个元素,例如 array.map()
- Functor是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力,把东西装进一个容器,只留出一个map接口给容器外的函数,当map一个函数时,就可以让容器自己来运行这个函数,这样容器就可以自由地选择when、where操作这个函数,以致于拥有惰性求值、错误处理、异步调用等特性
//模拟容器、函子
var Container = function(x){
this.__value = x;
}
//函数式编程一般约定,函子有一个 of 方法,用来生成一个新的容器
//为什么要of方法,因为new命令是面向对象编程的标志,不太像函数式编程
Container.of = x => new Container(x);
//Container.of('abcd');
//一般约定,函子的标志就是容器具有map方法,该方法将容器中的每一个值映射到另一个容器
Container.prototype.map = function(f){
return Container.of(f(this.__value));
}
Container.of(3) // Container(3)
.map(x => x + 1)// 执行 x => x + 1, Container(4)
.map(x => 'result is ' + x);// Container(result is 4)
我们学习函数式编程,实际上就是学习函子的各种运算。
由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。
函数式编程就变成了运用不同的函子,解决实际问题
// ES6 写法
class Functor {
constructor(val){
this.val = val;
}
map(f){
return new Functor(f(this.val));
}
}
(new Functor(3).map(x => x + 2));//5
Redux是函数式编程的经典实现
- Redux的stroe本身就是一个容器 Container,它的状态就是 value,通过 action (变形关系),变成一个新的容器,具体过程如下:
- store -> Container
- currentState -> container._value
- action -> f 变形函数
- currentReduer -> map
- middleware -> IO Functor(异步函子)
Pointed函子
- 函子只是实现了map契约接口,Ponited函子的是函子的一个子集
- 生成新函子的时候使用new,因为new为面向对象编程方式,函数式编程约定函子有一个of方法,用来代替new
Functor.of = x => new Functor(x);
Functor.of(2).map(x => x + 1); // 2
Maybe函子
- Maybe用于处理错误和异常,函子接受各种函数、处理容器内部的值时,会遇到一个问题:
- Functor.of(null).map(e => e.toUpperCase());
- 容器内部的值可能是一个空值,而外部函数未必有处理空值的机制,如果传入空值,可能出错
- 修改上面的代码,可解决
Container.prototype.map = function(f){
return this.isNothing() ? Container.of(null) : Container.of(f(this.__value));
};
Container.prototype.isNothing = function(){
return (this.__value === null || this._value === undefined);
};
class Maybe extends Functor {
map(f) {
this.val ? Maybe.of(f(this.val)) : Maybe.of(null)
}
}
Either函子
- 在函数式编程里,我们使用Either函子表达if...else,用于解决 if...else 和 error
- try/catch/throw 并不是"纯"的,因为它从外部接管了我们的函数,并在这个函数出错时抛弃了它的返回值
- Either函子内部有两值: 左值(Left,右值不存在时的默认值) 和 右值(Rihgt,正常情况下使用的值)
class Either extends Functor {
constructor(left, right) {
this.left = left;
this.right = right; //正常情况的值
//如果左跟右都没有 就使用默认值
}
map(f) {
//1==1 ?变形函数(值) :
return this.right ?
Either.of(this.left, f(this.right)) :
Either.of(f(this.left), this.right);
}
}
Either.of = function(left, right){
return new Either(left, right);
}
var addOne = function (x){return x + 1;};
Either.of(5, 6).map(addOne); // Either(5, 7);
Either.of(1, null).map(addOne); // Either(2, null)
AP函子
- 函子里面包含的值__value可能是函数(of(fn)),
- 提供ap方法
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val));
}
}
Ap.of(addTwo).ap(Functor.of(2));
IO函子
- IO 跟前面几个 Functor 不同的地方在于,它的__value 是一个函数
- 它把不纯的操作(比如 IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作的执行,因此,IO 包含的是被包裹的操作的返回值
- IO 其实也算是惰性求值
- IO 负责了调用链,积累了很多不纯的操作,带来了复杂性和不可维护性
import _ from 'lodash';
var compose = _.flowRight;
var IO = function(f) {
this.__value = f;
}
IO.of = x => new IO(_ => x);
IO.prototype.map = function(f){
return new IO(compose(f, this.__value));
};
var fs = require('fs');
var readFile = function(filename){
return new IO(function(){
return fs.readFileSync(filename, 'utf-8');
});
};
readFile('./user.txt').chain(tail).chain(print);
Monad函子
- Monad 是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤,只要提供下一步运算所需的函数,整个运算就会自动进行下去
- Promise 就是一种 Monad
- Monad 避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如 IO 和其它异步操作
class Monad extends Functor {
join() {
return this.val;// 保证返回的是一个单一的函子
}
flatMap(f) {
return this.map(f).join();
}
}
函数编程应用
处理IO或异步等脏操作
- 创建IO函子,把“脏操作”丢给IO函子,this.val 为脏操作函数 a. IO继承Monad,这样,IO就有了flatMap和join方法 b. flatMap: f => this.map(f).join(),join: () => this.val(); c. IO重写map方法,map: f => IO.of(compose(f, this.val))(函数组合)
- 调用flatMap(另一个IO函子) a. this.val = compose(f, this.val)
- 因为val是一个函数,val(),就得到最后的结果,惰性求值
//做一些准备工作 模拟脏操作
localStorage.test = ["a", "b"];
//函数组合代码
const compose = (f, g) => (x => f(g(x)));
/**
* 拥有了map对象的容器变成了函子 此处声明基础函子
* map对象的作用是可以通过变性关系(f函数)作用函子的每一个值
*/
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
/**
* 此处声明Monad函子 它的核心作用是总是返回一个单层的函子
* 如果不明白上面这句话 可以先往下看
*/
class Monad extends Functor {
join() {
return this.val();
}
flatMap(f) {
return this.map(f).join();
}
}
/**
* 现实开发环境不可能所有的操作都非常纯 所以IO函子主要是封装那些不纯的操作
* IO的val是一个函数
* 它的map有些特殊
*/
class IO extends Monad {
map(f) {
//of 很简单 主要是减少new的过程 和函数式编程区分开
return IO.of(compose(f, this.val))
}
of = x => new IO(x)
}
// ================= 请开始你的表演 =====================
// 将三个不纯的函数包裹进IO函子,同时函数延迟执行
const readFile = function (data) {
return new IO(function () {
console.warn('chain start');
return localStorage[data];
});
};
const tail = function (x) {
return new IO(function () {
console.warn(x[x.length - 1] + "【step 1】");
return x[x.length - 1] + "【step 1】";
});
}
const print = function (x) {
return new IO(function () {
console.warn(x + "【step 2】");
return x;
});
}
const result = readFile('test') // IO 函子
.flatMap(tail) // warn: chain start, IO(val为tail方法)
.flatMap(print) // warn: b【step 1】 IO(val为print方法)
.val() // warn: b【step 1】【step 2】
//函数式编程只关心计算 和 数据的映射 并不关注该题的步骤 是旧的范畴到新范畴的映射
//其余的什么curry 懒加载 递归 等等都是衍生
代码过程分析
1.readFile('test') 创建了一个IO函子,val值是read操作函数;
2.IO继承自Monad 所以拥有了flatMap(把它叫chain也行)
3.flatMap接收了tail函数 tail干了啥呢 接受一个x返回一个新的IO 为啥呢?? 因为tail里的操作不纯啊
4.flatMap内部执行了map,这个map是IO的map哦 因为extends的时候重写了
5.IO.of(compose(tail, this.val)) => IO函子
- this.val 为 function g() { console.warn('chain start'); return localStorage[data]; }
- compose(tail, g) 为 x => tail(g(x))); // 生成组合后的函数
- IO.of(x => tail(g(x))) = new IO(x => tail(g(x)))
- 此时,this.val 为 函数 const xx = x => tail(g(x))
6.继续执行join函数 如果不执行join 最下面要一层层的执行val(可以去掉join试验一下) 这也是monad精髓所在
- 执行join后,this.val() >> f(g(x)) >> g(x)执行的函数作为f函数的参数,并执行, 新IO(val为下面的t函数)
- t 函数为 function (x) { console.warn(x[x.length - 1] + "【step 1】"); return x[x.length - 1] + "【step 1】"; }
7.上面实际上返回了 一个新的IO 所以可以链式的继续flatMap 但是万万注意的是这个io的value是组合函数传回来的一个函数 需执行记住啦!!
8.所以join return this.val()会继续返回新的IO方便链式 完成了全部的操作
尾声
函数式编程,就是操作各种函子,来降低系统复杂度,解决我们的实际问题