关于前端:撬开函数式编程的门

32次阅读

共计 15872 个字符,预计需要花费 40 分钟才能阅读完成。

函数式编程

函数式编程其实是一个十分古老的概念,甚至早于计算机的出世,那咱们为啥当初还要学习函数式编程呢,当初很多框架也在拥抱 函数式编程,比如说 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 的数据

当 fn 比较复杂的时候,咱们能够把函数 fn 拆分成多个小函数,此时多了两头运算过程产生的 m 和 n。

上面这张图能够设想成把 fn 这个管道拆分成 3 个管道 f1 f2 f3,数据 a 通过管道 f3 失去后果 m,m 再通过管道 f2 失去后果 n,n 通过管道 f1 失去最终后果 b

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)

正文完
 0