-
- 编程思路的概念[补充]
- 函数式编程思维
-
- 范畴论
- 基本理论
- 基本概念
- 纯函数
- 函数的柯里化
- 函数的组合
- Point Free
- 声明式与命令式代码
- 惰性求值,惰性函数
-
- 函数式编程常用的核心概念
-
- 高阶函数
- 尾调用优化
- 闭包
- 容器 \ Functor
- 错误处理 \ Either \ AP
- IO
- Monad
-
- 当下函数式编程最热的库
-
- RxJS
- cycleJS
- lodashJS
- underscoreJS
- ramdajs
-
- 函数式编程的实际应用场景
-
- 易调试、热部署、并发
- 单元测试
- 总结与补充
-
编程思路的概念[补充]
//1.面向过程的 想到哪写到哪
//2.面向对象的 共有的属性和方法封装到一个类里 封装
//3.面向切面编程 统计一个函数执行的时间
//4.函数式编程 提纯无关于业务的纯函数 函数套函数产生神奇的效果
//5.函数式编程不是用函数来编程 函数套函数让函数更强大 OOP
函数式编程思维
范畴论
- 范畴论是数学分支的一门学科, 世界上所有的体系都可以抽象出一个个范畴;
- 彼此之间存在某种关系,概念,事物,对象等等,都构成范畴. 只要找出他们之间的关系就能
定义
; - 箭头函数表示范畴成员之间的关系,正式名称叫做
态射
,范畴论认为,同一个范畴的所有成员,就是不同状态的”变形”,通过 态射 一个成员可以变形成另一个成员.
所有成员是一个集合; 变形关系是函数
基本理论
- javascript函数称为一等公民,指的是函数与其他数据类型一样.处于平等地位,可以赋值给其他变量,也可以作为参数,转入另一个函数,或者作为别的函数的返回值;
- 不可改变量.在函数编程中, 我们通常理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达式, 这里所说的变量是不可以被修改的. 所有的变量只能被赋值一次初值;
- map & reduce 是最常用的函数编程的方法;
基本概念
纯函数
- 对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态;
var xs=[1,2,3,4,5];
//Array.slice是纯函数,因为他没有副作用,对于固定的输入和输出总是固定的;
xs.slice(0,3);
xs.slice(0,3);
xs.splice(0,3);
xs.splice(0,3);
- 优缺点:
//example 1
import _ from 'lodash';
var sin=_.mimorize(x=>Math.sin(x));
//第一次计算的时候,会稍微慢一点,
var a=sin(7);
//第二次有了缓存,速度极快;
var b=sin(7);
优 : 纯函数不仅可以有效的降低系统的复杂度;还有很多很棒的特性,比如可缓存性;
//example 2
//不纯的
var min=18;
var checeage=age=>age>18;
//纯的
var checkage=age=>age>18;
//在不纯的版本中,checkage不仅取决于age 还有 外部的依赖的变量min;而纯的版本中,checkage 把关键字 18 硬编码在函数内部,扩展性比较差,推荐使用 函数柯里化优雅的解决;
函数的柯里化
- 传递给函数的一部分参数来调用它, 让他返回一个函数去处理剩下的参数.
- 我们一起用柯里化来改上面的那个代码
//example 3
var checkage=age=>age>min;
var checkage18=checkage(18)(20);
//example 4 我们再来看一组函数柯里化的code
//未柯里化之间
function add(x,y){return x+y};
add(1,2)//3
//柯里化之后
function add(x){
return function(y){
return x+y;
}
}
add(2)(1)
//example 5 优缺点
import {curry} from 'lodash';
var match = curry((reg,str)=>str.match(reg));
var filter=curry((f,arr)=>arr.filter(f));
var haveSpace = match(/\s+/g);//清楚空格
filter(haveSpace)(["asdffgh","hello world"])
事实上柯里化 是一种
预加载
函数的方法, 通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的”缓存”,是一种非常高效的编写函数的的方法;
函数的组合
- 纯函数以及如何把柯里化写出的洋葱代码h(g(f(x))),为了解决函数嵌套问题,我们需要用到”函数”组合”;
- 我们一起用柯里化开改他,让多个函数像拼积木一样
//example 6
const compose=(a,g)=>(x=>a(g(x)));
var first = arr => console.log(arr[0]);
var reverse=arr=>arr.sort();
var last = compose(first, reverse);
last([6,2,3,4,5,1])
Point Free
- 把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量.(一般这种思路用于封装之间的API)
//example 7
//一般我们是这样使用api的,
const f=str=>str.toUpperCase().split(' ');
//Point Free 改写之后呢.
var toUpperCase=word=>word.toUpperCase();
var split=x=>(str=>str.split(x));
var f=compose(split(' '),toUpperCase);//example 6 的compose
f("abcd efgh");
这种风格能够帮我们减少不必要的命名,让代码保持简洁和通用
声明式与命令式代码
//example 8
//先看段代码
//命名式:需要我们一条一条的指令去执行代码
let ceo=[];
for(var i=0;i>arr.length;i++){
ceo.push(arr[i])
}
//声明式:通过表达式的方式来声明我们想干的事,而不是一步一步指示;
let CEO=arr.map(c=>c)
优 : 这种声明式的代码,对于无副作用的纯函数,可以不考虑函数内部的实现,专注编写业务代码;相反对于不纯的函数,代码会出现副作用或者依赖外部系统的环境,这对于程序员来说式极大的负担.
惰性求值,惰性函数
- 定义 由于每个函数都有可能改动或者依赖于其外部状态,因此必须顺序执行;
函数式编程常用的核心概念
高阶函数
- 函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象
//example 9
//命令式
var add=function(a,b){
return a+b;
};
function math(f,array){
return f(array[0],array[1]);
}
math(add,[1,2]);//3
尾调用优化
- 函数内部最后的动作是函数调用,该调用的返回值, 直接返回给函数;函数调用自身,称为
递归
;如果尾调用自身,就称为尾递归
。递归需要保存大量的调用记录,很容易发生栈溢出 错误,如果调用了尾递归优化,将递归变成循环,如果尾调用自身,就称为尾递归;
//example 10
// 不是尾递归,无法优化
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
} //ES6强制使用尾递归,一定要调用自身
闭包
//example 11
function makePowerFn(power) {
function powerFn(base) {
return Math.pow(base, power); //Math.pow(x,y)返回 x 的 y 次幂的值。
}
return powerFn;
}
var square = makePowerFn(2);
square(3)//3
虽然外层的makePowerFn函数执行完毕了,栈上的帧也被释放了,但是堆上的作用域并没有被释放,因此powerFn依旧可以被
容器 \ Functor
- 我们可以发”范畴”想象成一个容器,里面包含两样东西,值(value),值的变形关系,也就是函数
- 范畴论使用函数,表达范畴之间的关系.
- 伴随着范畴论的发展,就发展出一整套函数的运算方法.这套方法起初只是用于数学元算,后来有人将它在计算机上实现了. 就变成了今天的”函数式编程”.
- 函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴,这就涉及到了函子(Functor)
-
函子
是函数式编程里面最重要的数据类型,也是基本的运算单位的功能单位.它首先是一种范畴,也就是说,是一种容器,包含了值和变形关系; 比较特殊的是,它的变形关系可以依次作用于每一个值,将当前的容器变形成一个容器.
//example 12
//先设定一个容器
var Container=function (x) {
this.__value=x;
}
//按照约定, 函子会有一个of方法
Container.of = x => new Container(x);
// 一般约定,函子的标志就是容器具有map方法. 该方法将容器里面的值映射到另一个容器
Container.prototype.map=function (f) {
return Container.of(f(this.__value))
}
Container.of(3).map(x=>x+1).map(x=>console.log('Result is ' + x));
example 12中,Functor是一个函子,它的map方法接受函数f作为 参数,然后返回一个新的函子,里面包含的值是被f处理过的 (f(this.val))。 一般约定,函子的标志就是容器具有
map
方法。该方法将容器里 面的每一个值,映射到另一个容器。 上面的例子说明,函数式编程里面的运算,都是通过函子完成, 即运算不直接针对值,而是针对这个值的容器—-函子。函子本 身具有对外接口(map方法),各种函数就是运算符,通过接口 接入容器,引发容器里面的值的变形。 因此,学习函数式编程,实际上就是学习函子的各种运算。由 于可以把运算方法封装在函子里面,所以又衍生出各种不同类 型的函子,有多少种运算,就有多少种函子。函数式编程就变 成了运用不同的函子,解决实际问题
.
//example 13
class Functor{
constructor(val){
this.val=val;
}
map(f){
return new Functor(f(this.val))
}
}
(new Functor(2)).map(two=>two+2)
你可能注意到了,上面生成新的函子的时候,用了 new命令。这实在太不像函数式编程了,因为new命令是 面向对象编程的标志。 函数式编程一般约定,函子有一个
of
方法,用来生成新 的容器
.
//example 14
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);
}
Maybe(null)
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个 空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错 上面的 example 14 使用三目运算来判断值, 产生新的容器称之为
Maybe 函子
错误处理 \ Either \ AP
- 我们的容器能做的事太少了,try/catch/throw 并不是”纯”的,因为它从外部接管了我们的函数,并且在这个函数出错时抛弃了它的返回值;
-
Promise
是可以调用catch采集处理错误的 - 事实上Either [译:两者之一] 并不是用来做错误处理的,它表示了逻辑”或”
class Functor{
constructor(val){
this.val=val;
}
map(f){
return new Functor(f(this.val))
}
}
class Either extends Functor {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
map(f) {
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.of(1,null).map(addOne);
// Either.of({address: "xxxx"}, currentUser.address).map(updataField);//真实场景
IO
Monad
//做一些准备工作
localStorage.test = ["a", "b"];
//函数组合代码
const compose = (f, g) => (x => f(g(x)));
/**
* 基础函子
* 1,拥有map对象的容器变成函子
* 2,map对象的作用是可以通过变形关系 f 函数 作用到每一个函子的值
*/
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
/**
* Monad 函子
* 1, 核心作用是总是返回一个单层的函子 '
* 2, 通过拆解成互相链接的多个步骤,只要提供下一一步运行所需的函数,整个运算就会自动进行下去;
* 3, 可以让我们避开嵌套地狱,可以轻松的进行深度嵌套的函数, 比如IO和其他异步任务
*/
class Monad extends Functor {
join() {
// 实现返回单层函子
return this.val();
}
flatMap(f) {
return this.map(f).join();
}
}
/**
* IO函子
* 1,现实开发中不是所有的操作都是非常纯的,所以IO函子主要是封装那些不纯的操作;
* 2,特别是要记下他的map方法
*/
class IO extends Monad {
map(f) {
return IO.of(compose(f, this.val))
}
}
IO.of = x => new IO(x);
//这里有三个不纯的函数;所以要用IO函子包裹,然后就变纯了 一定要包裹到IO函子里面
const print = function (x) {
return new IO(function () {
console.warn(x + "【step 2】");
return x;
});
}
const tail = function (x) {
return new IO(function () {
console.warn(x[x.length - 1] + "【step 1】");
return x[x.length - 1] + "【step 1】";
});
}
const readFile = function (data) {
return new IO(function () {
console.warn('chain start');
return localStorage[data];
});
};
//关键的核心代码
//1.readFile('test') 创建了一个IO函子 值是 return localStorage["test"];
//2.IO继承自Monad 所以拥有了flatMap(把它叫chain也行)
//3.flatMap 接收了tail函数 tail干了啥呢 接受一个x返回一个新的IO 为啥呢??因为tail里的操作不纯啊
//4.flatMap内部执行了map 这个map是IO的map哦 因为extend的时候重写了
//5.IO.of(compose(f, this.val)) => IO函子(value = function(x){return f(g(x) })
// var g = function () {
// console.log('chain start');
// return localStorage[data];
// })
// var f = function (x) {
// return new IO(function () {
// console.log(x[x.length - 1] + "【step 1】");
// return x[x.length - 1] + "【step 1】";
// });
// }
// f(g());
//6.继续执行join函数 如果不执行join 最下面要一层层的执行val(可以去掉join试验一下) 这也是monad精髓所在
//7.上面实际上返回了 一个新的IO 所以可以链式的继续flatMap 但是万万注意的是这个io的value是组合函数传回来的一个函数 需执行记住啦!!
//8.所以join return this.val()会继续返回新的IO方便链式 完成了全部的操作
const result = readFile('test')
.flatMap(tail)
.flatMap(print);
//函数式编程只关心计算 和 数据的映射 并不关注该题的步骤 是旧的范畴到新范畴的映射
//其余的什么curry 懒加载 递归 等等都是衍生知识 仅此而已 如果你更关注过程的话 最后的一步解答方式该是如下
//result.val();