函数式编程

现在大公司的编程方式有:

1.oop(面向对象编程);

2.aop(面向切面编程);

3.函数式编程(JavaScript Functional Programming);

范畴论Category Theory

  1. 函数式编程是范畴论的数学分支是一门很复杂的数学,认为世界上所有概念体系都可以抽象出一个个范畴
  2. 彼此之间存在某种关系概念、事物、对象等等,都构成范畴。任何事物只要找出他们之间的关系,就能定义
  3. 箭头表示范畴成员之间的关系,正式的名称叫做“态射”(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的“变形”(transformation)。通过“态射”,一个成员可以变形成另一个成员

函数式编程5大特点

  1. 函数是第一等公民
  2. 只用表达式,不用语句
  3. 没有副作用
  4. 不修改状态
  5. 引用透明(函数运行只靠参数)

专业术语

  1. 纯函数
  2. 函数的柯里化
  3. 函数组合
  4. Point Free
  5. 声明式命令式代码
  6. 惰性求值

纯函数

对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。

var xs=[1,2,3,4,5];
//Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
xs.slice(0,3); //[1,2,3]
xs.slice(0,3);  //[1,2,3]
xs.splice(0,3);   //[1,2,3]
xs.splice(0,3);  //[4,5]
import _ from 'lodash';
var sin=_.memorize(x=>Math.sin(x));
var a=sin(1); //第一次计算的时候会稍慢一点
var b=sin(1); //第二次有了缓存,速度极快
//纯函数不仅可以有效降低系统的复杂度,还有很多很棒的特性,比如可缓存性
//惰性函数

不纯

var min=18;
var checkage=function(age){
    return age>min; //依赖于外部的min,导致不纯
}

函数的柯里化

传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

用柯里化来改造上面的不纯函数

var checkage=min=>(age=>age>min);
var checkage18=checkage(18);
checkage18(20);

Point Free

  1. 把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量
  2. 这个函数中,我们使用了str作为我们的中间变量,但这个中间变量除了让代码变得长了一点以外是毫无意义的
    const f=str=>str.toUpperCase().split('')

应用

var toUpperCase=word=>word.toUpperCase();
var split=x=>(str=>str.split(x));
var f=compose(split('').toUppercase);
f("abcd efgh");

这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用

声明式与命令式代码

命令式代码的意思就是,我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。而声明式就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。

//命令式
let CEOs=[];
for(var i=0;ic.CEO);

优缺点

函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。

相反,不纯的函数式的代码会产生副作用或者依赖外部系统环境,使用他们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于程序员的心智来说是极大的负担。

惰性求值

function fn(){
    if(IE){//IE时
        fn=a;
    }else{//chrome时
        fn=b;
    }
    return fn;
}

第一次执行时会走if,然后fn重新赋值,第二次执行fn时,直接赋值不用判断,提高执行效率

高阶函数

函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象

//命令式
var add=function(a,b){
    return a+b;
};
funtion math(func,array){
    return func(array[0],array[1]);
}
math(add,[1,2]); //3

尾调用优化

指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。函数调用自身,称为递归。如果尾调用自身,就称为尾递归。递归需要保存大量的调用记录,很容易发生栈溢出错误,如果使用尾递归优化,将递归变为循环,那么只需要保存一个调用记录,这样就不会发生栈溢出错误了。

//不是尾递归,无法优化
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强制使用尾递归

普通递归时,内存需要记录调用的堆栈所出的深度和位置信息。在最低层计算返回值,再根据记录的信息,跳会上一层级计算,然后再跳回到更高一层,依次运行,直到最外层的调用函数。在cpu计算和内存会消耗很多,而且当深度过大时,会出现堆栈溢出

function sum(x){
    if(x===1) return 1;
    return x+sum(x-1);
}
sum(5);  //15  递归
function sum(x,total){
    if(x===1) return x+total;
    return sum(x-1,x+total);
}
sum(5,0); 
sum(4,5);
sum(3,9);
sum(2,12);
sum(1,14);
15 //尾递归,每次执行之后,函数重新传入参数,直到结束

整个计算过程是线性的,调用一次sum(x,total)后,会进入下一个栈,相关的数据信息和跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的sum(5,0).这能有效的防止堆栈溢出。
在ECMAScript6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能。

闭包

自己领会

函数式编程比较火热的库

  • Rxjs //截流与仿抖
  • cyclejs
  • lodashjs
  • underscorejs //开始学最佳的库
  • ramadajs

需要学习

范畴与容器

  1. 我们可以把“范畴”想象成是一个容器,里面包含两样东西。值(value)、值的变形关系,也就是函数。
  2. 范畴论使用函数,表达范畴之间的关系。
  3. 伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的“函数式编程”。
  4. 本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式式同一类东西,都是数学方法,只是碰巧他能用来写程序。为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。

函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。

容器、Functor(函子)

  1. $(...)返回的对象并不是一个原生的DOM对象,而是对于原生对象的一种封装,这在某种意义上就是一个"容器"(但它并不函数式)
  2. Functor(函子)遵守一些特定规则的容器类型
  3. Functor是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力。把东西装进一个容器,只留出一个接口map给容器外的函数,map一个函数时,我们让容器自己来运行这个函数,这样容器就可以自由地选择何时何地如何操作这个函数,以致于拥有惰性求值、错误处理、一步调用等非常牛掰的特性
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')

Maybe 函子

函子接受各种函数,处理容器内部的值,这里就有一个问题,容器内部的值可能是一个空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。

Functor.of(null).map(function(s){
    return s.toUpperCase();
});
//TypeError
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) //报错,未定义
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) //不会报错了
//新的容器我们称为Maybe

Either 函子

条件运算if...else 是常见的运算之一,函数式编程里面,使用Either函子表达。Either函子内部有两个值:左值(left)和右值(right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。

class Either extends Functor{
    constructor(left,right){
        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(5,7);
Either.of(1,null).map(addOne);  //Either(2);
Either
    //右值中有address这个属性,则覆盖原来的xxx,否则使用默认的xxx
    .of({address:'xxx'},currentUser.address)    .map(updateField);

es5写法

错误处理、Either

var Left=function(x){
    this.__value=x;
}
var Rigth=function(x){
    this.__value=x;
}
Left.of=function(x){
    return new Left(x);
}
Right.of=function(x){
    return new Right(x);
}
Left.prototype.map=functin(f){
    return this;
}
Right.prototype.map=function(f){
    return Right.of(f(this.__value));
}

Left和Right唯一的区别就在于map方法的实现,Right.map的行为和我们之前提到的map函数一样。但是Left.map就很不同了:它不会对容器做任何事情,只是很简单地把这个容器拿进来又扔出去。这个特性意味着,Left可以用来传递一个错误消息。

var getAge = user => user.age ? Right.of(user.age):Left.of("Error");;
getAge({name:'stark',age:'21'}).map(age=>'Age is '+age);
//Right('Age is 21');
getAge({name:'stark'}).map({age=>'Age is '+age});
//Left('Error');

Left 可以让调用链中任意一环的错误立即返回到调用链的尾部,这给我们错误处理带来了很大的方便,再也不用一层又一层的Try/catch

AP因子

函子里面包含的值,完全可能是函数。我们可以想象这样一种情况,一个函子的值是数值,另一个函子的值是函子。

class Ap extends Functor{
    ap(F){
        return Ap.of(this.val(F.val));
    }
}
Ap.of(addTwo).ap(Functor.of(2));

实例

function Functor(val){
    this.__val=val;
}
Functor.of=function(val){
    return new Functor(val);
}
Functor.prototype.map=function(fn){
    return Functor.of(fn(this.__val));
}
function addTwo(x){
    return x+2;
}
function Ap(val){
    Functor.call(this,val);
}
Ap.of=function(val){
    return new Ap(val);
}
var __proto=Object.create(Functor.prototype);
__proto.constructor=Ap.prototype.constructor;
Ap.prototype=__proto;
Ap.prototype.ap=function(F){
    return Ap.of(this.__val(F.__val));
}

const A=Functor.of(2);
const B=Ap.of(addTwo);
console.log(B.ap(A));  //4
//console.log(B.ap(A).ap(A));此时会报错,B.ap(A)其中A不是个函数

IO

真正的程序总要去接触肮脏的世界

function readLoaclStorage(){
    return window.localStorage;
}

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));
}

Monad

Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。

Promise就是一种Monad

Monad糖我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其它异步任务

Maybe.of(
    Maybe.of(
        Maybe.of({name:'Mulburry',number:99})
    )
)
class Monad extends Functor{
    join(){
        return this.val;
    }
    flatMap(f){
        return this.map(f).join();
    }
}

Monad 函子的作用是,总是返回一个单层的函子。它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,他会取出后这内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。

如果函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了flatMap方法总是返回一个单层的函子。这意味着嵌套的汉子会被铺平(flatMap)

你可能感兴趣的:(函数式编程)