函数式编程

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

  • 函数式编程是随着React的流行受到越来越多的关注(react中的高阶组件是用高阶函数来实现的,高阶函数就是函数式编程的一个特性,react中的高阶组件是用高阶函数来实现的但不是纯函数的,另外react的一些生态,如redux他使用了函数式编程的思想)。
  • Vue3也开始拥抱函数式编程,vue2的源码中也大量使用了高阶函数,这些流行框架都在趋向于函数式编程
  • 函数式编程可以抛弃this
  • 打包过程中可以更好的利用tree shaking 过滤无用代码
  • 方便测试、方便并行处理
  • 有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda

二、什么是函数式编程(Functional Programming,FP)

FP是编程范式之一,我们常说的编程范式还有面向对象编程、面向过程编程。

  • 面向对象编程思维方式:把现实世界的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的练习
  • 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
    • 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中涉及很多输入或输出的函数
    • x->f(联系、映射)->y,y=f(x)
    • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数映射关系,如:y=sin(x),x和y的关系
    • 相同的输入始终要得到相同的输出(纯函数)
    • 函数式编程用来描述数据(函数)之间的映射,对运算过程的抽象

举个例子:

****非函数式编程****
let num1 = 2, num2 = 3
let sum = num1 + num2
console.log(sum) //5 

****函数式编程****
function add(n1, n2) {
    return n1 + n2
}
let sum2 = add(2, 3)
console.log(sum2) //5 
复制代码

函数式编程好处:1.代码可以重用。2.是细粒度的,可以组成功能更强大的函数!

三、函数式编程的特性(纯函数、柯里化、函数组合等)

1. 函数相关基础

(1) 函数是一等公民

  • 原因

    1. 函数可以存储在变量中

      var hello = function() {
          console.log("Hello, Fundebug!");
      };
      console.log(typeof hello); // 打印 function,可知,hello变量的类型是"function"。
      复制代码
    2. 函数作为参数

      **** eg1****
      setTimeout(function() {
          console.log("Hello, Fundebug!");
      }, 1000);
      复制代码
      **** eg2****
      const glob = {
          index(posts) { return Views.index(posts) },
          show(posts) { return Views.index(posts) },
          create(posts) { return Db.index(posts) },
          update(posts) { return Db.index(posts) },
          destory(posts) { return Db.index(posts) }
      }
      复制代码

      关于eg2解释:有一个index方法内部又调用Views.index(posts)方法,并且把他的结果返回。注意,index方法和内部调用的方法有相同的形式,即他的参数和返回值是一样的,如果将来我们遇到一个函数包裹了另一个函数,而且他们的形式也是一样的话,我们就认为这是一样的函数,所以可以把以上代码进行精简。

      **** eg简化****
      const glob = {
          index:Views.index, //赋值的是方法本身,不是他的调用
          show:Views.index,
          create:Db.index,
          update:Db.index,
          destory:Db.index
      }
      复制代码
    3. 函数作为返回值

      function sayHello(message) {
          return function() {
              console.log(`Hello, ${message}`);
          };
      }
      复制代码

    也就是说,在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量

    例如,字符串在几乎所有编程语言中都是一等公民,字符串可以做为函数参数,字符串可以作为函数返回值,字符串也可以赋值给变量。

    对于各种编程语言来说,函数就不一定是一等公民了,比如Java 8之前的版本。

    对于JavaScript来说,函数可以赋值给变量,也可以作为函数参数,还可以作为函数返回值,因此JavaScript中函数是一等公民。

(2) 高阶函数(Higher-order function)

  • 原因
    1. 可以把函数作为参数传递给另一个函数
    ****模拟数组的forEach方法 遍历数组的每一个元素并且对每一个元素进行处理****
    
    function forEach(array, fn) {//参数fn,是为了根据不同处理而定
        for (let i = 0; i < array.length; i++) {
            fn(array[i])//调用fn处理数组中的每一个元素
        }
    }
    // 测试 --打印数组每一项
    let arr = [1, 2, 3, 4, 5]
    forEach(arr, function (item) {//item接收传入的每一项
        console.log(item)//1,2,3,4,5
    })
    复制代码
    // 模拟数组的filter方法 遍历数组的每一个元素并且把满足条件的存储并返回
    function filter(array, fn) {
        let res = []
        for (let i = 0; i < array.length; i++) {
            if (fn(array[i])) {
                res.push(array[i])
            }
        }
        return res
    }
    // 测试 --找到偶数的项
    let arr = [1, 2, 3, 4, 5]
    let r = filter(arr, function (item) { //因为是return出来的,所以要用一个变量接收
        return item % 2 === 0
    })
    console.log(r)//[2,4]
    复制代码

    好处:函数作为参数可以让函数更灵活,无需考虑函数内部实现。
    2. 可以把函数作为另一个函数的返回结果(用一个函数来生成一个函数)

    function makeFn() {
        let msg = "hello"
        return function () {
            console.log(msg)
        }
    }
    ****第一种调用方法****
    const fn = makeFn() //先接收这个返回的函数,再接下来调用
    fn()  //hello 
    
    ****第一种调用方法****
    makeFn()()  //hello
    复制代码
    ***once这一个函数只执行一次(场景:一个支付订单,一个用户无论点击多少次,都只支付一次)***
    function once(fn) {
        let done = false  //定义是否被执行过
        return function () {
            console.log(arguments) //Arguments [5, callee: ƒ, Symbol(Symbol.iterator): ƒ]
            if (!done) {
                done = true
                return fn.apply(this, arguments)//要获取用户从function传进来的参数,所以要获取function的参数arguments,最后返回结果成功还是失败
            }
        }
    }
    let pay = once(function (money) {
        console.log(`支付了${money}RMB`)
    })
    pay(5) //支付了5RMB
    pay(5) //--
    pay(5) //--
    pay(5) //--
    ***通过once生成了一个函数,而这个函数内部对fn的调用只有一次***
    复制代码
  • 意义:
    1. 抽象可以帮助我们屏蔽细节,只需要关注于我们的目标
    2. 高阶函数是用来抽象通用的问题
  • 常用的高阶函数
    1. forEach
    2. map
    3. filter
    4. every
    5. some
    6. find/findIndex
    7. reduce
    8. sort
    ***模拟map***
    const map = (array, fn) => {
        let results = []
        for (let value of array) {
            results.push(fn(value)) //把处理后的加入
        }
        return results
    }
    let arr = [1, 2, 3, 4]
    arr = map(arr, item => item * item)
    console.log(arr) //[1, 4, 9, 16]
    
    ***模拟every***
    const every = ((array, fn) => {
        let result = true
        for (let value of array) {
            result = fn(value)
            if (!result) {//当前元素不满足
                break
            }
        }
        return result
    })
    let arr = [11, 12, 14]
    let r = every(arr, v => v > 10)
    console.log(r)
    
    ***模拟some***
    const some = ((array, fn) => {
        let result = false
        for (let value of array) {
            result = fn(value)
            if (result) {
                break
            }
        }
        return result
    })
    let arr = [9, 10, 11]
    let r = some(arr, v => v < 5)
    console.log(r)//false
    复制代码

(3) 闭包

  • 原因

函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包

  • 特点

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

  • 详细讲解链接

作用域+闭包

四、函数式编程的基础

(1) 纯函数(函数式编程中的函数指的就是纯函数)

  • 概念:相同的输入永远得到相同的输出,而且没有任何可观察的副作用

    • 纯函数就类似于数学中的函数(用来描述输入输出之间的关系),y=f(x)
    • lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
  • 纯函数和不纯函数的区别
    以数组的slice和splice为例,slice返回数组中的指定部分,不会改变原数组splice对数组进行操作返回该数组,会改变原数组

      **** slice ****
     let arr = [1, 2, 3, 4, 5]
     console.log(arr.slice(0, 3)) //[1, 2, 3]
     console.log(arr.slice(0, 3)) //[1, 2, 3]
     console.log(arr.slice(0, 3)) //[1, 2, 3]
     
     **** splice ****
     let arr2 = [1, 2, 3, 4, 5]
     console.log(arr.splice(0, 3)) //[1, 2, 3]
     console.log(arr.splice(0, 3)) //[4, 5]
     console.log(arr.splice(0, 3)) //[]
    复制代码

slice根据相同的输入得到相同的输出所以是纯函数,splice相同输入不同输出,所以不是纯函数

  • 自己编写一个简单的纯函数
function add(a, b) {
    console.log(a + b)
}
add(1, 2) //3
add(1, 2) //3
add(1, 2) //3
复制代码

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

(2) 函数式编程库 Lodash(纯函数的代表)

- 安装

    1. 浏览器环境: 

    `<script src="https://juejin.cn/post/lodash.js"></script>`  

    2. 通过 npm:  

    `$ npm i --save lodash`  

    3. Node.js:  

    `var _ = require('lodash');`
- 使用(first/last)
```
const array = ['jack', 'tom', 'lucy', 'kate']
console.log(_.first(array))//jake
console.log(_.last(array)) //kate
```
- 注意:Lodash的FP模块才是纯函数!
复制代码
  • 纯函数好处
    • 可缓存: 因为纯函数对相同的输入始终有相同的输出,所以可以把纯函数的结果缓存起来
    **** lodash 中 memoize 方法****
    function getArea(r) {
        return Math.PI * r * r
    }
    let res = _.memoize(getArea)
    console.log(res(4))
    
    **** 模拟 memoize ****
    function memoize(f) {
        let cache = {}//f对象参数作为cache的键名
        return function () {
            let key = JSON.stringify(arguments)
            cache[key] = cache[key] || f.apply(f, arguments)  //有计算过的值?把结果取回,没有值调用f去计算。调用f把参数传递进来!
            return cache[key]
        }
    }
    function getArea(r) {
        return Math.PI * r * r
    }
    let getM = memoize(getArea)
    console.log(getM(4))
    复制代码
    • 可缓存: 纯函数让测试更方便
    • 并行处理:在多线程环境下并行操作共享的内存数据很可能会出现意外情况,纯函数不需要访问共享的内存数据,所以在并行环境下我们可以任意运行纯函数(es6后js引入了多线程,要设置Web Worker!)
  • 纯函数副作用
     //不纯
     let min = 18
     function checkAge(age) {
         return age >= min
     }
     //纯函数(有硬编码,后续可以通过柯里化解决)
     function checkAge(age) {
         let min = 18
         return age >= min
     }
    
    复制代码
    • 副作用让一个函数变得不纯(如上),如果函数过度依赖外部状态就无法保证相同的输出,就会带来副作用。
    • 副作用来源
      • 配置文件
      • 数据库
      • 获取用户的输入

      所有的外部交互都有可能产生副作用,副作用也使得方法通用性下降不适合扩展和可重塑性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生。

(3) 柯里化

解决案例中硬编码问题

function checkAge(min, age) {
   return age >= min
}
console.log(checkAge(18, 20))
console.log(checkAge(18, 24))
复制代码
***柯里化 常写***
function checkAge(min) {
    return function (age) {
        return age >= min
    }
}
let cAge18 = checkAge(18)
let cAge20 = checkAge(20)
console.log(cAge18(20))
console.log(cAge18(24))

***柯里化 es6***
let checkAge = min => (age => age >= min)
复制代码
  • lodash中的柯里化函数–_.curry(func)
    • 功能:创建一个函数,该函数接收一个或者多个func的参数,如果func所需要的参数都被提供,则执行func并返回执行的结果,否则继续返回该函数被等待接收剩余的参数。
    • 参数: 需要柯里化的函数
    • 返回值:柯里化后的函数
    • 柯里化可以把多元函数转为一元函数(几个参数就是几元函数)
    • 使用
    function getSum(a, b, c) {
        return a + b + c
    }
    const curried = _.curry(getSum)
    console.log(curried(1, 2, 3))//6
    console.log(curried(1)(2, 3))//6
    console.log(curried(1, 2)(3))//6
    复制代码
  • 案例
    const match = _.curry(function (reg, str) {
        return str.match(reg)
    })
    const haveSpace = match(/\s+/g)
    console.log(haveSpace('hello world'))//[" "]
    复制代码
  • 实现原理
function curry(func) {
    return function curriedFn(...args) {
        if (args.length < func.length) {
            return function () {
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        return func(...args)
    }
}
function getSum(a, b, c) {
    return a + b + c
}
const curried = _.curry(getSum)
console.log(curried(1, 2, 3))//6
console.log(curried(1)(2, 3))//6
console.log(curried(1, 2)(3))//6
复制代码
  • 总结
    • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
    • 这是一种对函数参数的‘缓存’
    • 让函数变得更灵活,让函数的粒度更小
    • 可以把多元函数转成一元函数,可以组合使用函数产生强大的功能

(4) 函数组合

  • 纯函数和柯里化很容易写出洋葱代码h(g(f(x)))

获取数组的最后一个元素在转换成大写字母,.toUpper(.first(_.reverse(array)))

  • 函数组合好处:可以让我们把细粒度的函数重新组合成一个新的函数
  • 数据的管道:
    -a-> 管道(fn)-b->
    如果一个管道很长,我们可以拆分成多个管道(函数) fn=compose(f1,f2,f3)
  • 概念: 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
    – 函数就像数据的管道,函数组合就是把这些管道连接起来,让函数穿过多个管道形成最终结果
    函数组合默认是从右到左执行
  • 实现
    // 获取数组的最后一个元素为例
       function compose(f, g) {
           return function (value) {
               return f(g(value))
           }
       }
       function reverse(array) {
           return array.reverse()
       }
       function first(array) {
           return array[0]
       }
       const last = compose(first, reverse)
       console.log(last([1, 2, 3, 4])) //4
    复制代码
  • lodash中的组合函数 —flowRight()
    • lodash中的组合函数flow()和flowRight(),他们都可以组合多个函数
    • flow() 是从左到右运行
    • flowRight()从右向左运行,使用的更多一些
    • 举例子
      const reverse = arr => arr.reverse()
      const first = arr => arr[0]
      const toUpper = s => s.toUpperCase()
      const f = _.flowRight(toUpper, first, reverse)
      console.log(f(['one', 'two', 'three'])) //THREE
      复制代码
  • lodash中的组合函数 —flowRight()模拟实现
     const reverse = arr => arr.reverse()
     const first = arr => arr[0]
     const toUpper = s => s.toUpperCase()
    
     ***普通写法***
     function compose(...args) {
         return function (value) {
             return args.reverse().reduce(function (acc, fn) {//从后往前,所以参数反转;reduce 对数组中的每一个元素去执行一个由我们提供的函数,并将其汇总成一个单个的结果
                 //fn 是数组中的每一个参数,通过这个函数来处理传过来的value参数,处理完成把他返回。当处理第二个函数是,参数是上一次处理返回的结果
                 return fn(acc)
             }, value) //这里的value是第一次没有返回时,传入的
    
         }
     }
     
     ***es6写法***
     const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
     
     const f = compose(toUpper, first, reverse)
     console.log(f(['one', 'two', 'three'])) //THREE
    复制代码
  • 函数组合要满足结合律
    const f = compose(f,g,h)
    const f2 = compose(compose(f,g),h) == compose(f,compose(g,h))
    复制代码

    我们既可以把g和h进行组合,也可以把f和g进行组合,结果都是一样的

    const f = _.flowRight(_.toUpper, _.first, _.reverse)
    const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
    const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
    console.log(f(['one', 'two', 'three'])) //THREE
    复制代码
  • 如何调试组合函数
    ***将NEVER SAY DIE --> never-say-die***
     //_.split()   
       const split = _.curry((sep, str) => _.split(str, sep))
     //_.toLower()
     //_.join
       const join = _.curry((sep, array) => _.join(array, sep))
       const f = _.flowRight(join('-'), _.toLower, split(' ')) 
     //split应传入多个参数,curry将多个参数转为一个参数,使用函数柯里化,所以split只传递一个参数OK
       console.log(f('NEVER SAY DIE')) //n-e-v-e-r-,-s-a-y-,-d-i-e
    复制代码

    打印的不是我们想要的结果,那么如何找到底是哪一个函数出错了

      const log = v => {
         console.log(v)
         return v
      }//此处注意先定义后执行!!
    //当一个函数执行完毕,会把他的结果传递给下一个要处理函数
      const f = _.flowRight(join('-'), _.toLower, log, split(' '))
      console.log(f('NEVER SAY DIE')) //["NEVER", "SAY", "DIE"]
    //没问题
      const f = _.flowRight(join('-'), log, _.toLower, split(' '))
      console.log(f('NEVER SAY DIE')) //never,say,die 是字符串,但我们要数组
    //使用map
    // _.map
       const map = _.curry((fn, array) => _.map(array, fn))
       const f = _.flowRight(join('-'), map(_.toLower), split(' '))
       console.log(f('NEVER SAY DIE')) //never,say,die 是字符串我们要数组
    复制代码

    调试成功,但是有个问题,如果想一次多处调试就无法分辨具体哪个位置不对

      const f3 = _.flowRight(join('-'), log, map(_.toLower), log, split(' '))
      console.log(f3('NEVER SAY DIE')) //["NEVER", "SAY", "DIE"] ["never", "say", "die"]
    复制代码

    如何区分呢?

    const trace = _.curry((tag, v) => {
           console.log(tag, v)
           return v
    })
    const f= _.flowRight(join('-'), trace('toLower之后'), _.toLower, trace('split之后'), split(' '))
    console.log(f('NEVER SAY DIE')) //split之后 (3) ["NEVER", "SAY", "DIE"]  toLower之后 never,say,die
    复制代码

<1> lodash中的fp模块

  • lodash中的fp模块提供了实用的对函数式编程友好的方法
  • 提供了不可变 auto-cuuried (已经被柯里化的) iteratee-first(函数优先) data-last(数据滞后) 的方法
  • 使用

<script src="https://cdn.bootcdn.net/ajax/libs/lodash-fp/0.10.4/lodash-fp.js"></script>

*** 数据优先函数滞后 ***
_.map(['a','b','c'])
_.split('Hello world',' ')

*** 函数优先数据滞后 ***
fp.map(fp.toUpper,['a','b','c'])
fp.map(fp.toUpper)(['a','b','c'])
fp.split(' ','Hello world')
fp.split(' ')('Hello world')
复制代码
let _ = require('lodash')
let fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))//never-say-die
复制代码

<2> lodash-map方法的小问题

console.log(_.map(['23', '8', '10'], parseInt)) //[ 23, NaN, 2 ]
复制代码

map执行时候会遍历数组的每一个元素,并且把每一个元素都传递给parseInt进行处理
第一次,parseInt(’23’,0,array)
第二次,parseInt(‘8’,1,array)
第三次,parseInt(’10’,2,array)
parseInt()第二个参数是要解析的数字的基数,就是转成几进制,取值范围2~36,传0的话解析成10进制,所以parseInt(’23’,0,array)-》23。传0的话NAN。传2的话转为二进制2。
解决,自己封装一个parseInt 或者使用fp模块的map

console.log(fp.map(parseInt, ['23', '8', '10'])) //[ 23, 8, 10 ]
复制代码

lodash的maplodash/fp的map区别:前者parseInt接收3个参数,后者每次只接收一个参数。

<3> PointeFree

  • 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
    • 不需要指明需要处理的数据
    • 只需要合成运算过程
    • 需要定义一些辅助的基本运算函数
  • 例子
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
复制代码
  • 案例(1)
***非PointeFree模式**
function f(word) {
    return word.toLowerCase().replace(/\s/g, '_')
}
***PointeFree模式**
const f1 = fp.flowRight(fp.replace(/\s/g, '_'), fp.toLower)
console.log(f1('Hello world'))//hello_world
复制代码

函数式编程核心就是把运算过程抽象成函数,而PointeFree模式就是把抽象出来的函数再合成新的函数。这个合成的过程其实又是一个抽象的过程。

  • 案例(2)
// 把一个字符串中的首字母提取并转换成大写,使用.作为分隔符 (world wild web)-> W.W.W
// 提取首字母 转成大写 
// 使用空格切割字符串 每一项转大写 提取首字母 最后用.分割
const f = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split((' ')))
console.log(f('world wild web'))//W. W. W
复制代码

上面循环了两次,使用了两次map,改善

const f = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split((' ')))
console.log(f('world wild web'))//W. W. W
复制代码

五、函子-Functor

  • 为什么要学习函子

函数式编程会产生副作用,函子帮助我们把副作用控制在可控范围内、还能处理异常、进行异步操作等等

  • 什么是Functor
    • 容器:包含值和值的变形关系(这个变形关系就是函数)
    • 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)(可以把它想象成一个盒子,这个盒子对外要公布一个方法(map))
  • 示例
class Container {
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return new Container(fn(this._value))
    }
}
let r = new Container(5).map(x => x + 1).map(x => x * x)
console.log(r)//Container { _value: 36 }
复制代码

函子里面要维护一个值,这个值不对外公布,函子对外提供一个map方法,map方法要去接收一个处理值的函数,当我们调用map方法的时候,它会去调用fn去处理这个值,并且把处理的结果传递给一个新的函子对象,让新的函子对象来保存处理值。
map返回的不是一个值,是一个新的函子对象,在这个新的函子对象里面保存新的值,我们始终不把值对外公布,我们想要去处理值的话,我们就要给map对象传递一个处理值的函数。

*** 避免new 关键字 ***
class Container {
    static of(value) {//of 方法的作用就是给我们返回一个函子对象
        return new Container(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return Container.of(fn(this._value))
    }
}
let r = Container.of(5).map(x => x + 1).map(x => x * x)
console.log(r) //Container { _value: 36 }

复制代码
  • 总结
    • 函数式编程的运算不直接操作值,而是由函子完成
    • 函子就是一个实现了map契约的对象
    • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
    • 想要处理盒子中的值,我们需要给盒子的map方法传递一个值的函数(纯函数),由这个函数来对值进行处理
    • 最终map方法返回一个包含新值的盒子(函子)
  • 可能出现的副作用
    当我们要传入的值不小心变成了null或者undefined,结果就报错了,不符合纯函数的定义

    class Container {
        static of(value) {//of 方法的作用就是给我们返回一个函子对象
            return new Container(value)
        }
        constructor(value) {
            this._value = value
        }
        map(fn) {
            return Container.of(fn(this._value))
        }
    }
    let r = Container.of(null).map(x => x.toUpperCase())
    console.log(r) 
    复制代码

<1> MayBe 函子

我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理,MayBe函子的作用就是可以对外部的空值情况作为处理(控制副作用在允许的范围)

class MayBe {
   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 r2 = MayBe.of(null).map(x => x.toUpperCase())
console.log(r2) //MayBe { _value: null }

let r3 = MayBe.of('Hello world').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' '))
console.log(r3) //MayBe { _value: null }
复制代码

上面的r3虽然没有报错,但是无法找到具体是哪个位置传入了null

<2> 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 r2 = Left.of(12).map(x => x + 2)
     console.log(r1) //Right { _value: 14 }
     console.log(r2) //Left { _value: 12 }
     
     //验证
     function parseJSON(str) {
         try {
             return Right.of(JSON.parse(str))
         } catch (e) {
             return Left.of({ error: e.message })
         }
     }
     let r3 = parseJSON('{name:zs}')
     console.log(r3)//Left {_value: { error: 'Unexpected token n in JSON at position 1' } }
     let r4 = parseJSON('{"name":"zs"}')
     console.log(r4)//Right { _value: { name: 'zs' } }
    
     let r5 = parseJSON('{"name":"zs"}').map(x => x.name.toUpperCase())
     console.log(r5)//Right { _value: 'ZS' }
    复制代码

Either函子可以处理异常,并且记录错误信息

<3> IO函子(input output)

  • IO函子中的_value是一个函数,这里是把函数作为值来处理
  • IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包裹当前的操作
  • 把不纯的操作交给调用者来处理
const fp = require('lodash/fp')
class IO {
   static of(value) {//of 接收的是一个数据,返回一个IO函子
       return new IO(function () {//IO函子内部通过一个函数把这个值包裹起来了
           return value
       })
   }
   constructor(fn) {//与之前不一样,这里传入了一个函数 
       this._value = fn //将fn保存在value上
   }
   map(fn) {
       //把当前的value和传入的fn组合成一个新的函数
       return new IO(fp.flowRight(fn, this._value))
       //map方法先用flowRight把我们创建的function(){return value}函数,组合上我们传入的新的函数,然后返回一个IO函子
   }
}
let m = IO.of(process).map(p => p.execPath)//map方法里传入了新的函数
console.log(m) //IO { _value: [Function] }  IO一直是纯的
console.log(m._value()) ///usr/local/bin/node  _value可能是不纯的
复制代码

<4> Folktale

  • 异步任务的实现过于复杂,我们使用folktale中的Task来演示
  • folktale一个标准的函数式编程库

和lodash、ramda不同的是,他没有提供很多功能函数,只提供了一些函数式处理操作,例如:compose、curry等,一些函子Task、Either、MayBe等

  • compose
    const { compose, curry } = require('folktale/core/lambda')
    const { toUpper, first } = require('lodash/fp')
    let f = curry(2, (x, y) => {
       return x + y
    })
    console.log(f(1, 2)) //3
    console.log(f(1)(2)) //3
    let f2 = compose(toUpper, first)
    console.log(f2(['one', 'two']))//ONE
    复制代码

<5> Task–处理异步任务

 ```
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
function readFile(filename) {
    return task(resolver => {
        fs.readFile(filename, 'utf-8', (err, data) => {
            if (err) resolver.reject(err)

            resolver.resolve(data)
        })
    })
}
readFile('../package.json').map(split('\n')).map(find(x => x.includes('version'))).run().listen({
    onRejected: err => {
        console.log(err)
    }, onResolved: value => {
        console.log(value)
    }
})//  "version": "1.0.0",
 ```
复制代码

<6> Pointed 函子

  - Pointed 函子是`实现了of静态方法的函子`
  - of方法是为了避免使用new创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放在容器中,使用map来处理)
  ```
  class Container {
    static of(value) {
        return new Container(value)
    }
        ...
  }
  Container.of(2).map(x=>x+5)
  ```
复制代码
  • IO函子的问题
 let fp = require('lodash/fp')
 const fs = require('fs')
 class IO {
     static of(value) {//of 接收的是一个数据,返回一个IO函子
         return new IO(function () {//IO函子内部通过一个函数把这个值包裹起来了
             return value
         })
     }
     constructor(fn) {//与之前不一样,这里传入了一个函数 
         this._value = fn //将fn保存在value上
     }
     map(fn) {
         //把当前的value和传入的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))
 let r = cat('../package.json')
 console.log(r) //IO { _value: [Function] }
 let r2 = cat('../package.json')._value()._value()
 console.log(r2) //{"name": "ts-study",...}
复制代码
._value()._value()函子嵌套了
复制代码

<7> Monad函子

   - Monad函子是可以变扁的Ponited函子,IO(IO(x))
   - 一个函子如何具有join和of两个方法并遵守一些定律就是一个Monad
   
复制代码
 let fp = require('lodash/fp')
 const fs = require('fs')
 class IO {
     static of(value) {//of 接收的是一个数据,返回一个IO函子
         return new IO(function () {//IO函子内部通过一个函数把这个值包裹起来了
             return value
         })
     }
     constructor(fn) {//与之前不一样,这里传入了一个函数 
         this._value = fn //将fn保存在value上
     }
     map(fn) {
         //把当前的value和传入的fn组合成一个新的函数
         return new IO(fp.flowRight(fn, this._value))
     }
     join() {
         return this._value()
     }
     flatMap(fn) {
         return this.map(fn).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)//{"name": "ts-study",...}
 
 let r2 = readFile('../package.json').map((x => x.toUpperCase())).flatMap(print).join()
 console.log(r2)//{"NAME": "TS-STUDY",,...}
 
 let r3 = readFile('../package.json').map((x => fp.toUpper)).flatMap(print).join()
 console.log(r3)//{"NAME": "TS-STUDY",,...}
复制代码
调用方式直观且有可读性。Monad具有静态IO和join方法的函子。什么时候使用Monad?当一个函数返回一个函子的时候我们就要想到Monad,Monad可以帮我们解决函子嵌套的问题。当我们想要合并一个函数,并且这个函数返回一个值,这个时候我们可以调用map方法,当我们想要去合并一个函数这个函数返回一个函子,这个时候调用flatMap方法。
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享