“撬开函数式编程的门”

函数式编程

函数式编程其实是一个非常古老的概念,甚至早于计算机的出生,那我们为啥现在还要学习函数式编程呢,现在很多框架也在拥抱函数式编程,比如说React、Vue3,学习函数式编程可以抛弃那个让我们究极头疼的this,有没有眼前一亮的感觉,它也可以在打包的过程中更好的利用 tree shaking 过滤无用的代码、方便测试和并行处理

应用类库:lodash、underscore、ramda 函数式开发库
应用工具:nodemon 执行并监听 js 代码执行

什么是函数式编程

函数式编程(Functional Programming, FP), FP 是编程规范之一,我们常听说的编程范式还有面向过程编程、面向对象编程。

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

    • 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多输入和输出的函数
    • x -> y (联系、映射) -> y, y = f(x)
    • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y = sin(x), x 和 y 的关系
    • 相同的输入始终要得到相同的输出(纯函数)
    • 函数式编程用来描述数据(函数)之间的映射

非函数式编程

let num1 = 2;
let num2 = 3;
let sum = num1 + num2;
console.log(sum)

函数式-抽象了运算的过程

  function add (n1, n2) {
    return n1 + n2;
  }
  let sum = add(2, 3)
  console.log(sum)

前置知识

  • 函数是头等函数
  • 高阶函数
  • 闭包

函数是头等函数

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

在 JavaScript 中函数就是一个普通的对象(可以通过 new Function()),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,
甚至我们可以在运行程序的时候通过 new Function('alert(1)') 来构造一个新的函数。

  • 把函数赋值给变量

    // 把函数赋值给变量
    let fn = function () {
    console.log('Hello First-class Function')
    }
    fn()
    
    // 一个示例
    const BlogController = {
    index (posts) { return Views.index(posts) },
    show (posts) { return Views.show(posts) },
    create (attrs) { return Db.create(attrs) },
    update (posts, attrs) { return Db.update(posts, attrs) },
    destroy (posts) { return Db.destroy(posts) },
    }
    
    // 优化
    const BlogController = {
    index: Views.index,
    show: Views.show,
    create: Db.create,
    update: Db.update,
    destroy: Db.destroy,
    }

高阶函数

什么是高阶函数

  • 高阶函数(Higher-order function) HOF

    • 可以把函数作为参数传递给另个一个函数
    • 可以把函数作为另一个函数的返回结果
      -函数作为参数

      // 模拟 forEach
      function forEach (array, fn) {
        for(let i = 0; i < array.length; i++) {
          fn(array[i]);
        }
      }
      
      let array = [1, 2, 3, 4, 5, 6];
      
      forEach(array, item => {
        console.log(item)
      })
      
      // 模拟 filter
      function filter(array, fn) {
        let result = [];
        for(let i = 0; i < array.length; i++ ) {
          if(fn(array[i])) {
            result.push(array[i]);
          } 
        }
        return result;
      }
      
      let newArray = filter(array, item => {
        return item % 2 === 0;
      })
      
      console.log(newArray)
  • 作为返回值

     // once 只执行一次的函数,当前使用场景为支付
    function once (fn) {
      let done = false
      return function (...rest) {
        if(!done) {
          done = true
          fn.apply(this, rest)
        }
      }
    }
    
    let pay = once(function (money) {
      console.log(`我支付了¥${money} RMB`)
    })
    
    pay(5)
    pay(5)
    pay(5)
    
  • 高阶函数的意义

    • 抽象可以帮助我们屏蔽细节,只需要关注于我们的目标
    • 高阶函数是用来抽象通用的问题

      // 面向过程的方式
      let array = [1, 2, 3, 4, 5]
      for(let i = 0; i < array.length; i ++) {
        console.log(array[i])
      }
      
      // 高阶函数
      let array = [1, 2, 3, 4, 5]
      forEach(array, function (item) {
        console.log(item)
      })
      
      let r = filter(array, item => {
          item % 2 === 0
      })
      
  • 常用的高阶函数

    • 数组方法

      • forEach、map、filter、every、some、find/finedIndex、reduce、sort...

        // 模拟 map 方法
        const map = (array, fn) => {
         let results = []
         for (const value of array) {
           results.push(fn(value))
         }
         return results;
        }
        let arr = [1, 2, 3, 4, 5]
        arr = map(arr, item => item * item)
        console.log(arr)
        
        // every 是否都匹配指定的条件
        const every = (array,fn) => {
         let status = true
         let i = 0;
        for (const item of array) {
          if(!fn(item)) {
            status = false
            break
          }
        }
         return status;
        }
        
        let array = [1, 2, 3, 4, 5]
        let state = every(array, item => item > 10)
        // console.log(state)
        
        // some 数组中是否有一个满足指定的条件
        const some = (array, fn) => {
        let status = false
        for (const item of array) {
          if(fn(item)) {
            status = true;
            break
          }
        }
        return status
        }
        
        let array = [1, 5, 3,7, 15]
        let state = some(array, item => item % 2 === 0)
        console.log(state)

闭包

  • 闭包(Closure): 函数和其周围的状态(语法环境)的引用捆绑在一起形成的闭包。
  • 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员。

    // 函数作为返回值
    function makeFn() {
    let msg = 'Hello function'
    return function () {
      console.log(msg)
    }
    }
    
    const fn = makeFn()
    fn()
    
    // once 只被执行一次
    let once = (fn) => {
    let done = false
    return function (...rest) {
      if(!done) {
        done = true
        fn.apply(this, rest)
      }
    }
    }
    let pay = once(money => {
    console.log(`成功支付¥${money} RMB`)
    })
    pay(5);
    pay(5);
    pay(5);
    
  • 闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移出,但是栈上的作用域成员因为被外部引用,所以不能释放,因此内部函数依然可以访问外部函数的成员

纯函数 Pure functions

  • 相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
  • 纯函数就是类数学中的函数(用来描述输入和输出之间的关系),y = f(x)
  • lodash 是一个纯函数的功能库,提供了对数组、数字、字符串和函数等操作的一些方法
  • 数组中的 slicesplice 分别是纯函数和非纯函数

    • slice 返回数组中指定的部分,不会改变原数组
    • splice 对数组进行操作返回该数组,会改变原数组

      let arr = [1,2,3,4,5,6]
      //纯函数
      console.log(arr.slice(0, 2)) // [ 1, 2 ]
      console.log(arr.slice(0, 2)) // [ 1, 2 ]
      console.log(arr.slice(0, 2)) // [ 1, 2 ]
      
      //非纯函数
      console.log(arr.splice(0, 2)) // [ 1, 2 ]
      console.log(arr.splice(0, 2)) // [ 3, 4 ]
      console.log(arr.splice(0, 2)) // [ 5, 6 ]
  • 函数式编程不会保留计算中间的结果,所以变量是不可以变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理

Lodash

// 演示 lodash - first / last / toUpper / reverse / each / includes / find / findIndex
const _ = require('lodash')
const array = ['jack', 'tom', 'rose', 'kate']
console.log(_.first(array))
console.log(_.last(array))
console.log(_.toUpper(_.first(array)))
console.log(_.reverse(array))
console.log(_.each(array, (item, index) => {
  console.log(item, index)
}))
console.log(_.includes(array, 'jack'))
// console.log(_.find(array, 'jack'))
// console.log(_.includes(array, 'jack'))

纯函数的优势

  • 可缓存:

    • 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来

      const _ = require('lodash')
      
      function getArea (r) {
        console.log(r)
        return Math.PI * r * r
      }
      let getAreaWithMemory = _.memoize(getArea)
      console.log(getAreaWithMemory(4))
      console.log(getAreaWithMemory(4))
      console.log(getAreaWithMemory(4))
      
      /**
       4 只打印了一次,第一次之后直接从缓存中读取
       50.26548245743669
       50.26548245743669
       50.26548245743669
       */
      • 模拟 memoize 的实现

        function memoize(fn) {
        let cache = {}
        return function (...area) {
          let key = `${area}`
          cache[key] = cache[key] || fn(...area)
          return cache[key]
        }
        }
        function getArea (r) {
        console.log(r)
        return Math.PI * r * r
        }
        let getAreaWithMemory = memoize(getArea)
        console.log(getAreaWithMemory(4))
        console.log(getAreaWithMemory(4))
        console.log(getAreaWithMemory(4))
  • 可测试:

    • 纯函数让测试更方便
  • 并行处理:

    • 在多线程环境下并行操作共享的内存数据很可能出现意外情况
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker 可以开启多线程)

函数的副作用

  • 纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

    // 非纯函数
    let min = 18 // mini 是外部可变的,所以影响了纯函数的原则
    function checkAge (age) {
    return age >= min;
    }
    
    // 纯函数(有硬编码,后续可以通过柯里化解决)
    function checkAge (age) {
    let min = 18
    return age >= min
    }

    副作用让一个函数变得不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖外部状态就无法保证相同输出,就会带来副作用

大部分的副作用来源于:

  • 配置文件
  • 数据库
  • 获取用户输入
  • ......
    所有的外部交互都有可能代理副作用,副作用也使得方法的通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患和不确定性

柯里化 Haskell Brooks Curry

使用柯里化解决程序中硬编码问题

function checkAge (age) {
  let min = 18
  return age >= min
}

// 普通的纯函数
function checkAge(min, age) {
  return age >= min
}

console.log(checkAge(18, 20))

// 柯里化纯函数
function checkAge (min) {
  return function (age) {
    return age >= min
  }
}

//  柯里化ES6写法
let checkAge = min => (age => age >= min)

const checkAge18 = checkAge(18)
console.log(checkAge18(20))
const checkAge20 = checkAge(20)
console.log(checkAge20(18))
  • 柯里化

    • 当函数有多个参数需要传递的时候先传递一部分调用它(这部分参数以后永远不变)
    • 然后返回一个新函数接收剩余参数,返回结果

lodash 中的柯里化

  • _.curry(func)

    • 功能:创建一个函数,该函数接收一个或多个func的参数,如果 func 所需参数都被传入,则会立即执行并返回结果。如果传递的参数这是 func 所需参数的一部分,那它会返回一个函数用来接收剩余参数
    • 参数:需要柯里化的函数
    • 返回值:柯里化后的函数
let getSum = (a, b, c) => {
  return a + b + c
}

let getSumCurried = _.curry(getSum)
console.log(getSumCurried(1,2,3))  // 传递所有参数时,会立即调用并返回结果
console.log(getSumCurried(1)(2,3)) // 如果传递的并不是该函数的所有参数,那它会返回一个函数等待接收剩下的参数
console.log(getSumCurried(1,2)(3))
console.log(getSumCurried(1)(2)(3))
  • 学习案例

    console.log(''.match(/\+/g))
    
    let match = _.curry(function (reg, str) {
    return str.match(reg)
    })
    
    const haveSpace = match(/\s+/g)
    const haveNumber = match(/\d+/g)
    
    console.log(haveSpace('hello world'))
    console.log(haveNumber('123abc'))
    
    const filter = _.curry(function (fn, array) {
    return array.filter(fn)
    })
    
    let haveSpaceFn = filter(haveSpace);
    console.log(haveSpaceFn(['gu1 yun', 'ho_wei']))
  • 总结
    柯里化的操是将函数的参数先传递一本分给函数,剩下的参数传递给返回的参数

    const haveSpace = match(/\s+/g)
    const haveNumber = match(/\d+/g)
    
    // 自己通过柯里化改造函数
    const filter = function (fn) {
    // fn: 使用的工具函数
    return function (array) {
      // array: 需要使用到的数据
      return array.filter(fn)
    }
    }
    
    const haveSpaceCurry = filter(haveSpace)
    // ES6
    const filter = fn => (array => array.filter(fn))
    
    // lodash 提供的柯里化方法
    const filter = _.curry(function (fn, array) {
    return array.filter(fn)
    })
    // lodash 的 curry 方法可以吧一个普通的函数编程柯里化函数
    // 最终的目标是将函数变成一元函数

柯里化实现原理

模拟实现 lodash 中的 curry 方法

function getSum (a, b, c) {
  return a + b + c
}

function curry (fn) {
  return function curriedFn (...args) {
    if(args.length < fn.length ) {
      return function () {
        return curriedFn(...args.concat(Array.form(arguments)))        
      }
    }
    return fn(...args)
  }
}

const curried = curry(getSum)

console.log(curried(1,2,3))

柯里化总结

  • 柯里化可以我们给一个函数传递较少的参数,得到一个已经记录了某个固定参数的新函数
  • 这是一种函数对参数的缓存
  • 让函数变得更灵活,让函数的颗粒度更小
  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

函数组合

使用纯函数和柯里化很容易写出洋葱代码 a(b(c(d)))

  • 获取数组的最后一个元素再转换成大写字母 _.toUpper(_.first(_.reverse(array)))
    函数组合可以让我们把细粒度的函数重新组合成一个新的函数

管道

下面这张图表示程序中使用函数处理数据的过程,给 n 函数输入参数 a,返回结果 b,可以想想 a 通过一个管道得到了 b 的数据
img.png
当fn比较复杂的时候,我们可以把函数 fn 拆分成多个小函数,此时多了中间运算过程产生的 m 和 n。

下面这张图可以想象成把 fn 这个管道拆分成3个管道 f1 f2 f3,数据 a 通过管道 f3 得到结果 m,m 再通过管道 f2 得到结果 n,n 通过管道 f1 得到最终结果 b
img_1.png

fn = compose(f1, f2, f3)
b = fn(a)

commpose 就相当于是一个管道,它把三个函数组合成一个函数fn,调用 fn 返回结果b,实际上这个数据是由f1、f2、f3三个函数处理的,在它们处理时就会产生m、n等结果,对于这些结果我们并不需要关心

函数组合

  • 函数组合 (Compose): 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的处理函数合并成一个函数

    • 函数就是处理数据的管道,函数组合就是把这些管道链接,让数据穿过多个管道得到最终的结果
    • 函数组合默认是从右到左执行

      //  函数组合
      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 中的组合函数

lodash 中组合函数 flow() 或者 flowRight(),它们都可以组合多个函数

  • flow()从左到右执行
  • flowRight()从右到左执行(常用)

    const _ = require('lodash')
    
    let arr = ['a', 'b', 'c', 'd']
    
    const reverse = array => array.reverse()
    
    const first = array => array[0]
    
    const toUpper = value =>  value.toUpperCase()
    
    const f = _.flowRight(toUpper, first, reverse)
    
    console.log(f(arr)) // D

组合函数的实现原理

const _ = require('lodash')

let arr = ['a', 'b', 'c', 'd', 'e']

const reverse = array => array.reverse()
const first = array => array[0]
const toUpper = value =>  value.toUpperCase()

// 使用 lodash 中的组合函数
const f = _.flowRight(toUpper, first, reverse)

// --------------------------------------------------------------------

// 自定义组合函数
function compose (...args) {
  return function (value) {
    return args.reverse().reduce((count, fn) => fn(count), value)
  }
}
// es6 版本
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)

const f = compose(toUpper, first, reverse)

console.log(f(arr)) // E

组合律

函数组合要满足组合率(associativity),我们可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的

// 组合律 (associativity)
let f = compose(f, g, h)
let associactive = compose(compose(f, g), h) == compose(f, compose(g, h)) // true

// --------------------------------------------------------------------

const _ = require('lodash')

let arr = ['a', 'b', 'c', 'd', 'e']

const reverse = array => array.reverse()

const first = array => array[0]

const toUpper = value =>  value.toUpperCase()

// const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))

console.log(f(arr ))

调试

辅助函数来调试打印组合函数


// 将 NEVER SAY DIE 处理为 never-say-die
const _ = require('lodash')

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

const trace = _.curry((tag, v) => {
  console.log(tag, v)
  return v
})

// 通过封装将这些函数变为一元函数
const split = _.curry((sep, str) => _.split(str, sep))
const map = _.curry((fn, array) => _.map(array, fn))
const join = _.curry((sep, array) => _.join(array, sep))
// 使用 log 辅助函数将当前进度时的值进行打印,但有一个问题是不能区分打印的具体位置
// const compose = _.flowRight(join('-'),log, map(_.toLower),log, split(' '))
const compose = _.flowRight(join('-'),trace('这是 map 之后'), map(_.toLower),trace('这是 split 之后'), split(' '))

console.log(compose('NEVER SAY DIE'))

Lodash 中的 FP(Functional Programming) 模块

lodash/fp

  • lodash 的 fp 模块提供了实用的对函数式编程的友好的方法
  • 提供了不可变的、已经被柯里化的且具有auto-curried、iteratee-first、data-list等特点的方法

    
    // lodash 模块中的方法和 FP 模块中方法的区别
    const array = ['a', 'b', 'c']
    //  lodash - 数据优先,回调、操作往后排
    const _ = require('lodash')
    _.map(array, _.toUpper) // [ 'A', 'B', 'C' ]
    _.map(array) // [ 'a', 'b', 'c' ]
    _.split('hello world', ' ') // [ 'hello', 'world' ]
    
    // lodash/fp 模块 - 方法优先,已经是被柯里化的函数
    const fp = require('lodash/fp')
    
    fp.map(fp.toUpper, array)
    fp.map(fp.toUpper)(array)
    
    fp.split(' ', 'hello world')
    fp.split(' ')('hello world')

lodash 和 lodash/fp 模块中 map 方法的区别

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

  const array = ['23', '8', '10']

  console.log(_.map(array, parseInt)) // [ 23, NaN, 2 ]
  // lodash 中 map 方法的parseInt回调会要求三个参数:当前处理元素:'23' 下标:'0' 集合:array

  const fp = require('lodash/fp')
  console.log(fp.map(parseInt, array)) // [ 23, 8, 10 ]
  // fp 模块中的 map 方法的parseInt回调只要求一个当前处理元素的参数

PointFree

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

  • 合并时不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数

    // Point Free 模式其实就是函数的组合
    const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))

来吧,展示

// 把一个字符串中的首字母提取并转换为大写,使用"."作为分隔符号
// world wild web ==> W.W.W
const fp = require('lodash/fp')
// const firstLetterToUpper = fp.flowRight(fp.join('.'), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' ')) // 遍历两次
const firstLetterToUpper = fp.flowRight(fp.join('.'), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(firstLetterToUpper('world wild web'))

函子

函子的作用是帮助我们将副作用控制在可控的范围内、异常处理、异步操作等。

Functor 函子

什么是 functor

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

    class Container {
    constructor (value) {
    this._value = value
    }
    static of (value) {
    return new Container(value)
    }
    map (fn) {
    return Container.of(fn(this._value))
    }
    }
    let r = Container.of(5)
    .map(x => x + 2)
    .map(x => x * x)
    
    console.log(r)
    
    let upper = Container.of(null)
    .map(x => x.toUpperCase()) // 会报错,函子处理 null/undefined 等空值是会报错,需要使用 MayBe 函子来处理(下个小节)
    
  • 总结

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

MayBe 函子

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

class MayBe {
    constructor(value) {
      this._value = value
    }

    static of (value) {
      return new MayBe(value)
    }

    isNothing () {
      return this._value === null || this._value === undefined
    }

    map(fn) {
      return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }
  }

  // let r = MayBe.of('hello')
  //   .map(x => x.toUpperCase())
  // console.log(r) // 正常执行

  let r = MayBe.of(null)
    .map(x => x.toUpperCase())

  console.log(r) // MayBe { _value: null } 传递 null 也不会报错

  // 
  let r = MayBe.of('hello')
    .map(x => x.toUpperCase())
    .map(x => null)
    .map(x => x + ' world')
  console.log(r) // MayBe { _value: null }

  // 返回值为null

但其实还存在一个问题,当我们调用多个函子时,如果其中某一个位置发生了异常,是没有办法精确捕捉到的

class MayBe {
    constructor(value) {
      this._value = value
    }

    static of (value) {
      return new MayBe(value)
    }

    isNothing () {
      return this._value === null || this._value === undefined
    }

    map(fn) {
      return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }
  }

  let r = MayBe.of('hello')
    .map(x => x.toUpperCase())
    .map(x => null)
    .map(x => x + ' world')
    console.log(r) // MayBe { _value: null }

面对这种情况还是有解决的方法的

Either

Either 是两个其中的任何一个,类似于 if...else... 的处理,异常会让函数变得不纯,Either 函数可以用来做异常处理

来吧,展示

//定义一个处理异常的函子
class Mistake {
  static of (value) {
    return new Mistake(value)
  }

  constructor(value) {
    this._value = value
  }

  map (fn) {
    // 这里同于以往函子的的 map 方法
    return this
  }
}

// 定义一个正常执行的函子
class Right {
  static of (value) {
    return new Right(value)
  }
  constructor(value) {
    this._value = value
  }

  map(fn) {
    return  new Right(fn(this._value))
  }
}

// 通过 formatJSON 方法调用这两个函子
function formatJSON (json) {
  try {
    return Right.of(JSON.parse(json))
  } catch (e) {
    return Mistake.of({error: e.message})
  }
}

// 传递错误参数,捕捉到异常,返回了错误信息
// let r = formatJSON('{name: zs}')
// console.log(r) // Mistake { _value: { error: 'Unexpected token n in JSON at position 1' }}

// 正常执行
let r = formatJSON('{"name":"zs"}')
// console.log(r) // Right { _value: { name: 'zs' } }
.map(x => x.name.toUpperCase())
// console.log(r) // Right { _value: 'ZS' }

IO 函子

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

const fp = require('lodash/fp')

class IO {
  static of = (value) => new IO(() => value)

  constructor(fn) {
    this._value = fn
  }

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

let r = IO.of(process).map(p => p.execPath)

console.log(r._value())

Task 异步任务

异步任务实现比较复杂,这里使用 floktale 中的 Task 演示,floktale 是一个标准的函数式编程库,和 lodash、ramda不同的是它没有提供很多功能函数,只提供了一些函数处理操作,例如:compose、curry等,一些函子 `Task、Either、MayBe等

下面先演示一下 floktale 中的 compose、curry

const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')

// 第一个参数是要表明回调函数的参数个数,文档上讲是为了避免一些错误
let f = curry(2, (x, y) => x + y)

// console.log(f(1,2))
// console.log(f(1)(3))

let c = compose(toUpper, first)
console.log(c(['one', 'two']))
  • Task 异步执行

    • folktale(2.3.2) 2.x 中的 Task 和 1.0 中的区别还是挺大的,1.0中的用法更接近我们现在演示的函子,这里使用 2.3.2 来演示

      //  Task 函子
      const { task } = require('folktale/concurrency/task')
      const fs  = require('fs')
      const { split, find } = require('lodash/fp')
      
      // 返回一个 task 函子
      // const readFile = filename => task(resolver => {
      //   fs.readFile(filename, 'UTF-8', (err, data) => {
      //     if(err) resolver.reject(err)
      //     resolver.resolve(data)
      //   })
      // })
      // 读取 package.json 文件找到version
      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 方法读取文件
      .run()
      .listen({
        onRejected: err => console.log(err),
        onResolved: value => console.log(value)
      })

Pointed 函子

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

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

Monad (单)函子

  • monad 函子是可以变扁的 Pointed 函子, IO(IO(x))
  • 一个函子如果有 joinof 两个方法并遵守一些定律就是一个 Monad

来吧,最后一个展示

const fp = require('lodash/fp')
  const fs = require('fs')

  // IO Monad
  class IO {
    static of = (value) => new IO(() => value)

    constructor(fn) {
      this._value = fn
    }

    map (fn) {
      return new IO(fp.flowRight(fn, this._value))
    }
    // 使用 json 方法 把函子变扁拍平,也就是调用一次
    join () {
      return this._value()
    }

    flatMap (fn) {
      return this.map(fn).join()
    }
  }

  let readFile = filename => new IO(() => fs.readFileSync(filename, 'utf-8'))

  let print = x => new IO(() => {
    console.log(x)
    return x
  })

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

  console.log(r)

你可能感兴趣的