函数式编程

167 阅读9分钟

react就是函数式编程,高阶组件就是函数式编程实现
vue3也开始拥抱函数式编程

什么是函数式编程:FP

程序的本质就是:根据输入通过某种运算获得相应的输出
函数式编程就是对运算过程进行抽象,是用来描述数据(或者函数)之间的映射
函数式编程中的函数的概念指的不是程序中的函数(方法),而是数学中的函数映射关系

y=sin(x) y和x就是函数式编程的函数 而不是sin

相同的输入始终要得到相同的输出,这就是纯函数的概念

函数是一等公民

函数可以存储在变量中
函数可以作为参数
函数可以作为返回值

js中函数式一个普通对象(可以通过new Function()),所以我们可以把函数存储到变量或数组中,因为函数是对象,所以它还可以作为另一个函数的参数和返回值

高阶函数

可以把函数作为参数传递给另一个函数 可以把函数作为另一个函数的返回结果(函数是一等公民的特性)

forEach filter 函数实现-------- 函数作为参数

function makeFn(){
    let msg ='11111'
    return function(){
        console.log(msg)
    }
}
使用高阶函数的意义

抽象可以帮我们屏蔽细节,只需要关注我们的目标
高阶函数是用来抽象通用问题的
使函数变得更灵活,代码简洁

//map
const map = (arr, fn) => {
  let newArr = [];
  for (let val of arr) {
    newArr.push(fn(val));
  }
  return newArr;
};
//every
const every = (array,fn)=>{
    let result = true
    for (let val of array) {
        result = fn(val)
        if(!result){
            break
        }
    }
    return result
}

// some 
const some = (array,fn)=>{
    let result = false
    for (let val of array) {
        result = fn(val)
        if(result){
            break
        }
    }
    return result
}

闭包

闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员

从另一个作用域中,访问一个函数的内部函数,这个内部函数可以访问同级的变量或者说是可以访问外部函数的其他成员

好处是延长了,外部函数内部变量的作用范围

纯函数

相同的输入始终要得到相同的输出,而且没有任何可观察的副作用

纯函数类似于数学中的函数(用来描述输入输出之间的关系)

lodash就是一个纯函数的库

// 纯函数和不纯的函数
//  slice/splice
let arr =[1,2,3,4,5,6,7]
console.log(arr.slice(0,3))
console.log(arr.slice(0,3))
console.log(arr.slice(0,3))
//多次调用返回相同输出
console.log(arr.splice(0,3))
console.log(arr.splice(0,3))
console.log(arr.splice(0,3))
//多次调用返回不同输出

函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
我们可以把一个函数的执行结果交给另一个函数去处理

纯函数的好处
可缓存
// 缓存
function memoize(fn){
    let  cache = {

    }
    return function(){
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || fn.apply(fn,arguments)
        return cache[key]
    }
}
可测试
并行处理

在多线程环境下并行操作共享的内存数据很可能会出现同时修改某个数据,会出现意外情况

纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数

es6 web worker可以开启多线程进行处理

副作用

副作用让一个函数变得不纯,如果函数依赖外部的状态就无法保证输出相同,就会带来副作用

来源:所有的外部交互,把副作用可控范围之内

柯里化(Curry)

当一个函数有多个参数的时候,我们可以把这个函数改造,我们可以调用这个函数,只传递部分参数,并且让这个函数返回一个新的函数,并且这个新的函数接受剩余的函数,返回执行的结果

// 手写
function curry(func){
    return function curried(...args){
        // 判断形参和实参的个数         函数的length   是参数长度
        if(args.length <func.length){
            return function(){
                return curried(...args.concat(Array.from(arguments)))
            }
        }
        return func(...args)
    }
}
  1. 柯里化可以让我们给一个函数传递较少的参数得到一个已经被记住了某些固定参数的新函数
  2. 使用了一种闭包,是一种对函数参数的缓存
  3. 让函数变得更灵活,让函数颗粒度更小
  4. 把多元函数转化为一元函数,可以组合使用函数产生强大的功能

函数组合

纯函数和柯里化容易写出洋葱代码
函数的组合可以把细粒度的函数重新组合生成一个新的函数
组合函数的实现原理

function compose(...args){
    return function(value){
        return args.reverse().reduce(function(pre,fn){
            return fn(pre)
        },value)
    }
}

const compose = (...args) => {
    (value) => args.reverse().reduce((pre, fn) => fn(pre), value);
};
函数的组合要满足结合律
fn = compose(a,b,c)
// 结合律
compose(compose(a,b),c) ==  compose(a,compose(b,c))
如何调试组合函数

在中间插入不影响功能的日志函数

const log =v=>{
    console.log(v)
    return v
}

compose(a,log,b,log,c)

Pointfree

我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起(就是函数的组合),在使用这种模式之前我们需要定义一些辅助的基本运算函数

不需要指明处理的数据
只需要合成运算过程
需要定义一些辅助的基本运算函数
const f = fp.flowRight(fp.join('-'),fp.map(_.toLower),fp.split(' '))
案例

world wild web ==》 W. W. W
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(fp.join('. '),fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '))

函子Functor

什么是Functor
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)

// Functor 函子
class Container {
    constructor(value){
        this._value= value
    }
    map(fn){
        return new Container(fn(this._value))
    }
}

new Container(5)
    .map(x => x+1)    // 6
    .map(x => x*x)  // 36

class Container {
    static of(value){
        return new Container(value)
    }
    constructor(value){
        this._value= value
    }
    map(fn){
        return Container.of(fn(this._value))
    }
}

new Container(5)
    .map(x => x+1)    // 7
    .map(x => x*x)  // 49

总结:

  1. 函数式编程的运算不直接操作,而是由函子完成
  2. 函子就是一个实现了map契约的对象
  3. 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
  4. 想要处理盒子中的值,我们需要给map方法传入一个处理值的函数(纯函数)
  5. 最终map方法返回一个包含新值的盒子(函子)
MayBe函子

本质就是 增加处理异常的逻辑
问题是不知道哪里出了null之类的问题

class MayBe {
    static of(value){
        return new MayBe(value)
    }
    constructor(){
        this._value =value
    }

    map(fn){
        return this.isNothing()?MayBe.of(null) : MayBe.of(fn(this._value))
    }
    isNothing(){
        return this._value === null || this._value === undefined
    }
}
Either函子

Either两者中的任何一个,类似于if...else...的处理

异常会让函数变的不纯,Either函子可以用来做异常处理

class Left {
  static of(value) {
    return new Left(value);
  }
  constructor(value) {
    this._value = value;
  }
  map(fn) {
    return this;
    // 返回当前对象
  }
}

class Right {
  static of(value) {
    return new Right(value);
  }
  constructor(value) {
    this._value = value;
  }
  map(fn) {
    return Right.of(fn(this._value));
  }
}
// let r1=Right.of(12).map(x=>x+2)
// let r1=Left.of(12).map(x=>x+2)
// console.log(r1); // 14
// console.log(r2); // 12

function parseJson(str) {
  try {
    return Right.of(JSON.parse(str));
  } catch (e) {
    return Left.of({ error: e.message });
  }
}

let r = parseJson('{"name":"zs"}') // 返回的是right函子对象,这个时候可以.map了
  .map((x) => x.name.toUpperCase());
console.log(r);
IO函子

IO函子中的_value是一个函数,这里把函数作为值来处理
IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),通过包装使当前操作变纯
把不纯的操作交给调用者来处理

//IO 函子
const fp = require("lodash/fp");
class IO {
  static of(value) {
    return new IO(function () {
      return value;
    });
  }

  constructor(fn) {
    this._value = fn;
  }

  map(fn){
      return new IO(fp.flowRight(fn,this._value))
  }
}
let r = IO.of(process).map(p=>p.process) //  这一步是纯的,应为调用map始终会返回一个IO函子
// 但是通过map方法,把多个函数合并组合,可能是不纯的,这些不纯延迟到了调用的时候,变的可控
console.log(r._value());
Task函子 异步执行

异步任务的实现过于复杂,我们使用folktale中的Task来演示
folktale是一个标准的函数式编程库,提供了一些函数式处理的操作,例如compose、curry等

const { task } = require("folktale/concurrency/task");
const { split, find } = require("lodash/fp");
const fs = require("fs");

function readFile(filename) {
  return task((resolver) => {
    fs.readFile(filename, "utf-8", (err, data) => {
      if (err) resolver.reject(err);
      resolver.resolve(data);
    });
  });
}
readFile("package.json") // 这一步操作不会去读取文件,而是返回的一个task函子,所有函子都有一个map方法
  .map(split("\n"))
  .map(find((x) => x.inclides("version")))
  .run() // 读取文件
  .listen({
    onRejected: (err) => {
      console.log(err);
    },
    onResolved: (value) => {
      console.log(value);
    },
  });
Pointed函子

Pointed函子是实现了of静态方法的函子 of方法是为了避免使用new 来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)

Monad(单子)

Monad函子是可以变扁Pointed函子,IO(IO(x)),变扁解决函子嵌套的问题
一个函子如果具有join和of方法并准守一些定律就是一个Monad

IO函子的问题

//IO 函子的问题
const fp = require("lodash/fp");
const fs = require("fs");

class IO {
  static of(value) {
    return new IO(function () {
      return value;
    });
  }

  constructor(fn) {
    this._value = fn;
  }

  map(fn) {
    return new IO(fp.flowRight(fn, this._value));
  }
}

let readFile = function (fileName) {
  return new IO(function () {
    // 读取文件的步骤 延迟执行
    return fs.readFileSync(fileName, "utf-8");
  });
};
let print = function (x) {
  return new IO(function () {
    console.log(x);
    return x;
  });
};
let cat = fp.flowRight(print, readFile);
// IO(IO(x)) cat  是嵌套的函子
let r = cat("package.json");
let r = cat("package.json")._value(); //print里面的那个function
let r = cat("package.json")._value()._value(); //readFile里面的ß那个function

let r = readFile("package.json")
        .map(fp.toUpper)
        .flatMap(print)
        .join();

总结

函数式编程

一种编程思想,把运算过程抽象成函数

函数相关
  1. 函数是一等公民,
    函数也是对象,可以把函数像值一样处理,可以作为另一个函数参数,也可以作为返回值
  2. 高阶函数
    就是把函数作为另一个函数参数,也可以作为返回值 柯里化 函数组合 基于高阶函数的
  3. 闭包
函数式编程的基础
lodash

提供了很多方法辅助函数式处理

纯函数

相同的输入输出相同的输出,没有副作用
好处:可缓存,可测试,方便并行处理

柯里化

对函数进行降维处理,把多元函数降为一元函数,目的是函数组合时一起使用

管道

就是黑盒子的函数

函数组合
函子

控制副作用、异常处理、异步操作等
概念: 一个盒子,包裹一个值,处理这个值的话,需要调用这个盒子提供的map方法,map方法接受一个函数类型的参数,传递的这个函数就是处理值得函数
Maybe 函子 处理空值的异常
Either 函子 对异常处理 内部的value 都是保存的值
IO函子 内部的value 保存的是一个函数,可以延迟执行一个函数,控制副作用
Task -folktale 异步处理,处理异步任务
Monad 处理函子嵌套的问题 join和of方法