函数式编程深入理念
范畴与容器:函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、 行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。为 什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种 数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数 运算法则了。
-
函子(Functor):是一个对于函数调用的抽象,我们赋予容器自己去调用 函数的能力。把东西装进一个容器,只留出一个接口 map 给容 器外的函数,map 一个函数时,我们让容器自己来运行这个函数, 这样容器就可以自由地选择何时何地如何操作这个函数,以致于 拥有惰性求值、错误处理、异步调用等等非常牛掰的特性。
-
容器、Functor(函子)
-
函子的标志就是容器具有 map 方法。该方法将容器里 面的每一个值,映射到另一个容器。
-
函数式编程里面的运算,都是通过函子完成, 即运算不直接针对值,而是针对这个值的容器—-函子。函子本 身具有对外接口(map 方法),各种函数就是运算符,通过接口 接入容器,引发容器里面的值的变形
<!-- 容器、Functor(函子) -->
var Container = function(x){
this.__value = x;
}
// 函数式编程一般约定,函子有一个of方法
Container.of = x => new Container(x);
// Container.of(‘abcd’);
// 一般约定,函子的标志就是容器具有map方法。
// 该方法将容器 里面的每一个值,映射到另一个容器。
Container.prototype.map = function(f){
return Container.of(f(this.__value));
}
Container.of(3)
.map(x => x + 1) //=> Container(4)
.map(x => 'Result is ' + x); //=> Container('Result is 4')
- map
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
// Functor是一个函子,它的map方法接受函数f作为 参数,然后返回一个新的函子,里面包含的值是被f处理过的 (f(this.val))。
return new Functor(f(this.val))
}
}
(new Functor(2)).map(function(two) {
return two + 2;
}); // Functor(4)
- of: 函数式编程一般约定,函子有一个 of 方法,用来生成新 的容器。
Functor.of = function(val) {
return new Functor(val);
}
Functor.of(2).map(function(two) {
return two + 2;
}); // Functor(4)
Maybe 函子:函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个 空值(比如 null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Functor(null).map(function(s){
return s.toUpperCase();
}); // TypeError
<!-- Maybe 函子 es6 class -->
class Maybe extends Functor {
map(f){
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
}
Maybe.of(null).map(function(s){
return s.toUpperCase();
}); // Maybe(null)
<!-- Maybe 函子 es5 -->
var Maybe = function(x) {
this.__value = x;
}
Maybe.of = function(x){
return new Maybe(x);
}
Maybe.prototype.map = function(f){
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}
Maybe.prototype.isNothing = function(){
return this.__value === null || this.__value === undefined
}
错误处理、Either
- try/catch/throw 并不是 “纯”的,因为它从外部接管了我们的函数,并且在这个 函数出错时抛弃了它的返回值。
- Promise 是可以调用 catch 来集中处理错误的。
- 事实上 Either 并不只是用来作错误处理的,它表示了逻辑或,范畴学里的 coproduc。
var Left = function(x){
this._value = x;
}
var Right = function(x){
this._value = x;
}
Left.of = function(x){
return new Left(x);
}
Right.of = function(x){
return new Right(x);
}
<!-- 区别处理 -->
// 不会对容器做任何事情,只是很简单地把这个容器拿进来又扔出去。 这个特性意味着,Left 可以用来传递一个错误消息
Left.prototype.map = function(f){
return this;
}
Right.prototype.map = function(f){
return Right.of(f(this._value));
}
<!-- 逻辑处理 -->
var getAge = user => user.age ? Right.of(user.age) : Left.of('Error');
getAge({name: 'stark', age: 22}).map(age => 'Age is ' + age); // Right('Age is 21')
// Left 可以让调用链中任意一环的错误立刻返回到调用链的尾部, 这给我们错误处理带来了很大的方便,再也不用一层又一层的 try/catch
getAge({name: 'stark'}).map(age => 'Age is ' + age); // Left('Error')
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;
<!-- es5 -->
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));
}
<!-- es6 -->
class IO extends Monad {
map(f){
return IO.of(compose(f,this.__value));
}
}
- IO 函子
var fs = require('fs');
var readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
});
};
Monad:Monad 就是一种设计模式,表示将一个运算过程,通过 函数拆解成互相连接的多个步骤。你只要提供下一步运算 所需的函数,整个运算就会自动进行下去。
- Promise 就是一种 Monad
- Monad 让我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如 IO 和其他异步任务
- Monad 函子的作用是,总是返回一个单层的函子。它有一个 flatMap 方法,与 map 的方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个层的容器,不会出现嵌套的情况
Maybe.of(
Maybe.of(
Maybe.of({name: 'sdasd', age: '14'})
)
)
<!-- Monad -->
class Monad extends Functor {
join(){
return this.val;
}
// 如果函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了 flatMap 方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平
flatMap(f) {
return this.map(f).join();
}
}
易调试、热部署、并发
- 函数式编程中的每个符号都是 const 的,于是没有什么函数会有副作用。 谁也不能在运行时修改任何东西,也没有函数可以修改在它的作用域之外修 改什么值给其他函数继续使用。这意味着决定函数执行结果的唯一因素就是 它的返回值,而影响其返回值的唯一因素就是它的参数。
- 函数式编程不需要考虑”死锁”(deadlock),因为它不修改变量,所以根本 不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所 以可以很放心地把工作分摊到多个线程,部署”并发编程”(concurrency)。
- 函数式编程中所有状态就是传给函数的参数,而参数都是储存在栈上的。 这一特性让软件的热部署变得十分简单。只要比较一下正在运行的代码以及 新的代码获得一个 diff,然后用这个 diff 更新现有的代码,新代码的热部署就 完成了
流行的几大函数式编程库
- rxjs: 所有的外部输入(用户输入、网络请求等等)都被视作一 种 『事件流』
- cycle: 一个基于 Rxjs 的框架,它是一个彻彻底底的 FRP 理念的框架, 和 React 一样支持 virtual DOM、JSX 语法,但现在似乎还没有看到大型 的应用经验。
- loadsh: 一个 JavaScript 工具库,它提供了一整套函数式编程 的实用功能,但是没有扩展任何 JavaScript 内置对象
- lazy(惰性求值)
- underscore: 一个具有一致接口、模块化、高性能等特性的 JavaScript 工 具库,是 underscore.js 的 fork,其最初目标也是“一致的跨浏览器行 为。。。,并改善性能”。lodash 采用延迟计算,意味着我们的链式方法在显式或者隐式的 value()调用之前是不会执行的,因此 lodash 可以进行 shortcut(捷 径) fusion(融合)这样的优化,通过合并链式大大降低迭代的次 数,从而大大提升其执行性能。
- ramda.js: 一个非常优秀的 js 工具库
- ramda 里面的提供的函数全部都是 curry 的 意味着函数没有默认参 数可选参数从而减轻认知函数的难度。
- ramda 推崇 pointfree 简单的说是使用简单函数组合实现一个复杂 功能,而不是单独写一个函数操作临时变量。
- ramda 有个非常好用的参数占位符 R._ 大大减轻了函数在 pointfree 过程中参数位置的问题