JavaScript Function Programming - 函数式编程


编程思路的概念[补充]

//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();

当下函数式编程最热的库

RxJS

cycleJS

lodashJS

underscoreJS

ramdajs

函数式编程的实际应用场景

易调试、热部署、并发

单元测试

总结与补充

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章