学习笔记——函数式编程、柯里化、函数组合、函子

240 阅读20分钟

点此直达总结:学习总结——函数式编程、柯里化、函数组合、函子

1. 函数式编程

函数式编程(Functional Programming, FP),是一种编程范式之一。

  • 函数式编程中的“函数”,不是程序中的函数or方法,是数学中的函数,即映射关系。如 y=sinx,x & y 的关系
  • 纯函数:相同的输入始终要有相同的输出
  • 函数式编程描述的是数据(函数)之间的映射
  • 核心思想:对运算过程进行抽象,将运算过程抽象为函数,可重用
  • 函数式编程不会保留计算的中间结果,变量不可变(无状态)
  • 可以把一个函数的执行结果交给另一个函数处理

2. 函数是一等公民

2.1 函数可以存储在变量 or 数组中

const blogController = {
  index(posts) { return Views.index(posts) }
}
// 意义同上
const blogController = {
  index: Views.index
}

Q:const blogController = { index: () => Views.index() } 是否与上文意义相同?

2.2 函数可以作为参数

  • forEach

    function forEach(arr, fn) {
    	for (let i = 0, i < arr.length; i++) {
    		fn(arr[i]) // 函数处理数组项
    	}
    }
    // 调用
    forEach([1,2,3], item => console.log(item))
    
  • filter

    function filter(arr, fn) {
    	let result = []
    	for (let i = 0; i < arr.length; i++) {
    		if (fn(arr[i])) {
    			result.push(arr[i])
    		}
    	}
    	return result
    }
    // 调用
    filter([1,2,3], item => item % 2 === 0)
    

使函数更灵活;不需要考虑函数内部实现;函数名有实际意义

2.3 函数可以作为返回值

  • makefn

    function makefn() {
      const msg = 'Hello fn'
      return function () {
        console.log(msg)
      }
    }
    // 调用1
    const fn = makefn()
    fn()
    // 调用2:第二个括号为调用返回的函数
    makefn()()
    
  • once

    function once(fn) {
      let done = false // 标记函数是否已执行
      return function () {
        if (!done) {
          done = true
          // apply改变this,arguments为用户调用fn时传递的参数
          return fn.apply(this, arguments)
        }
      }
    }
    // 调用
    let pay = once(function (money) {
      console.log(`支付了${money}RMB`)
    })
    pay(5)
    pay(6)
    

    Q:done在函数内部,let 声明会有作用域问题,为什么once还是会被执行?

    A:这是闭包问题(可查看【4. 闭包】)。内部函数若使用了外部函数的成员,则外部函数的成员不会被释放,因此多次调用时,变量值不重置

3. 高阶函数

高阶函数(Higher order function)

  • 可将函数A作为函数B的参数
  • 可将函数A作为函数B的返回值

3.1 意义

  • 让我们不需要关注函数内部细节,只需要知道函数是做什么的
  • 使代码更简洁,更灵活,复用性强

3.2 常用的高阶函数

forEach map filter every some find findIndex reduce sort ...

  • map

    const map = (arr, fn) => {
      let result = []
      for (let val of arr) {
        result.push(fn(val))
      }
      return result 
    }
    // 调用
    let arr = map([1,2,3], v => v * v)
    console.log(arr)
    
  • every

    const every = (arr, fn) => {
      let flag = true
      for (let val of arr) {
        if (!fn(val)) {
          flag = false
          break
        }
      }
      return flag
    }
    // 调用
    let arr = every([1,2,3], v => v > 2)
    console.log(arr)
    
  • some

    const some = (arr, fn) => {
      let flag = false
      for (let val of arr) {
        flag = fn(val)
        if (flag) {
          break
        }
      }
      return flag
    }
    // 调用
    let arr = some([1,2,3], v => v > 2)
    console.log(arr)
    

4. 闭包

4.1 概念

闭包(Closure):函数和其周围状态的引用捆绑在一起 ,形成闭包。

在另一个作用域中调用一个函数的内部函数,并访问该函数作用域中的成员。其实是将外部函数中成员的作用域范围进行了延长。

// 普通函数:每次调用时,函数内部变量会被释放,每次调用都是新的
function makefn() {
  let msg = 'Hello fn'
}
const fn = makefn()
fn()

// 闭包函数
function makefn() {
  let msg = 'Hello fn'
  return function () {
    console.log(msg)
  }
}
// 调用之后,此时fn是makefn内部的函数,当外部对内部函数有引用时,内部函数的成员不能被释放
const fn = makefn()
// 调用fn时,即调用了内部的匿名函数,同时也访问了msg,延长了msg的作用域范围
fn()

4.2 本质

函数在执行的时候会放到一个执行栈上,当函数执行完毕会从执行栈上移除,内部变量也会从执行栈上移除。但是当堆上的作用域成员被外部成员引用,则不能被释放,因此内部函数依然可以访问外部函数的成员。

4.3 案例

4.3.1 求数字的幂

背景:求一个数的幂,可以使用 Math.pow(数字, 幂),若求平方,则总是需要传第二个参数为2,如 求4的平方可以用 Math.pow(4, 2),现需写一个函数让它可以生成一个可以求平方 or 求幂的函数,以不用传第二个参数。

// power 多少次幂,number对哪个数字求幂
function makePower(power) {
  return function (number) {
    return Math.pow(number, power)
  }
}
// 调用
let power2 = makePower(2) // 求平方
let power3 = makePower(3) // 求立方
console.log(power2(2)) // 求2的平方
console.log(power3(5)) // 求5的立方

可在调用时打断点,关注浏览器 Sources

  • Call Stack:函数调用栈

  • Scope:作用域

    • Script:通过 let 定义的变量;
    • Global:通过 var 定义的变量会挂在到全局对象
    • Closure(xx):闭包(闭包相关的外层函数)

4.3.2 求员工的薪资

背景:定义一个函数可求员工薪资,getSalary(基本薪资,绩效),假设级别A的员工工资为12k,级别B的员工为工资为13k,同级别的员工基本薪资相同,现需写一个函数,为不同级别的员工生成一个函数,避免相同的基本薪资重复。

// base 基本工资,performance 绩效
function makeSalary(base) {
  return function (performance) {
    return base + performance
  }
}
// 调用
let salaryLevel1 = makeSalary(12000) // 级别1的员工基本薪资为12000
let salaryLevel2 = makeSalary(13000) // 级别2的员工基本薪资为13000
console.log(salaryLevel1(2000)) // 已知员工的绩效,求级别1的员工总薪资
console.log(salaryLevel2(2000)) // 已知员工的绩效,求级别2的员工总薪资

5. 纯函数

5.1 概念

相同的输入永远会得到相同的输出,且没有可观察的副作用

  • slice:截取数组中的指定部分,不改变原数组<纯函数>

    let arr = [1, 2, 3, 4, 5]
    // start 数组索引(第一个),end 截取到哪一个索引,不包含end
    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 arr = [1, 2, 3, 4, 5]
    // start 数组索引(第一个),end 截取几个
    console.log(arr.splice(0, 3)) // [1, 2, 3],此时arr = [4, 5]
    console.log(arr.splice(0, 3)) // [4, 5]
    console.log(arr.splice(0, 3)) // []
    

5.2 Lodash

5.2.1 初始化 package.json

npm init -y
npm i lodash

5.2.2 方法示例

first last toUpper reverse each es6:includes find findIndex

const _ = require('lodash')
const arr = ['jack', 'tom', 'lucky']

console.log(_.fist(arr)) // 求第一个元素
console.log(_.last(arr)) // 求最后一个元素
console.log(_.toUpper(_.first(arr))) // 对第一个元素大写

// 数组使用翻转 reverse,可以直接用 arr.reverse(), 无参数且会改变原数组-非纯函数
console.log(arr.reverse())
console.log(_.reverse(arr))

const eachArr = _each(arr, (item, index) => {
  console.log(item, index)
})
console.log(eachArr)

5.3 优点

  • 可缓存:提高性能

    计算圆面积

    const _ = require('lodash')
    /**
     * 求圆面积
     * @param {number} r 圆的半径
     */
    function getArea(r) {
      console.log(r)
      return Math.PI * r * r
    }
    // memoize:接受一个纯函数为参数,将纯函数的结果缓存,返回有记忆功能的函数
    let getAreaWithMemory = _.memoize(getArea)
    console.log(getAreaWithMemory(4)) // 求半径为4的圆面积
    console.log(getAreaWithMemory(4)) // 多次调用,console.log(r) 只会打印一次
    

    模拟 memoize

    function memoize(fn) {
      // 用对象存储函数及其返回结果,key:函数参数,value:函数执行结果
      let cache = {}
      return function () {
        // 将匿名函数的参数,伪数组 arguments 处理成字符串,作为对象的键
        let key = JSON.stringify(arguments) 
        // 由于arguments可能有很多值,使用fn.apply()方法可改变函数内部的this,第二个参数可把一个(伪)数组展开传递给fn,我们的目的是将arguments的值展开传递给fn
        cache[key] = cache[key] || fn.apply(fn, arguments)
        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))
    
  • 可测试

  • 可并行处理

    • 多线程环境并行操作共享的内存数据,会出现意外
    • 纯函数不需要访问共享的内存数据,因此在并行环境下可任意运行(Web Worker)

6. 副作用

让一个纯函数不纯。如果函数依赖于外部状态,则无法保证相同的输出,由此会带来副作用。

6.1 来源

  • 全局变量
  • 配置文件
  • 数据库
  • 获取用户输入
  • ...

6.2 影响

  • 使方法通用性下降

  • 给程序带来安全隐患

7. 柯里化(Haskell Brooks Curry)

7.1 概念

当一个函数有多个参数时,可先传递一部分参数调用它(这部分函数不会变),然后返回一个新的函数,接收剩余的参数,返回相应结果。

7.2 解决函数硬编码问题

function checkAge(age) {
  let min = 18 // 变量固定-此为硬编码
  return age >= min
}
// 优化 - 将min作为变量
function checkAge(min, age) {
  return age >= min
}
console.log(checkAge(18, 20))

// 再优化 - 若min的值基本相同,则可定义一个函数,省去重复传值
function checkAge(min) {
  return function (age) {
    return age >= min
  }
}
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
console.log(checkAge18(20))

// 再再优化 - 箭头函数
let checkAge = min => (age => age >= min)

7.3 lodash 中的柯里化函数

7.3.1 _.curry(func):纯函数

功能:创建一个函数,该函数接收一个或多个 func 参数,若 func 所需要的参数都被传递,则直接调用 func 并返回结果,否则继续返回该函数并等待剩余参数。

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)) // 先返回该函数,并等待剩余参数(2, 3)
console.log(curried(1, 2)(3))

7.4 案例

7.4.1 提取字符串的空白字符

// 面向过程的方式
str.match(/\s+/g) // 提取字符串的空白字符
str.match(/\d+/g) // 提取字符串中的数字

// 函数式编程-重用
function match(reg, str) {
  return str.match(reg)
}

// 若某一正则经常使用,则可对它进行柯里化处理
const _ = require('lodash')
const match = _.curry(function (reg, str) {
  return str.match(reg)
})
const haveSpace = match(/\s+/g) // 判断字符串是否有空白字符
const haveNum = match(/\d+/g) // 判断字符串是否有数字
console.log(haveSpace('helloworld')) // null
console.log(haveNum('123abc')) // ['123']

// 过滤数组中具有空白字符的元素
const filter = _.curry((func, arr) => {
  return arr.filter(func)
})
console.log(filter(haveSpace, ['hello world', 'hello_world'])) // ['hello world']

const findSpace = filter(haveSpace)
console.log(findSpace(['hello world', 'hello_world'])) // 同上

7.5 原理模拟

以上文 getSum 为例,若 curry 接收的参数个数与 getSum 所需参数个数相同,则立即调用 getSum 且返回其执行结果,否则会返回一个新的函数,并等待接收剩余参数

function curry(func) {
  // ...args 剩余实际参数
  return function curriedFn (...args) {
    // 判断实参 & 形参个数,若实参个数 < 形参个数,则返回一个新的函数
    if (args.length < func.length) {
      return function () {
        // 1. 需要将实际参数与二次调用传递的参数进行合并,再比较,因此需要调用curriedFn
        // 2. arguments 是伪数组,需要将其转换为数组
        // 3. args.concat(Array.from(arguments)) 是一个数组,传递参数时需将内容一个个传递,因此需要将数组展开进行传递,则可使用...的形式
        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))
console.log(curried(1)(2, 3))
console.log(curried(1, 2)(3))

7.6 作用

  • 让我们可以给一个函数传递较少的参数,得到一个能记住某些固定参数的新函数
  • 对函数参数进行缓存
  • 让函数更灵活,粒度更小
  • 可把多元函数变成一元函数,组合使用函数

8. 函数的组合

纯函数 &柯里化容易写出洋葱代码——一层包一层 h(g(f(x)))

8.1 概念

函数组合(compose):若一个函数需经过多个函数处理才能得到最终值,此时可将中间过程的函数合并成一个函数。注意:函数组合默认从右往左执行

// f、g 为两个参数
function compose(f, g) {
  // value 输入内容
  return function (value) {
    return f(g(value)) // 依然为洋葱代码
  }
}

🌰获取数组中的最后一个元素:反转数组、获取数组中第一个元素

// 反转
function reverse(arr) {
  return arr.reverse()
}
// 获取数组中的第1个元素
function first(arr) {
  return arr[0]
}
const last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))

8.2 lodash中的组合函数

flow() & flowRight() 将更多函数处理成一个组合函数。

  • flow() 从左到右执行

  • flowRight() 从右到左执行

    将数组中最后一个元素转为大写

    const _ = require('lodash')
    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']))
    

8.3 原理模拟

// 不确定函数参数有多少个,因此可以用...args表示
function compose(...args) {
  return function (value) {
    // 1. 由于从右往左执行,因此需要翻转 args.reverse()
    // 2. 需要有一个函数对value进行处理,且把值累计并返回
    // 3. acc 累计的结果,fn 如何处理每次的结果
    return args.reverse().reduce(function (acc, fn) {
      return fn(acc)
    }, value)
  }
}
// 测试
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))

// 优化
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))

8.4 结合律

函数组合需要满足结合律(associativity),既可以把 f & g 结合,也可以把 g & h 结合,结果相同

compose(f, g, h) == compose(compose(f, g), h) == compose(f, compose(g, h))

8.4.1 案例

const _ = require('lodash')
const f1 = _.flowRight(_.toUpper, _.first, _.reverse)
const f2 = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f3 = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
// 以上三种写法,结果一样
console.log(f1(['one', 'two', 'three']))

8.5 调试

8.5.1 案例

需求:将 NEVER SAY DIE --> never-say-die

const _ = require('lodash')
// 由于 _.split、_.join 等方法都是数据优先函数在后,且没有进行柯里化,因此需要我们先将其进行柯里化,并转为函数优先数据在后的形式
const split = _.curry((sep, str) => _.split(str, sep)) // 切割字符串
const join = _.curry((sep, arr) => _.join(arr, sep))
const fn = _.flowRight(join('-'), _.toLower, split(' '))
console.log(fn('NEVER SAY GOODBYE')) // 打印结果有误

const log = v => {
  console.log(v)
  return v
}  
// 改写fn,使用log去调试是哪一步出现问题
// 发现:split将字符串切割为数组,toLower会将其转为字符串,由此再进行join的时候,会报错
const fn = _.flowRight(join('-'), _.toLower, log, split(' '))

// 改造1(改变执行顺序) :先切割 - 拼接 - 转小写
const fn = _.flowRight(_.toLower, join('-'), log, split(' '))

// 改造2(不改变执行顺序):遍历数组中的每个元素,将其转为小写,并返回新数组,可使用map
const map = _.curry((fn, arr) => _.map(arr, fn))
const fn = _.flowRight(join('-'), log, map(_.toLower), log, split(' '))
// 此时若进行log调试,会不明确打印处的是哪个函数的结果,改需改造log
// trace 跟踪,tag标记,v实际的值
const trace = _.curry((tag, v) => {
  console.log(tag, v)
  return v
})
const fn = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('split之后'), split(' '))

8.6 lodash——fp模块

使用函数组合会使用到 lodash 的方法,但若函数参数较多,需使用柯里化的形式重新包装方法,因此可使用 fp (函数式编程)模块。

  • lodash 模块——数据优先,函数在后

  • lodash 中的 fp 模块 —— 函数优先,数据置后

与 8.5 调试案例 进行对比,fp 模块时已经柯里化后的函数,无需再次柯里化

const fp = require('lodash/fp')
const fn = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log('NEVER SAY GOODBYE')

8.7 lodash 与 fp 模块 map 的区别

需求:将字符串元素都转为整型

// lodash 中的 map
const _ = require('lodash')
console.log(_.map(['23', '8', '10'].parseInt)) // [23, NaN, 2]

/**
 * 分析
 * _.map():调用 map 中传递的函数时,会接收三个参数(值, 数组的索引/对象的键, 集合)
 * parseInt('23', 0, arr) // 值、索引0、arr
 * parseInt('8', 1, arr) // 值、索引1、arr
 * parseInt('10', 10, arr) // 值、索引2、arr
 * 但是parseInt 函数第二个参数实际是将值转换为几进制,若为0,默认10进制
 * 在遍历到8的时候,表示将8转为1进制,但是不支持1,因此是NaN
 * 若想解决,需要重新封装parseInt,让它只接收1个参数
 */

// lodash 中的 fp 模块中的map
const fp = require('lodash/fp')
console.log(fp.map(parseInt, ['23', '8', '10']))

9. PointFree

9.1 概念

是一种编程模式,它的具体实现是函数的组合。

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
// 需求:将 Hello   World --> hello_world
// 非 Point Free 模式:接收一个参数,并对其进行处理
function fn (word) {
  return word.toLowerCase().replace(/\s+/g, '_') // 全局(/g)匹配多个(+)空白(\s)
}

// Point Free 模式:先定义一些基本运算函数,再将其合成函数,合成过程中不需指明需要处理的数据
const fp = require('lodash/fp')
const fn = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
函数式编程Point Free 模式
把运算过程抽象为函数把抽象的函数合成新的函数,合成过程不关心数据

9.2 案例

需求:把一个字符串中的首字母提取并转换成大写,使用 . 作为分隔符。如:world wild web ==> W. W. W

const fp = require('lodash/fp')
// ①将字符串以空格分隔为数组,②遍历截取第一个字符,③将其再以相应分隔符组合,④ 转为大写
const firstLetterToUpper = fp.flowRight(fp.toUpper, fp.join('. '), fp.map(fp.first)), fp.split(' '))

// 解法2
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.toUpper, fp.first)), fp.split(' '))
console.log(firstLetterToUpper('world wild web'))

10. Functor(函子)

10.1 概念

  • 容器:会包含值以及处理值的函数
  • 函子:特殊的容器。
    • 函数式编程的运算不直接操作值,而是由函子完成
    • 函子是一个实现了 map 契约的对象
    • 可将函子想象成一个盒子,盒内封装了一个值
    • 想要处理盒内的值,需给 map 方法传递一个处理值的纯函数,由这个函数来处理
    • 最终 map 方法返回一个包含新值的盒子(函子),可使用 .map 链式调用
// 用一个类来描述函子
class Container {
  // 初始化一个构造函数,接受一个值 value
  constructor (value) {
    // 构造函数内部设置一个属性用来接收值 value,此值在盒子里不对外公布
    this._value = value // 约定下划线的为私有变量
  }
  
  // 盒子需对外公布一个map方法,map 方法接收一个处理值的函数,处理后返回一个新的函子
  map(fn) {
    return new Container(fn(this._value))
  }
}

// 调用,处理函子内部的值,需使用map方法,
// 注意map返回的不是值,而是新的函子对象,在新的函子对象中保存新的值,始终不把值对外
let r = new Container(5)
	.map(x => x + 1)
	.map(x => x * x)
console.log(r)

改造一下

/* 改造:现在创建函子需要使用 new 的方法,由于我们是函数式编程而非面向对象编程,因此可以避免使用 new 来创建 */
class Container {
  // 可创建一个静态的方法,定义方法名为 of
  static of(value) {
    return new Container(value)
  }
  // 函子内部有一个值,此值在盒子里不对外公布
  constructor (value) {
    this._value = value // 约定下划线的为私有变量
  }
  
  // 盒子需对外公布一个map方法,map 方法接收一个处理值的函数,处理后返回一个新的函子
  map(fn) {
    return Container.of(fn(this._value))
  }
}
let r = Container.of(5)
	.map(x => x + 2)
	.map(x => x * x)
// r 为函子对象,值在对象里,若想得到值,可调用map方法,在map方法中打印
console.log(r)

若传值时传了 null or undefined,怎么处理?可使用 【10.2 MayBe 函子】

Container.of(null)
	.map(x => x.toUpperCase()) // 此时 x 为null,会报错

10.2 MayBe函子

可处理外部空值的情况(将副作用控制在允许的范围)。

class MayBe {
  static of(value) {
    return new MayBe(value)
  }
  // 初始化一个构造函数,接收一个值 value
  constructor (value) {
    // 在构造函数内部需要设置一个属性用来接收这个值
    this._value = value
  }
  // 判断值是否为null or undefined
  isNothing() {
    return this._value === null || this._value === undefined
  }
  // 接收一个函数,返回新的函子,但需先判断值是否为空
  map(fn) {
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
  }
}

// 测试1
const r = MayBe.of(null)
  .map(x => x.toUpperCase())
console.log(r) // 返回一个value为null的函子

// 测试2
const r = MayBe.of(null)
  .map(x => x.toUpperCase())
  .map(x => null)
  .map(x => x.split(' '))
console.log(r)

会发现一个问题:虽然可拦截错误,但是当多次调用时,不能知道错误发生在哪一步,此时可使用 【10.3 Either 函子】 来解决此问题

10.3 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))
  }
}
// 调用1
let r = Right.of(12).map(x => x + 2)
let l = Left.of(12).map(x => x + 2)
console.log(r) // Right { _value: 14 }
console.log(l) // Left { _value: 12 }

演示可能会发生错误的函数,并捕获错误信息

function parseJSON(value) {
  try {
    // Right 处理正确的值
    return Right.of(JSON.parse(value))
  } catch (err) {
    // Left 捕获错误的信息
    return Left.of({ error: err.message })
  }
}
let r = parseJSON ('{name: zs}')
let r1 = parseJSON('{"name": "zs"}').map(x => x.name.toUpperCase())
console.log(r) // Left { _value: { error: 'xxx' } }
console.log(r1) // Right { _value: 'ZS' }

10.4 IO 函子

IO(input & output)输入输出

  • IO 函子中 _value 是一个函数,这里把函数作为值梳理
  • IO 函子可把不纯的动作存储进 _value 中,惰性执行
  • 把不纯的操作交给调用者处理
const fp = require('lodash/fp')
class IO {
  // 接收一个值,返回新的 IO 函子,由于构造函数中需要接收一个函数,因此这里传函数
  static of (value) {
    return new IO(function () {
      return value
    })
  }
  // 初始化一个构造函数,IO 函子里保存的是一个函数,因此将函数作为参数
  constructor (fn) {
    this._value = fn
  }
  // 将当前函子的 value(函数) 和传入的函数组成一个新的函数
  map(fn) {
    return new IO(fp.flowRight(fn, this._value))
  }
}
// 调用:process 进程;execPath node 进程执行的路径
let r = IO.of(process).map(p => p.execPath)
console.log(r) // IO { _value: [Function (anonymous)] }
console.log(r._value()) // 打印出node 执行路径

11. Folktale

一个标准的函数式编程库。

Folktalelodash、ramda
提供一些函数式处理操作,
如 compose,curry等,
函子 Task、Either、MayBe 等
提供数组、字符串等相关功能函数

11.1 快速开始

11.1.1 安装 Folktale 库

npm i folktale

11.1.2 引入包,小🌰

const { compose, curry} = require('folktale/core/lambda') // 路径可查官网
// curry(指明后面传入的函数有几个参数,函数)
let f = curry(2, (x, y) => x + y)
console.log(f(1,2))
console.log(f(1)(2))

// compose 组合纯函数
const { toUpper, first } = require('lodash/fp')
let f = compose(toUpper, first)
console.log(f['one', 'two'])

11.2 Task 函子

处理异步任务

const fs = require('fs')
const { task } = require('folktale/concurrency/task') // 路径可查官网
const { split, find } = require('lodash/fp')

function readFile(filename) {
  // task 接受一个函数,参数固定为resolver
  return task(resolver => {
    // fs.readFile(读取的文件路径,文件编码,回调函数(错误优先))
    fs.readFile(filename, 'utf-8', (err, data) => {
      if (err) resolver.reject(err)
      resolver.resolve(data)
    })
  })
}
readFile('package.json') // 到此只返回一个 task 函子,并没有读取文件
	.map(split('\n')) // 以“换行”将文件进行切割
	.map(find(x => x.includes('version'))) // 寻找数组中具有verson的元素
	.run() // 读取文件。在run之前可使用map处理拿到的结果
	.listen({ // 监听当前执行状态,以事件机制执行
  	onRejected: err => { // 执行失败后执行的函数
      console.log(err)
    },
    onResoleved: value => { // 执行成功后执行的函数
      console.log(value)
    }
	}) 

11.3 Pointed 函子

Pointed 函子是实现了 of 静态方法的函子。

of 方法是为了避免使用 new 来创建对象,更深层含义是:of 方法用来把值放到上下文,放进容器中,用 map 来进行处理

// 一个函子,有 of 方法,则为 pointed 函子
class Container {
  // of 作用:把值包裹进一个新的函子中并返回
  static of (value) {
    // 返回的结果即为上下文
    return new Container(value)
  }
  ...
}
// 调用 of 方法时获取一个上下文,在上下文中处理数据
Container.of(2)
	.map(x => x + 2)

11.4 Monad (单子)

11.4.1 先看IO 函子中的问题

模拟 Linux 下的 cat 命令——读取文件并将文件内容打印出来

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

class IO {
  static of (value) {
    return fn(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
  })
}

// readFile 返回一个IO的函子,调用readFile时,会把IO函子传递给print
// print的 x 为上一步中(即readFile)返回的 IO函子
// print拿到这个函子后返回一个IO函子,在function 中又返回一个函子
let cat = fp.flowRight(print, readFile)
// cat 事实上是函子嵌套函子,IO(IO(x)),外层IO是print 返回的IO函子,内层IO是readFile 返回的IO函子
let r = cat('package.json') // 此时返回的是函数,还没有读取文件

// 读取文件,调用_value,执行的是print里IO中的function
let r1 = cat('package.json')._value() // 打印两次IO函子,第一次为 print 中IO的function,把readFile中的IO函子打印出来;第二次为实际console

let r2 = cat('package.json')._value()._value() // 终于拿到文件内容
console.oog(r1)

问题即为想要拿到真正的值,需要链式调用多次

11.4.2 概念

Monad 函子是可变扁的 Pointed 函子,IO(IO(x))。

一个函子如果具有 join 和 of 两个方法,并遵守一些定律,即为 Monad

使用场景:当一个函数返回一个函子的时候,考虑用Monad,可解决函子嵌套的问题

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

class IO {
  static of (value) {
    return fn(value)
  }
  // 当函数返回一个函子时,可将它变成一个 Monad
  constructor (fn) {
    this._value = fn
  }
  map(fn) {
    return new IO(fp.flowRight(fn, this._value))
  }
  // 调用value,并返回值(返回的是一个函子)
  join() {
    return this._value()
  }
  // 同时调用 map & join,而map方法需要一个参数
  flatMap (fn) {
    return this.map(fn).join()
  }
}

// readFile 方法同上,不再写一次了
// 当合并的函数返回的是值,则直接调用map;当合并的函数返回的是函子,则调用flatmap
let r = readFile('package.json')
	.flatMap(print)
	.join()
console.log(r)

读完文件,如何处理文件内容?比如将内容转为大写

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