函数式编程-进阶

581 阅读9分钟

前言

如果对函数式编程不了解,或者有兴趣,可以先阅读前面的文章 函数式编程-入门,会帮助理解本篇文章。

本文旨在理论,有些枯燥乏味,但非常重要,说到函数式编程,那一定逃不过范畴论,那么什么是范畴论

范畴论

  • 函数式编程是范畴论的数学分支,范畴论认为世界上的所有概念体系都可以抽象出一个个范畴
  • 彼此之间存在某种关系概念、事物、对象等,都构成范畴,说白了,任何事物只要找到他们之间的关系,就能定义
  • 用函数表达范畴之间的关系,叫“态射”,同一个范畴所有成员,就是不同状态的“变形”(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是函数式编程的经典实现

image.png

  • 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或异步等脏操作

  1. 创建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))(函数组合)
  2. 调用flatMap(另一个IO函子) a. this.val = compose(f, this.val)
  3. 因为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方便链式 完成了全部的操作

尾声

函数式编程,就是操作各种函子,来降低系统复杂度,解决我们的实际问题