2
头图

从本文你能了解到那些知识

  • 认识函数式编程
  • 函数相关复习

    • 函数是一等公民
    • 高级函数
    • 闭包
  • 函数式编程基础

    • lodash
    • 纯函数
    • 柯里化
    • 管道
    • 函数组合
  • 函子

    • Functor
    • MayBe
    • Either
    • IO
    • Task - folktale
    • Monad

什么是函数式编程

Functional Programming (FP)编程范式之一 函数式编程的历史

  • 编程范式类型:

    • 面向过程编程:按步骤实现功能
    • 面向对象编程:把现实事物抽象成程序中的类和对象,通过封装继承多态来演示事物之间的联系(抽象现实生活的事物)
    • 函数式编程:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)

      • 程序的本质:根据输入通过某种运算获得相应的输出
      • x -> f(映射,联系) -> y | y = f(x) 用于描述x和y的映射关系
      • 注意函数式编程的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如y = sin(x), y和x的关系
      • 总结:函数式编程用来描述数据(函数)之间的映射

image.png

为了什么要了解函数式编程

  1. 基于目前框架的使用,(react,vue3)
  2. 函数式编程可以抛弃this,(相比面向对象编程方式)
  3. 打包过程可以更好的利用tree shaking过滤无用代码
  4. 方便测试和并行处理以及代码重用
  5. 函数式编程不会保留中间的结果,所以变量是不可变的(无状态的)
  6. 线上开源函数式库lodashunderscoreramda

前言,函数复习,函数可以怎么用

(函数)

  • 函数是一等公民
  • 函数可以存储在变量中 const fn = function (){}
  • 函数可以作为参数 forEach(array,fn)
  • 函数作为返回值 function fn(){ return function(){} }

(高阶函数)

  • 高阶函数利用函数式编程思想,通过函数作为参数传递,动态控制输入和输出,封装过程关注输入和输出
//高阶函数-函数作为参数(由于对数组的操作是变化的,那么用fn做参数来定义对参数1的变化)
//好处:可以让函数更灵活
//使用时屏蔽过程细节,只关注目标和结果

//实现一个forEach方法
function forEach(arr:Array<any>,fn:Function){
  for(let i:number=0;i<arr.length;i++)
  {
    fn(arr[i],i,arr)
  }
}

forEach([1,4,56,76],function (item:any,index:number,originArr:any[]) {
  console.log(item,index,originArr)
})

//实现一个filter
function filter(arr:Array<any>,fn:Function) {
  let _arr = [];
  for(let i:number=0;i<arr.length;i++)
  {
    let _fnbol = fn(arr[i],i,arr)
    _fnbol && _arr.push(arr[i])
  }
  return _arr
}

let filter2 = filter([1,4,43,2134],function (item:any,index:number,arr:any[]) {
    return item%2===0
})
console.log("取偶数:",filter2)

闭包

  • 函数和其周围的状态捆绑到一起形成闭包,可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域成员
  • 函数会放在一个执行栈上,当函数执行完毕后会从栈内移除,但是堆上的作用域成员因为被外部引用不能被释放,因此内部依然可以访问外部函数
  • 通过断点观察Call Stack看到Scope作用域,Closure即为储存下来的闭包
  • 应用场景

    • 函数只执行一次(函数内部变量打标记)
    • 缓存公共方法的固定参数,变其他参数
/**
 * 闭包
 * 函数和其周围的状态捆绑到一起形成闭包,
 * 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域成员
*/
function a() {
  let w = "mcgee"
  return function b() {
    return w
  }
}

const _a = a()  //此时外部对函数内部有引用,此时,a内的成员w变量不能被释放
_a()

//函数会放在一个执行栈上,当函数执行完毕后会从栈内移除,
//但是堆上的作用域成员因为被外部引用不能被释放,因此内部依然可以访问外部函数
//只执行一次的函数(用标记判断是否执行过
//isDone标记由于词法作用域会被保存下来,并被更改为true,下面调用没用了)
function once(fn:Function) {
  let isDone:boolean = false
  return function (num) {
    // console.log("innerthis",this) //window
    if(!isDone)
    {
      isDone = true
      return fn.call(null,num)
    }
  }
}

let pay = once(function (money) {
  console.log("支付了:",money)
})
pay(10) //支付了: 10
pay(10) //无效
//定义2次幂,3次幂方法 例如:Math.pow(2,2),Math.pow(7,2) 重复写入2次幂
//思想:将2次幂 缓存在函数里,再调用基于2次幂的方法
//还可以通过断点 Call Stack 可以看到 scope作用域 ,Closure即为闭包
function pow2(num:number) {
  let mi:number= 2
  return function () {
    return Math.pow(num,mi)
  }
}

function powFn(mi:number) {
  return function (num:number) {
    return Math.pow(num,mi)
  }
}
let mi2 = powFn(2) //返回一个2次幂的函数
console.log(mi2(5))
console.log(mi2(10));
  • 总结,当一个函数返回一个函子时候要想到Monad 。当调用一个函数,返回一个值时调用map,当调用一个函数,返回一个函子时调用mapjoin
  • 认识函数编程,函数复习,函数式编程基础,函子

函数式编程基础

  • lodash:基于函数式编程的库,本文中用到
    first / last / toUpper / toLower / reverse / each / includes / find / findIndex / split / join / map / memorize / curry / flow / flowRight方法,lodash提供了对数组,数字,对象,字符串,函数等操作的方法 (详见官文)
  • 纯函数:始终有输入和输出,相同的输入始终得到相同的输出
  • 函数柯里化:将多参数函数分割成颗粒度更小的纯函数,便于重用,同时组合产生强大的功能
  • 函数组合:利用纯函数实现细粒度函数,通过对细粒度函数的组合组合成更强大的函数

(纯函数)

image.png

//纯函数:对相同的输入永远会得到相同的输出
//slice和splice区别
let arr = [1,2,3,4,5,6]

//纯
console.log(arr.slice(0,4))
console.log(arr.slice(0,4))
console.log(arr.slice(0,4))

//不纯
console.log(arr.splice(0,3))
console.log(arr.splice(0,3))
console.log(arr.splice(0,3))
  • lodash库使用

    //lodash 演示
    //first/last/toUpper/reverse/each/includes/find/findIndex
    const _ = require('lodash')
    
    const array = ["mcgee","jack","rose","kobe"]
    console.log(_.first(array))
    console.log(_.last(array));
    
    console.log(_.toUpper(_.first(array)))
    console.log(_.reverse(array))
    
    const r = _.each(array, (item,index)=>{
    console.log(item,index)
    })
    console.log(r)
    
    const _clude = _.includes(array, "kobe", 0)
    console.log(_clude)
    
    /**
     * 参数
        collection (Array|Object): 一个用来迭代的集合。
        [predicate=_.identity] (Array|Function|Object|string): 每次迭代调用的函数。
        [fromIndex=0] (number): 开始搜索的索引位置。
    */
    const _find = _.find(array, (item)=>{return item == "kobe"}, 0)
    console.log(_find)
    
    
    /**
     * 参数
        array (Array): 要搜索的数组。
        [predicate=_.identity] (Array|Function|Object|string): 这个函数会在每一次迭代调用。
        [fromIndex=0] (number): The index to search from.
    */
    const _findIndex = _.findIndex(array, (item)=>{return item == "kobe"}, 0)
    console.log(_findIndex);
  • 可缓存,lodash const fn1 = _.memoize(fn)

    //可缓存
    function getArea(r) {
    console.log(r)  //只执行一次 memoize是缓存方法
    return Math.PI*r*r
    }
    const _memoize= _.memoize(getArea)
    console.log(_memoize(2));
    console.log(_memoize(2));
    console.log(_memoize(2));
    //模拟memoize实现
    //getArea为纯函数,key取半径,value取算出来的pai r fang
    function memoize(fn:Function) {
    let cache={}
    return function (...num) {
      let key = JSON.stringify(num)
      cache[key] = cache[key] || fn.apply(null,num)
      return cache[key]
    }
    }
    
    const _memofn = memoize(function(r:number){
    console.log(r)
    return Math.PI * r * r
    })
    console.log(_memofn(2))
    console.log(_memofn(2))
    console.log(_memofn(2))
  • 可测试,纯函数始终有输入和输出,单元测试就是在断言函数的结果,所以纯函数都是可测试的
  • 可并行处理,多线程环境下并行操作共享的内存数据很可能出现意外情况 (共享全局变量),纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (参考阮一峰的:WebWorker)
  • 副作用,纯函数属于封闭的空间,如果函数依赖于外部的状态就无法保证函数的输出相同,导致函数不纯,就会带来副作用

    • 副作用来源:1.外部变量 2.配置文件 3.数据库 4.获取用户的输入

      //副作用
      
      //不纯
      let baseaAge = 18
      function buchun(age:number){
      return age >= baseaAge
      }
      //对于相同的输入,不一定有相同的输出,因为baseAge可变
      
      //纯函数(但是有{18}硬编码,后续可以通过柯里化解决)
      function chun1(age:number){
      let baseaAge = 18
      return age >= baseaAge
      }
    • 所有外部交互都有可能产生副作用,使得方法的通用性下降,不利于扩展和重用,有可能带来不确定性和安全隐患,通过程序尽量控制
  • 纯函数的库lodash的使用,提供了对数组,数字,对象,字符串,函数等操作的方法 (详见官文)

(柯里化)

  • 使用柯里化Curry解决硬编码问题
  • 函数多个参数时进行改造,调用时只传递部分参数,并且让函数返回新的函数,新的函数接收剩余参数,并返回相应的结果
  • lodash _curry(func)使用

    • 接收一个或多个func的参数,如果func所需的参数都被提供则执行func并返回执行结果,否则继续返回该函数等待接收剩余参数
    • 参数:需要柯里化的函数,返回值:柯里化后的函数
    • 类似于对于参数的'缓存',将多参数函数分割成颗粒度更小的纯函数,便于重用,同时组合产生强大的功能
//lodash _.curry()使用
const _ = require("lodash")

//三元函数
function getSum(a,b,c) {
  return a+b+c
}
const curried = _.curry(getSum)
console.log(curried(1,2,3))
console.log(curried(1)(2,3))
console.log(curried(1,2)(3))
console.log(curried(1)(2)(3));
  • 柯里化的原理实现

    //柯里化原理模拟
    
    //通过判断返回的函数参数是否传全产生两种情况,
    //传全那么直接调用传入的fn方法,为传全返回一个新函数,未传全则返回一个新函数,新函数的参数为上次参数和当前的arguments的和
    /**
    * 解释01:此时的arguments为继续调用的参数的伪数组 例如:fn(1)(2,3) arguments指(2,3)
    *        伪数组转化通过Array.from(),
    *        args是个闭包内的变量被保存下来,指的是上一次调用curriedFn时的args
    * */
    function curry(fn:Function) {
    return function curriedFn(...args) {
      if(args.length < fn.length) //传入参数的个数 or 获取fn函数形参个数
      {
        return function () {
          return curriedFn(...args.concat(Array.from(arguments))) //解释01
        }
      }
      return fn.apply(null,args) //fn(...args)
    }
    }
    
    function addSum(a,b,c) {
    return a+b+c
    }
    
    const _addSum = curry(addSum)
    console.log(_addSum(1,2,3));
    console.log(_addSum(1)(2,3));

(函数组合)

  • 纯函数和柯里化很容易写出洋葱代码 h(g(f())) _.toUpper(_.first(_.reverse(array)))
  • 函数组合实现以上相同功能,但是通过组合的方式

    //函数组合演示
    function compose(f,g) {  
    return function (value) {
      return f(g(value)) //封装个洋葱函数
    }
    }
    
    //案例:取数组最后一个值
    const _reverse = function (value) { //辅助函数
    return value.reverse()
    }
    const _first = function (value) { //辅助函数
    return value[0]
    }
    
    const _compose = compose(_first,_reverse)
    console.log(_compose([1,2,3]))

    (管道)

概念:将洋葱代码拆分成多个函数 fn=compose(fn1,fn2,fn3) b=fn(a) a -> fn3 -> fn2 -> fn1 -> b
image.png

  • 注意,组合函数执行顺序从右到左,a -> fn3 -> fn2 -> fn1 -> b
  • lodash _.flow(fn3,fn2,fn1) _.flowRight(fn1,fn2,fn3)

    const arr_ = ["one","two","three"]
    const f1 = _.flowRight(_.toUpper,_.first,_.reverse)
    console.log(f1(arr_));
  • flowRight的原理

    //通过fn循环调用
    function flowRight(...args) {
    return function (val) {
      let fn:Function;
      for(let i = args.length-1; i>=0;i--){
        fn = !fn?args[i](val):args[i](fn)
      }
      return fn
    }
    }
    
    //通过reduce实现循环调用
    function compose(...args) {
    return function (val) {
      return args.reverse().reduce((data,currentdata)=>{ //data累积器 currentdata当前值,每次用当前值调
        return currentdata(data)
      },val)//累积器的初始值
    }
    }
    
    //优化成ES6
    const compose1 = (...args) => val => args.reverse().reduce((data,currentdata)=> currentdata(data),val)
    
    
    //案例:取出数组的最后一个元素并转化为大写
    const _t = value => value.toUpperCase()
    const _f = value => value[0]
    const _r = value => value.reverse()
    
    const f = flowRight(_t,_f,_r)
    console.log(f(["one","two","three"]));
    
    const f1 = compose(_t,_f,_r)
    console.log(f1(["one","two","three"]));
  • 组合要满足结合律,结果一致 compose(compose(f,g),h) == compose(f,compose(g,h))

    const arr_ = ["one","two","three"]
    const f1 = _.flowRight(_.toUpper,_.first,_.reverse)
    console.log(f1(arr_));
    
    const f2 = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse)
    console.log(f2(arr_));
  • 注意函数组合时候组合的函数只有一个参数,如果多个参数可以用_.curry “降参”,后面会通过模块解决“降参”问题
  • 注意函数组合log调试,可以通过插入一个log函数的形式

    const _ = require('lodash')    
    // 组合函数的调试
    
    //需求:NEVER SAY DIE ----> never-say-die
    //为了函数组合,对两个参数的lodash进行“降参”,转化成curry,先存用什么切,再输入切什么,再执行_.split()
    // const _split = _.split("z-z-z-z","-")
    // const _join = _.join(["z","z","z","z"],"&")
    
    const split = _.curry((sep,str)=>_.split(str,sep))
    const join = _.curry((sep,str)=>_.join(str,sep))
    const map = _.curry((fn,arr)=>_.map(arr,fn))
    
    //插入调试方法并返回给下一个函数,插入过多,结构不清晰
    // const log = v=> { 
    //   console.log(v)
    //   return v
    // }
    
    //打印的位置,打印的数据
    const trace = _.curry((tag,v)=>{
    console.log(tag,v)
    return v
    })
    
    // const f = _.flowRight(join("-"), log ,_.toLower, log ,split(" "))
    const f = _.flowRight( join("-") , map(_.toLower) , split(" "))
    // const f =  _.flowRight( join("-") , trace("map之后") , map(_.toLower) , trace("split之后") , split(" "))
    console.log(f("NEVER SAY DIE"));
  • lodash/fp:auto-curried iteratee-fist data-last 自带柯里化,函数优先,数据滞后

    //lodash/fp模块 lodash里的函数式编程模块,用来解决上节函数组合lodash里的“降参”
    //它提供了不可变的“auto-curried” "iteratee-fist" "data-last" 自带柯里化,函数优先,数据滞后
    //以前的方法是_.map(["a","b","c"],_.toUpper) 数据优先,函数滞后
    //fp.map(fp.toUpper,["a","b","c"])如果只调用前面返回一个curried等待数据
    const _ = require("lodash")
    const fp = require("lodash/fp")
    
    //需求:NEVER SAY DIE ----> never-say-die
    
    // const split = _.curry((sep,str)=>_.split(str,sep))
    // const join = _.curry((sep,str)=>_.join(str,sep))
    // const map = _.curry((fn,arr)=>_.map(arr,fn))
    // const f = _.flowRight( join("-") , map(_.toLower) , split(" "))
    // console.log(f("NEVER SAY DIE"));
    
    const f = fp.flowRight(fp.join("-"),fp.map(fp.toLower),fp.split(" "))
    console.log(f("NEVER SAY DIE"));
  • 注意对于lodash迭代的方法有个问题,例如parseInt它不止一个参数,解决办法自己封装parseInt,对于lodash/fp没有此问题参考地址

image.png

// const _ = require("lodash")
//lodash 和 lodash/fp模块中的 map 方法区别

// const map_ = _.map(["1","23"],parseInt)
// console.log(map_) [1,"NaN"]
//因为parseInt问题 parseInt("1",0,array) parseInt("23",2,array) 元素,索引,数组
//每次调用时候会像parseInt传三个参数,而pf支持柯里化,它只传了1 or 23

const fp = require("lodash/fp")
console.log(fp.map(parseInt,["1","23"]))
  • PointFree:基于pf模块,不需要指明处理的数据,只需要合成运算过程fp.flowRight(fp.join("-"),fp.map(_.toLower))

函子

(函子)帮助我们控制副作用,处理异常,和异步操作

Functor函子 |
MayBe函子 |
Either函子 |
IO函子 |
Task函子 - folktale |
Monad函子
  • Functor 使用函子 对 函数式编程如何把副作用控制在可控范围内,异常处理,异步操作
  • 容器:包含值与值得变形关系(这个变形关系就算函数)
  • 函子:一个特殊的容器,通过普通对象实现。该对象拥有map方法,map方法接收一个函数,运行接收的函数对构造函数传入的值进行处理(变形关系),并将处理结果返回给一个新函子对象
  • Functor函子

    //Functor,函子,不直接操作变量
    class Container{
    private _value   //维护的值,永远不对外公布(ts要加_value定义,js不用这行)
    static of (value){ //设置静态变量方便外部调用,当然也可以不用,设置是为了区分面向对象new
      return new Container(value)
    }
    constructor(value)
    {
      this._value = value
    }
    map(fn){
      return Container.of(fn(this._value)) //返回新函子供链式调用,不直接操作this._value
    }
    }
    
    const newC1 = Container.of(5)
    .map(x=>x+2)   //对 5 处理的方法,并返回一个新盒子
    .map(x=>x*2)   //x为上一次处理后的返回值
    console.log(newC1)
    //Functor有个问题,如果传入null undifined 会导致函数不纯报错,解决办法MayBe函子
    const newC2 = Container.of(null)
    .map(x=>x.toUpperCase())
  • MayBe函子,解决由于Functor函子传入null,undefined导致函数不纯产生副作用。即可以对外部的空值情况作处理

    //MayBe 函子 
    //添加传入判空操作,防止报错导致不纯
    class MayBe{
    private _value
    static of(value)
    {
      return new MayBe(value)
    }
    constructor(value)
    {
      this._value = value
    }
    map(fn)
    {
      return this.isNothing()? MayBe.of(null):MayBe.of(fn(this._value)) //不存在返回一个内部变量是null的盒子
    }
    isNothing(){
      return this._value === null || this._value === undefined
    }
    }
    
    let r = MayBe.of("hello world")
    .map(x=>x.toUpperCase())
    console.log(r);  // MayBe {_value:'HELLO WORLD'}
    let w = MayBe.of(null)
    .map(x=>x.toUpperCase())
    console.log(w); // MayBe {_value: null}
    //MayBe 函子有个问题 ,无法抓取链式调用哪一步为null,解决办法Either函子
    let k = MayBe.of("hello world")
    .map(x=>x.toUpperCase())
    .map(x=>null)
    .map(x=>x.split(" "))
    console.log(k); //MayBe { _value: null }
  • Either函子,解决了由MayBe函子链式调用产生错误但无法定位错误的情况

    //Either函子 用于处理异常
    //定义两个函子,类似if...else... 
    //Left类返回this不会执行map方法,存储错误信息,Right类正常执行
    class Left{
    private _value
    static of(value){
      return new Left(value)
    }
    constructor(value){
      this._value = value
    }
    map(fn){
      return this //不操作fn
    }
    }
    
    class Right{
    private _value
    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 r2 = Left.of(12)
    .map(x=>x+2)
    
    console.log(r1,r2) //Right { _value: 14 } Left { _value: 12 }
    //使用Either函子处理报错案例
    function parseJSON(str) {
    try{
      return Right.of(JSON.parse(str))
    }catch(e){
      return Left.of(e.message)
    }
    }
    let r3 = parseJSON("{name:'mcgee'}")
    console.log(r3)  //Left { _value: 'Unexpected token n in JSON at position 1' }
    let r4 = parseJSON('{"name":"mcgee"}')
    console.log(r4) //Right { _value: { name: 'mcgee' } }
  • IO函子:与其他函子本质区别是_value是一个函数,函数可以是不纯的,可以用于异步执行,将纯不纯的内部函数包裹在纯函数中,用_value()来延时执行不纯的操作

    //io 函子,inputoutput
    //_value为函数,将传入的value包裹在函数中
    //通过_value()惰性请求,可以包裹不纯函数
    const fp = require("lodash/fp")
    class IO{
    public _value;
    static of(value){
      return new IO(function(){
        return value
      })
    }
    constructor(fn){
      this._value = fn
    }
    
    map(fnMap){
      return new IO(fp.flowRight(fnMap,this._value))
    }
    }
    //使用IO函子输出process.execPath
    const r1 = IO.of(process)
    .map(x=>x.execPath)
    console.log(r1._value())
    /使用IO函子执行异步
    const r = IO.of("hello,world")
    .map(x=>{
      return new Promise(resolve=>{
        setTimeout(()=>{
          resolve(x.split(","))
        },2000)
      })
    })
    .map(x=>x.then((res)=>res.join("_")))
    console.log(r._value().then(result=>console.log(result)))
    //输出Promise{<pending>} 2slater... hello_world
    //IO函子通过map会返回一个新IO函子,那么这个新IO函子格式 是 `A {_value:{}}`
    //这个函数是立即返回的,所以在第一个map里的异步还未执行完时候已经有Promise { <pending> }新函子对象输出了
  • IO函子的问题,如果有两个IO函子,放入fp.flowRight("IO2","IO1")执行,那么会产生函子嵌套,解决函子嵌套问题,查看后面的Monad函子,先忽略
  • folktale函数式编程库,提供了compose,curry等方法,Task,Either,MayBe等函子

    //folktale 中的compose curry
    const {compose,curry} = require("folktale/core/lambda")
    const {toUpper,first} = require("lodash/fp")
    
    let f = curry(2,function (x,y) {  //参数1表示参数个数,参数2便是柯里化的函数
      return x+y
    })
    console.log(f(1)(6));
    
    let f1 = compose(toUpper,first) //从右向左执行
    console.log(f1(["first","one"]))
  • Task函子的使用

    //folktale 中的Task异步处理 
    //案例:解析package.json里的version
    const {task} = require("folktale/concurrency/task") //task在2.0中是个函数,返回一个函子对象
    const fs = require("fs") //node读取文件模块
    
    function readFile(filename) {
    return task((resolver)=> { //task接收一个函数,接收函数固定参数resolver,返回一个Task函子
      fs.readFile(filename,'utf-8',(err,data)=>{ //node读取文件api,文件名|格式|回调错误优先
        if(err) resolver.reject(err)
        resolver.resolve(data)
      })
    })
    }
    readFile("package.json")
    .run()                  //执行readFile
    .listen({               //监听返回值        
      onRejected:err=>{
        console.log(err)
      },
      onResolved:msg=>{
        console.log(msg);   //打印出读出的package.json
      }
    })
  • 上面只能返回值,如果要对返回的值进行操作,需要在listen添加map方法,map方法返回一个新函子同lodash链式调用

    readFile("package.json")
    .map(split("\n"))
    .map(find(x=>x.includes("version"))) //find x是上面map返回的数组的每项,
    .run()                  //执行readFile
    .listen({               //监听返回值        
      onRejected:err=>{
        console.log(err)
      },
      onResolved:msg=>{
        console.log(msg);   //打印出读出的package.json
      }
    })
  • Point函子:实现了of静态方法的函子,of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理)
  • Monad函子:由于IO函子组合时会出现嵌套函子情况,Monad函子是可以变扁的Point函子 IO(IO(x)),一个函子如果具有joinof两个方法并遵守一些定律就是Monad

    //IO函子的问题 嵌套问题
    const fp = require('lodash/fp')
    const fs = require("fs")
    class IO{
    private _value
    static of(value){
      return new IO(function(){
        return value
      })
    }
    constructor(fn)
    {
      this._value = fn
    }
    map(fnMap){
      return new IO(fp.flowRight(fnMap,this._value))
    }
    }
    //案例,读取文件,并打印
    
    //读出文件存在异步,导致函数不纯,用IO包一下
    function readFile(filename) { 
    return new IO(function () {
      return fs.readFileSync(filename,"utf-8") //node fs 同步读取
    })
    }
    
    
    
    function print(data) {
    return new IO(function(){
      console.log(data)
      return data
    })
    }
    
    //我们知道flowRight执行时,会把添加的函数从右至左依次执行,并且,先执行的函数的返回值会传给下一个函数,也就是f(g(k()))
    //那么下面函数的执行时,readFile执行完返回一个new IO函子,再把新函子通过 data形参 执行print 执行print函子,
    //当print IO执行完,也会返回了一个new IO
    //此时两个IO的关系相当于 IO(IO(data))
    /**
    *   {
          tag: 'print 返回的 IO'
          _value: function() {
            return {
              tag: 'readFile 返回的 IO',
              _value: function() {
                return fs.readFileSync(...)
              }
            }
          }
        }
    
    第一次调用 _value() 就是执行了 print 返回的 IO 函子
    第二次调用 _value() 就是执行了 readFile 返回的 IO 函子
    * 
    */
    let cat = fp.flowRight(print,readFile)   
    
    let r = cat("package.json")
    console.log(r) //IO { _value: [Function] }  类似IO(IO(data))
    console.log(r._value(),r._value()._value()) //输出 print的函子,readFile的函子
//Monad函子 用来解决函子嵌套的问题
//Monad函子是可以变扁的Point函子 IO(IO(x))
const fp = require('lodash/fp')
const fs = require("fs")
class IO{
  private _value
  static of(value){
    return new IO(function () {
      return value
    })
  }
  constructor(fn){
    this._value = fn
  }
  map(fnMap){
    return new IO(fp.flowRight(fnMap,this._value))
  }

  join(){
    return this._value()
  }

  // 对map和join的联合封装,先执行map,返回一个new IO,
  // new IO里面存在join方法,同时他的this._value表示上次map传入的运行结果
  // 调用join相当于执行了上个函子存的缓存函数,也就是对上一个函子所存的函数进行自执行 
  // IO(IO(x)) -> flatMap = IO(x)
  flatMap(fnMap){
    return this.map(fnMap).join()
  }
}

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 r = readFile("package.json")
  .flatMap(print)
  .join()
// console.log(r,r._value())
console.log(r)
  • 当然可以对Monad进行额外操作
let r = readFile("package.json")
  .map(fp.toUpper)  //转化成大写
  .flatMap(print)
  .join()
// console.log(r,r._value())
console![image.png](/img/bVcREEK)

mcgee0731
60 声望4 粉丝

不会做饭的程序猿不是一个好厨子