二更详解函数式编程

写在前面

是不是还在日复一日地面向百度、面向谷歌、面向过程?是不是也曾想过在平常业务中尝试使用各种设计模式?晚上想想千条路,早上起来走原路。
我一直都在鄙夷过去写的代码,想着重构却又不敢下手,不少祖传代码写成了连我自己都怕的样子,甚至于牵一发而动全身的片段不在少数。每每看到这些代码我就在想,如何才能优雅一点、再优雅一点。
上一篇文章 《一口气看完Vue源码》 中我提到过几次高阶函数,其本质就是在内部又返回了一个函数的函数,我们可以将这种做法看作是函数是一等公民的特征。
Vue3 使用 CompositionAPI 代替了原来的 OptionAPI,这一举措才让我知道有一种范式叫作函数式编程。其实使用 React 的项目中函数式编程并不少见,但由于我从业三年多以来没写过一个 React 项目,使得自身的见识稍显浅薄。就算如今真的知道了,其实也不是突然变得好学,什么都想去接触一下,而是由于最近上网课的原因,毕竟心里想法千千万,真要实现还得靠外力。
如果想对函数式编程有一个完全的详细的了解,推荐《JS 函数式编程指南》,本文必不会比这本书讲得更到位,仅是我个人对于看的各类资料中对于函数式编程的一些整理、归纳和总结。
最后,如果想使用函数式编程的范式书写代码,推荐使用 Lodash 和 Lodash/fp,文档齐全,重要的是闲暇之余可以了解一下源码,肯定会对函数式编程有更深的理解。

函数式编程

优点

  • 使代码更加简洁,抛弃多余的垃圾代码
  • 摒弃 JavaScript 中 this,解决了日常工作中指针不知道指向谁的问题
  • 由于函数式编程的特性可以使得打包过程中 tree shaking 更好地工作
  • 模块化颗粒度小,高复用低耦合,可预测性,方便 单元测试debug

函数式编程中的函数并不是指 function,而是数学中的函数,即 f(x)

下面的函数就是一个简单的 f(x) = x + 1 的实现

const plusOne = number => number + 1

既然是数学范畴的理论,那就应该满足数学领域的计算定律,比如交换律、结合律、分配律,下面这个例子中将展示函数式编程中对其的妙用

const plus = (x, y) => x + y
const multiply = (x, y) => x * y
// 交换律
// 2 + 3 = 3 + 2 = 5
// 2 * 3 = 3 * 2 = 6
plus(2, 3) === plus(3, 2)
multiply(2, 3) === multiply(3, 2)
// 分配率
// 4 * (2 + 3) = (4 * 2) + (4 * 3) = 20
multiply(4, plus(2, 3)) === plus(multiply(4, 2), multiply(4, 3))
// 结合律
// (2 + 3) + 4 = 2 + (3 + 4) = 9
// (2 * 3) * 4 = 2 * (3 * 4) = 24
plus(plus(2, 3), 4) === plus(2, plus(3, 4))
multiply(multiply(2, 3), 4) === multiply(2, multiply(3, 4))

函数是一等公民

  • 不将函数当做方法来看待,而是将其当作普通对象来使用
  • 既然是普通对象那就可以赋值给变量、可以当作返回值、也可以存进数组…
    当做返回值在后面可以闭包/柯里化部分可以了解,这里先不做介绍,所以就先只介绍赋值给变量。既然要将函数赋值给变量那就要使用 函数表达式,这里的妙用可以实现的就是我在优点里提到的使代码更加简洁,抛弃多余的垃圾代码
// 例1
const hi = name => `Hi ${name}`
const greeting = name => hi(name)
const greeting = hi
// greeting 方法传入了一个 name 到 hi 方法中,并调用 hi 方法
// 本质上来讲 greeting 方法完全可以直接使用 hi 方法也不会有问题
// 因此我们可以将其改写成 const greeting = hi

// 例2
const getServerStuff = callback => ajaxCall(json => callback(json))
const getServerStuff = callback => ajaxCall(callback)
const getServerStuff = ajaxCall
// json => callback(json) 可以看做 const f = json => callback(json)
// 和上面 const greeting = name => hi(name) 例子一样,我们可以将其改写为 const f = callback
// 因此 ajaxCall(json => callback(json)) 可以写作 ajaxCall(f),也就是 ajaxCall(callback)

// callback => ajaxCall(callback) 可以看做 const f = callback => ajaxCall(callback)
// 和上面同理我们可以将其改写为 const f = ajaxCall
// 因此 callback => ajaxCall(callback) 可以写作 f,也就是 ajaxCall
// 最终得到 const getServerStuff = ajaxCall

// 例3
const BlogController = {
  index(posts) { return Views.index(posts) },
}
const BlogController = {
  index: posts => Views.index(posts),
}
const BlogController = {
  index: Views.index,
}

高阶函数

  • 函数可以作为参数
  • 函数可以作为返回值 ( 详见最后一节的柯里化 )
// Lodash 中 map 方法的简单实现
const _map = curry((f, target) => target.map(f))

这段改写看起来毫无意义,不就是把数组原生的 map 方法封装了起来而已吗?
乍看如此,其实不然。首先 _map 方法调整了方法和对象的位置,对于一个函数来说,方法内部逻辑是固定的,但是传入的变量不是同的。因此没必要等到所有参数都集齐了再去实现这个方法,可以先传入一个方法,然后等待变量的传入。结合下文的组合函数和柯里化食用更佳
其次,在下面一大部分篇章中我会介绍一个叫函子的容器,我们需要通过调用函子的 map 方法来操作函子内部的值,所以说存在即是合理

纯函数

使用过 Gulp 或者 Webpack 的开发者应该对 pipe 和 loader 有一定的印象,其运行本质就是通过当前的 pipe / loader 中的方法输出一个 JavaScript 可执行函数给下一个 pipe / loader 方法对代码进行处理。


如果在 pipe / loader 执行过程中输出的结果每次都不一样( 有副作用 ),那我们必不可能对打包的结果有什么信心,因此函数式编程的特性之一就是纯函数,输入固定的内容得到的也必然是固定的内容

// 因为 slice 输入相同得到了相同的答案,所以 slice 是纯函数,splice 则为不纯的函数
const array = [1, 2, 3, 4, 5]
array.slice(0, 1) // [1]
array.slice(0, 1) // [1]
array.slice(0, 1) // [1]

array.splice(0, 1) // [1]
array.splice(0, 1) // [2]
array.splice(0, 1) // [3]

副作用 Side Effect

  • 既然有纯函数的存在,那么就会有不纯的函数,即拥有副作用
  • 函数体内部引用了外部参数视为副作用
  • 函数体内有 http 请求视为副作用
  • 函数体内操作 DOM
  • console.log 视为副作用

  • 大家也许会疑问 console.log 为什么也算副作用,那请在请在浏览器中尝试输入以下代码
console.log = e => e + 1
console.log(2) // 会得到 3,而并非 2
// console 是一个外部可操控的对象,因此函数体内存在外部可改变的对象时,也是不纯的

可缓存性 Cacheable

既然纯函数每次相同的输入都会得到相同的输出,那函数内部一旦复杂的话其实没必要每次都去运行一次全部的流程,可以直接通过缓存拿到该输入的输出值,即 memoize 技术

const memoize = f => {
  const cache = {}
  return function() {
    const input = JSON.stringify(arguments)
    cache[input] = cache[input] || f.call(this, ...arguments)
    return cache[input]
  }
}
const squareNumber = memoize(x => x * x)
squareNumber(4) // 16 计算所得
squareNumber(4) // 16 从缓存中读取输入值为 4 的结果

可移植性/自文档化 Portable / Self-Documenting

下面的函数因为有 _toString 的存在看起来像是有副作用,但由于其调用其实是在一个闭包中,因此被延迟执行,对于 is 方法本身来说依然是一个纯函数。


此外,由于 _toString 被抽离出函数本体变成了一个可配置的函数,所以对于 is 方法来说只要改变了 _toString 方法就可以改变自身内部的实现,_toString 成了模块的存在,由此达到可移植性

const _toString = (target, type) => Object.prototype.toString.call(target) === `[object ${type}]`
const is = type => target => _toString(target, type)
const isObject = is('Object')
const isArray = is('Array')
const isFunction = is('Function')

可测试性 Testable

同样是因为固定的输入会得到固定的输出,在测试时便没必要经历每一个流程最后抵达需要测试的方法,只需要给需要测试的方法一个输入便可以测试到结果是否是我们想要的

合理性 Reasonable

上面介绍过数学中的函数有一些定律,比如交换律结合律等,这里的合理性则是数学中的同一律,意思是如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。比如 if (teamA.name === teamB.name) 在纯函数中可以直接当做 if (‘myTeam’ === ‘yourTeam’) 来处理

并行代码

不是一个组合的纯函数可以做到并行,因为相互之间没有数据的访问。
但是对于这一点我存在着一些疑问,比如日常业务中经常会处理一个数组中各个对象字段的值,如果使用并行+函子是否代价会更大呢?

组合和柯里化

组合函数的存在就是为了实现数学中的结合律,在看下文之前请大家先思考三个问题,然后带着问题看柯里化和组合函数的内容

两个问题:

    1. 在 call / apply / bind 这三个方法中为什么是将新指针放在 arguments 第一位?
    1. 对于数据的处理时,大家认为是数据被先确认还是方法呢?
    1. bind 方法为什么在传入值以后还要再调用一次?实现原理是什么?

柯里化

  • 柯里化其实就是运用了闭包的思想,将父函数的 arguments( 多是一个方法 ) 存放在子函数中,在参数补全时执行
  • 参数的长度可能是已知的,也可能是未知的,因此柯里化的实现需要对参数个数进行区分
  • 当我们只已知一个参数,另一个参数需要通过其他方式接收时,我们可以通过柯里化一个多入参的函数,在第一次调用时先将一个已知的值传入,等剩下的值都接收完毕后让系统自动调用
// 已知长度的柯里化
function curry(f) {
  // 获取 f 的参数个数
  const len = f.length
  const inner = (...args) => {
    if (args.length === len) {
      return f.apply(this, args)
    } else {
      return function(...rest) {
        return inner(...args.concat(rest))
      }
    }
  }
  return inner
}
// 未知长度的柯里化 - 1
function curry(f) {
  const params = []
  const inner = (...args) => {
    if (args.length) {
      params.push(...args)
      return inner
    } else {
      return f.apply(this, params)
    }
  }
  return inner
}
// 未知长度的柯里化 - 2
function curry(f) {
   const inner = (...args) => {
    return function(...rest) {
      if (rest.length) {
        return inner(...args.concat(rest))
      } else {
        return f.apply(this, args)
      }
    }
  }
  return inner
}
const add = curry((a, b, c) => a + b + c)
console.log(add(1)(2)(3)()) // 6
console.log(add(1, 2, 3)()) // 6
console.log(add(1)(2, 3)()) // 6

手写 bind 实现

// 先来了解一下函数调用的指针问题
const obj = {
  name: 'JavaScript',
  sayHello() {
    console.log(`hello ${this.name || 'nameless man'}`)
  }
}
const sayHello = obj.sayHello
obj.sayHello()
sayHello()
const aeorus = {
  name: 'aeorus'
}
const meetAeorus = obj.sayHello.bind(aeorus)
const meetAeorusAgain = sayHello.bind(aeorus)
meetAeorus()
meetAeorusAgain()
/*
如果大家对于 JavaScript 的调用有所了解的话那答案应该很快就能得到
分别是:
  hello JavaScript
  hello nameless man
  hello aeorus
  hello aeorus

在 obj.fn() 中,obj 是调用者,fn 内部的指针就指向了 obj
在 fn() 中,没有可见的调用者,因此 fn 内部的指针指向了 window
而 bind 方法就是为了帮助 fn 重新指定一个指针
*/
// 注意事项:这里定义不能使用箭头函数,因为箭头函数没有指针
Function.prototype.myBind = function(...args) {
  const context = args[0] || window // 获取环境上下文
  args = args.slice(1) // 获取 fn 的参数
  const fn = this // 获取 fn,因为调用时是 fn.bind,所以 this 会指向 fn
  context.fn = fn // 重新指定 fn 的上下文环境
  return function() {
    return context.fn(...args) // 在该上下文环境中调用 fn
  }
}

组合函数

  • 上一节中有提到 pipe 和 loader 的运行机制,在我们编写的函数式编程思想的方法中同样需要一个相似的函数,即 compose 组合函数
  • 组合函数必须符合数学中的结合律,指的是无论传入的方法集合里的方法顺序怎么变,得到的答案必须是相同的,这也是为什么函数式编程的特性之一是纯函数的原因
const compose = (...args) => {
  // 将组合函数内的方法集合进行倒序排列
  args = args.reverse()
  // 使用 reduce 方法将上一次执行的结果传递给下一次充当参数,最后返回结果
  return value => args.reduce((prev, curv) => curv(prev), value)
}
// 例1
const toUpper = str => str.toUpperCase()
const reverse = array => array.reverse()
const first = array => array[0]
// 利用不纯的 console.log 
const trace = curry((tag, result) => {
  console.log(`${tag} -> ${result}`)
  return result
})
const upperLastEle = compose(
  compose(
    toUpper,
    trace('after first'),
    first
  ),
  trace('after reverse'),
  reverse
)
// after reverse -> ['c', 'b', 'a']  after first -> c  completed -> C
trace('completed', upperLastEle(['a', 'b', 'c']))
// after reverse -> ['g', 'f', 'e']  after first -> g  completed -> G
trace('completed', upperLastEle(['e', 'f', 'g']))

// 例2 优化代码
const authenticate = form => {
  const user = toUser(form)
  return logIn(user)
}
const authenticate = form => logIn(toUser(form))
const authenticate = compose(logIn, toUser)

闭包

其实闭包没什么好讲的,如果上面柯里化的内容可以消化的话其实就可以说自己懂闭包了。但凡事也总有点例外,比如垃圾回收机制会怎么处理闭包呢?为什么总能听到闭包会导致内存泄漏?其实问题就在于存在于父函数中的变量被子函数引用时垃圾回收机制会怎么运行
问题1:当父函数被销毁,该变量是否会被一起销毁?如果不会是为什么?
问题2:当子函数被销毁,该变量是否会被一起销毁?如果不会是为什么?
答案其实很简单,父函数在调用后被立即销毁,但因为该变量被子函数引用,所以它不会被销毁。当子函数在调用后被立即销毁,但因为该变量不是自己的作用域内的变量,所以无权对其进行标记销毁。

链式调用

我不知道大家有没有想过链式调用的原理,如果还不了解的话建议先思考一下。


其实本质上很简单,即返回一个新的自己

class Calc {
  constructor(x) {
    this.__value = x
  }
  plus(x) {
    const result = x + this.__value
    return new Calc(result)
  }
  minus(x) {
    const result = this.__value - x
    return new Calc(result)
  }
}
new Calc(5).plus(3).minus(1).plus(2).minus(1) // Calc { __value: 8 }

全新领域

终于是讲到函子了,这块儿我个人都觉得晦涩难懂,不停地写 demo、不停地改 demo、思考应用场景…
就算是编我也算是编的有点东西了,话不多说,前方有些高能,我开始编了。

函子

  • 一个特殊的容器
  • 容器内的值可以是任意类型
  • 不直接操作值而是对外暴露一个 map 方法实现对容器内的值进行处理
    函子就像 Webpack 的插件一样,对外暴露一系列钩子,然后操作钩子中的 compilation 对文件进行修改,先来简单写一个函子
class Container {
  static of(x) {
    return new Container(x)
  }
  constructor(x) {
    this.__value = x
  }
  map(f) {
    return Container.of(f(this.__value))
  }
}
// 创建一个名为 x 的容器 ( Functor ) 接收值 9
const x = Container.of(9)
// 获取 x 容器内的值
console.log(x.__value) // 9
// 操作 x 容器内的值并得到名为 y 的新容器
// 此操作并不会改变 x 容器内的值
const y = x.map(value => value + 1)
// 获取 y 容器内的值
console.log(y.__value) // 10

Maybe函子

上面的例子就是一个最单纯的函子了,存值取值操作值,一切有如水到渠成。但是实际业务中哪来那么多的水到渠成。
我们可以猜想一下,如果函子内部传入的是 null 会出现什么问题呢?答案是会报错,而报错会导致函数变得不纯,这是函数式编程中我们不想看到的,因此出现了一种可以判断是否为空值的函子横空出世 —— Maybe函子

// 当 __value 不为空值时
const input = Container.of(['a', 'b', 'c'])
const result = input.map(value => value.filter(item => item === 'a'))
console.log(result.__value) // ['a']
// 当 __value 为空值时
const empty = Container.of()
const emptyResult = empty.map(value => value.filter(item => item === 'a')) // TypeError: Cannot read property 'filter' of undefined

让我们尝试使用 Maybe函子 重新处理上面这段代码

class Maybe extends Container {
  static of(x) {
    return new Maybe(x)
  }
  isNothing() {
    return this.__value === null || this.__value === undefined || !Object.keys(this.__value).length
  }
  map(f) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value))
  }
}
// 当 __value 不为空值时
const input = Maybe.of(['a', 'b', 'c'])
const result = input.map(value => value.filter(item => item === 'a'))
console.log(result.__value) // ['a']
// 当 __value 为空值时
const empty = Maybe.of()
const emptyResult = empty.map(value => value.filter(item => item === 'a'))
console.log(emptyResult.__value) // null

从上面这个例子中可以发现,程序不报错了,这真是件可喜可贺的事情,那接下来我们就需要想想怎么结合业务来做一些事情了。
比如电商平台总会将小数点前的数字变大,小数点后的数字和单位变小。

const products = [{
  name: 'any-one',
  sellPrice: '9.9',
}, {
  name: 'aeo-rus',
  sellPrice: '29.9',
}, {
  name: 'no-price',
}, {
  sellPrice: '49.9',
}]
/* 期望输出
[{
  float: "9",
  int: "9",
  name: "anyone",
  sellPrice: "9.9",
}, {
  float: "9",
  int: "29",
  name: "aeorus",
  sellPrice: "29.9",
}, {
  name: "noprice",
  sellPrice: null,
}, {
  float: "9",
  int: "49",
  name: null,
  sellPrice: "49.9",
}]
*/

平常我们面向过程时会怎么写呢?写一个方法将字段传入进行 format,得到返回值后和源数据进行合并

// 非 pointfree
const resolveProp = (f, key, target) => {
  const value = target[key] || null
  if (value) {
    const result = isObject(f(value)) ? f(value) : {
      [key]: f(value)
    }
    return Object.assign(target, result)
  } else {
    return Object.assign(target, {
      [key]: value
    })
  }
}
const formatName = name => name.split('-').join('')
const formatPrice = sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
}
Object.assign(products,
  products.map(product => {
    resolveProp(formatName, 'name', product)
    resolveProp(formatPrice, 'sellPrice', product)
    return {
      ...product,
    }
  })
)

既然学到了函子,那我们就要考虑下怎么下手了,首先 API 那块儿会根据是否有值而将字段进行传递,所以我们需要考虑到 null 的情况,因此我们不得不选用 Maybe函子

// pointfree
const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, f, key) => {
  const result = compose(
    _prop('__value'),
    compose(
      f,
      _map(_prop(key)),
    )
  )(target)
  return isObject(result) ? result : {
    [key]: result
  }
})
_map(product => {
  const resolveProp = resolveProduct(Maybe.of(product))
  Object.assign(product, {
    ...resolveProp(formatName, 'name'),
    ...resolveProp(formatPrice, 'sellPrice'),
  })
}, products)
  • 写是写完了,但总觉得不是太过优雅,好像代码量不但没有减少,理解难度还上去了。不急,接着往后面看,毕竟业务中哪有只用一个函子的?
  • 顺便提一嘴,Maybe函子 中经常会使用一个 maybe 辅助函数对返回值进行动态修改,达到错误收集或者指定返回值的效果
// 使用 maybe 辅助函数,将未定义的字段赋值为 null 返回
const maybe = curry((x, f, m) => m.isNothing() ? x : f(m))
const resolveProduct = curry((target, f, key) => {
  const result = compose(
    maybe(null, prop('__value')),
    compose(
      f,
      _map(prop(key)),
    )
  )(target)
  return isObject(result) ? result : {
    [key]: result
  }
})

我上面提到不太优雅的事,比如 resolveProduct 这个方法看起来没什么毛病,但我们需要想一下函子的目的是什么?难道仅仅就是提供一个 map 方法供 _map 方法在组合函数中调用到内部值吗?好像并没有解决组合函数的洋葱模型写法所带来的缺陷,如此看来函子的作用其实并不大?那我们就来改写一下 resolveProduct 这个方法试试

const resolveProduct = curry((target, f, key) => {
  const sourceValue = target.map(_prop(key))
  const formatValue = maybe(Maybe.of(null), f, sourceValue)
  const result = formatValue.map(value => isObject(value) ? value : {
    [key]: value
  })
  return result.__value
})

感觉美观了一些,但其实还不够

Either函子

  • Maybe函子 可以处理当内部值为空值时不去调用 map 方法中传入的函数,甚至可以通过 maybe 辅助函数来达到对链式调用最后结果返回值的动态控制,可如果我们希望 maybe 辅助函数的第一个参数也可以调用 map 方法中传入的函数呢?虽然我们可以改写 maybe 辅助函数,但并不太合理
  • 理论上来讲 try…catch… 并不纯,因为 catch 可能抛出各类异常并且终止程序运行。日常开发中我们希望程序报错,但并不希望程序终止,有可能的话最好是通过一个 errors 对象将报错信息收集起来,然后统一通过 new Image 传给后台记入日志中
    话不多说,先来写一个 Either函子
class Either {
  static of(left, right) {
    return new Either(left, right)
  }
  constructor(left, right) {
    this.left = left
    this.right = right
  }
  map(f) {
    return this.right ?
      Either.of(this.left, f(this.right)) :
      Either.of(this.right, f(this.left))
  }
}

接下来我们又要结合业务看看可以做一些事情了


比如电商平台中会根据连锁平台的地理位置进行省市的归类。上面提到,后端架构中有可能在商户没有填写某数值时不会返回那个字段,因此我们需要使用到 Maybe函子 来对源数据进行一层包裹处理

const userInfo = {
  address: '江苏省南京市江宁区'
}
/* 期望输出
[
  '江苏省南京市江宁区',
  '江苏省',
  '南京市',
  '江宁区',
  undefined,
  '',
  index: 0,
  input: '江苏省南京市江宁区',
  groups: [Object: null prototype] {
    province: '江苏省',
    city: '南京市',
    county: '江宁区',
    town: undefined,
    village: ''
  }
]
*/
const userInfo = null || {}
/* 期望输出
[
  '江苏省镇江市京口区',
  '江苏省',
  '镇江市',
  '京口区',
  undefined,
  '',
  index: 0,
  input: '江苏省镇江市京口区',
  groups: [Object: null prototype] {
    province: '江苏省',
    city: '镇江市',
    county: '京口区',
    town: undefined,
    village: ''
  }
]
*/
const regex = "(?<province>[^省]+省|.+自治区)(?<city>[^自治州]+自治州|[^市]+市|[^盟]+盟|[^地区]+地区|.+区划)(?<county>[^市]+市|[^县]+县|[^旗]+旗|.+区)?(?<town>[^区]+区|.+镇)?(?<village>.*)"
const match = curry((regex, target) => target.match(regex))
const adddress = Maybe.of(userInfo).isNothing() || Maybe.of(userInfo).map(_prop('address')).isNothing() ?
  Either.of('江苏省镇江市京口区', null) :
  Either.of(null, _prop('address', userInfo))
const result = adddress.map(match(regex))

这段代码写完先不说优雅不优雅,首先逼格就十足
接下来我们看下 Either函子 在错误收集上的应用

const parseJSON = (str) => {
  try {
    return Either.of(null, JSON.parse(str))
  } catch(e) {
    return Either.of({ error: e.message }, null)
  }
}
// Either { left: { error: 'Unexpected token n in JSON at position 2' }, null }
parseJSON('{ name: aeorus }')
// Either { left: null, right: { name: 'aeorus' } }
parseJSON('{ "name": "aeorus" }')

ap函子

  • applicative 的缩写,旨在可以用一个函子操作另一个函子
  • A函子 内部的值是一个常量,B函子 内部的值是一个 函数,当希望使用 B函子 的内部函数操作 A函子 的内部值时,可以使用 ap函子
class Ap extends Container {
  static of(x) {
    return new Ap(x)
  }
  ap(functor) {
    return Ap.of(this.__value(functor.__value))
  }
  map(f) {
    return Ap.of(f(this.__value))
  }
}

这个函子困惑了我得有很长时间,我一直在思考 _map 方法还不够用吗?这个函子的实际应用场景在哪儿?先写个简单的应用函数便于大家理解

// 输入 2 3
// 预期输出 5
const plus = curry((x, y) => x + y)
console.log(
  _prop(
    '__value',
    Ap.of(plus)
      .ap(Container.of(2))
      .ap(Container.of(3))
  )
)
console.log(
  _prop(
    '__value',
    Ap.of(plus(2))
      .ap(Container.of(3))
  )
)
console.log(
  _prop(
    '__value',
    Ap.of(2)
      .map(plus)
      .ap(Container.of(3))
  )
)

写完以后是不是觉得和组合函数好像啊,map 中传入方法,ap 中传入值,有点常规?
那接下来,结合 Maybe函子 中提供的业务案例改写成 ap函子 的写法

const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, name, sellPrice) => {
  Object.assign(target, {
    name,
    ...sellPrice
  })
})
const resolveProp = (f, prop) => maybe(Maybe.of(null), f, prop)
_map(product => {
  Ap.of(resolveProduct(product))
    .ap(resolveProp(formatName, Maybe.of(product.name)))
    .ap(resolveProp(formatPrice, Maybe.of(product.sellPrice)))
}, products)

有一说一,有没有感觉逼格突然就高上去了?刚才的不优雅变得优雅了许多?

IO函子

  • 很多时候我们不可能做到完全的纯函数,多多少少会在函数的内部调用或者访问某些外部的方法或者值,我们之前通过柯里化的方式延迟执行这个副作用,但这种做法其实不算太好,毕竟谁也不是鸵鸟,头埋在沙子里就当看不见?
  • 结合业务咱来举个例子,比如有时候商品数量过多而且有分类的情况下我们往往会为了节约带宽而将数据缓存到全局的 Map 中,当切换到那个分类时就直接从 Map 中获取该分类的 uid 所对应的数据,如果没有则再去请求

下面这个案例中,第一次调用 getProductsByCache 时获得 getProductsByUid 方法,通过 getProductsByUid 方法可以获取该分类的具体商品

const productsMap = {
  '3211011': [{
    name: 'any-one',
    sellPrice: '9.9',
  }, {
    name: 'aeo-rus',
    sellPrice: '29.9',
  }]
}
/* 普通纯函数写法预期输出
{
  '3211011': [{
    name: 'any-one',
    sellPrice: '9.9',
  }, {
    name: 'aeo-rus',
    sellPrice: '29.9',
  }],
  '3211012': [{
    name: 'anyone',
    sellPrice: '39.9',
  }, {
    name: 'aeorus',
    sellPrice: '49.9',
  }]
}
*/
/* IO函子 预期输出
{
  '3211011': [
    { name: 'anyone', sellPrice: '9.9', int: '9', float: '9' },
    { name: 'aeorus', sellPrice: '29.9', int: '29', float: '9' }
  ],
  '3211012': [
    { name: 'anyone', sellPrice: '39.9', int: '39', float: '9' },
    { name: 'aeorus', sellPrice: '49.9', int: '49', float: '9' }
  ]
}
*/
const getProductsByRequest = async uid => {
  try {
    let result
    if (!Object.prototype.hasOwnProperty.call(productsMap, uid)) {
      await Promise.resolve([{
        name: 'anyone',
        sellPrice: '39.9',
      }, {
        name: 'aeorus',
        sellPrice: '49.9',
      }]).then(products => {
        result = productsMap[uid] = products
      })
    } else {
      result = productsMap[uid]
    }
    return result
  } catch (e) {
    return Promise.reject()
  }
}
const getProductsByCache = () => {
  const getProductsByUid = async uid => {
    const result = await getProductsByRequest(uid)
    return result
  }
  return getProductsByUid
}
const productRequest = getProductsByCache()
productRequest(3211011).then(products => {
  console.log(products)
})
productRequest(3211012).then(products => {
  console.log(products)
})
setTimeout(() => {
  productRequest(3211012).then(products => {
    console.log(products)
  })
}, 1000)
  • IO函子 中的 __value 是一个函数
  • IO函子 可以将不纯的操作储存到 __value 中,延迟执行这个不纯的操作
  • 将不纯的操作交由调用者来处理
class IO {
  static of(f) {
    return new IO(() => f)
  }
  constructor(f) {
    this.__value = f
  }
  map(f) {
    return new IO(compose(f, this.__value))
  }
}

接下来,结合 ap函子 中提供的案例的业务逻辑改写上面案例的普通写法

const resolveProduct = curry((target, name, sellPrice) => {
  Object.assign(target, {
    name,
    ...sellPrice
  })
})
const resolveProp = (f, prop) => maybe(Maybe.of(null), f, prop)
const getProductsByRequest = IO.of(async uid => {
  try {
    let result
    if (!Object.prototype.hasOwnProperty.call(productsMap, uid)) {
      await Promise.resolve([{
        name: 'anyone',
        sellPrice: '39.9',
      }, {
        name: 'aeorus',
        sellPrice: '49.9',
      }]).then(products => {
        result = productsMap[uid] = products
      })
    } else {
      result = productsMap[uid]
    }
    return result
  } catch (e) {
    return Promise.reject()
  }
})
const getProducts = uid => _map(
  compose(
    _then(
      _map(product => {
        Ap.of(resolveProduct(product))
          .ap(resolveProp(formatName, Maybe.of(product.name)))
          .ap(resolveProp(formatPrice, Maybe.of(product.sellPrice)))
        // trace(productsMap)
      })
    ),
    _apply([uid])
  ),
  getProductsByRequest
)
getProducts(3211011).__value()
getProducts(3211012).__value()
setTimeout(() => {
  getProducts(3211012).__value()
}, 1000)

IO函子的作用就在于什么传参都不用管,我先给方法都写完,保证了我本身是绝对纯的,至于最后调用时纯不纯那是你者的锅,反正我是不粘锅

demo 和 utils

在文章中的 demo 和引用的方法将补充在下方,大家可以下载下来一个一个测着玩儿,我觉得我编得还算不错就是了

Container.js

export class Container {
  static of(x) {
    return new Container(x)
  }
  constructor(x) {
    this.__value = x
  }
  map(f) {
    return Container.of(f(this.__value))
  }
}

Maybe.js

import { Container } from './container.js'
import { curry, _prop, _map, isObject } from './utils.js'

export class Maybe extends Container {
  static of(x) {
    return new Maybe(x)
  }
  isNothing() {
    return this.__value === null || this.__value === undefined || !Object.keys(this.__value).length
  }
  map(f) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value))
  }
}
export const maybe = curry((x, f, m) => m.isNothing() ? x : f(m))

const products = [{
  name: 'any-one',
  sellPrice: '9.9',
}, {
  name: 'aeo-rus',
  sellPrice: '29.9',
}, {
  name: 'no-price',
}, {
  sellPrice: '49.9',
}]
const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, f, key) => {
  const sourceValue = target.map(_prop(key))
  const formatValue = maybe(Maybe.of(null), f, sourceValue)
  const result = formatValue.map(value => isObject(value) ? value : {
    [key]: value
  })
  return result.__value
})
_map(product => {
  const resolveProp = resolveProduct(Maybe.of(product))
  Object.assign(product, {
    ...resolveProp(formatName, 'name'),
    ...resolveProp(formatPrice, 'sellPrice'),
  })
}, products)

Either.js

import { Maybe } from './Maybe.js'
import { curry, _prop, _map } from './utils.js'

class Either {
  static of(left, right) {
    return new Either(left, right)
  }
  constructor(left, right) {
    this.left = left
    this.right = right
  }
  map(f) {
    return this.right ?
      Either.of(this.left, f(this.right)) :
      Either.of(this.right, f(this.left))
  }
}

const userInfo = {
  address: '江苏省南京市江宁区'
}
// const userInfo = {
//   name: 'aeorus'
// }
// const userInfo = null
const regex = "(?<province>[^省]+省|.+自治区)(?<city>[^自治州]+自治州|[^市]+市|[^盟]+盟|[^地区]+地区|.+区划)(?<county>[^市]+市|[^县]+县|[^旗]+旗|.+区)?(?<town>[^区]+区|.+镇)?(?<village>.*)"
const match = curry((regex, target) => target.match(regex))
const adddress = Maybe.of(userInfo).isNothing() || Maybe.of(userInfo).map(_prop('address')).isNothing() ?
  Either.of('江苏省镇江市京口区', null) :
  Either.of(null, _prop('address', userInfo))
adddress.map(match(regex))

const parseJSON = (str) => {
  try {
    return Either.of(null, JSON.parse(str))
  } catch (e) {
    return Either.of({ error: e.message }, null)
  }
}
// Either { left: { error: 'Unexpected token n in JSON at position 2' }, null }
parseJSON('{ name: aeorus }')
// Either { left: null, right: { name: 'aeorus' } }
parseJSON('{ "name": "aeorus" }')

ap.js

import { Container } from "./Container.js"
import { Maybe, maybe } from "./Maybe.js"
import { _prop, _map, curry } from "./utils.js"

export class Ap extends Container {
  static of(x) {
    return new Ap(x)
  }
  ap(functor) {
    return Ap.of(this.__value(functor.__value))
  }
  map(f) {
    return Ap.of(f(this.__value))
  }
}

const products = [{
  name: 'any-one',
  sellPrice: '9.9',
}, {
  name: 'aeo-rus',
  sellPrice: '29.9',
}, {
  name: 'no-price',
}, {
  sellPrice: '49.9',
}]
const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, name, sellPrice) => {
  Object.assign(target, {
    name,
    ...sellPrice
  })
})
const resolveProp = (f, prop) => maybe(Maybe.of(null), f, prop)
_map(product => {
  Ap.of(resolveProduct(product))
    .ap(resolveProp(formatName, Maybe.of(product.name)))
    .ap(resolveProp(formatPrice, Maybe.of(product.sellPrice)))
}, products)

IO.js

import { curry, compose, _map, _apply, _then } from './utils.js'
import { Maybe, maybe } from "./Maybe.js"
import { Ap } from './ap.js'

class IO {
  static of(f) {
    return new IO(() => f)
  }
  constructor(f) {
    this.__value = f
  }
  map(f) {
    return new IO(compose(f, this.__value))
  }
}

const productsMap = {
  '3211011': [{
    name: 'any-one',
    sellPrice: '9.9',
  }, {
    name: 'aeo-rus',
    sellPrice: '29.9',
  }]
}
const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, name, sellPrice) => {
  Object.assign(target, {
    name,
    ...sellPrice
  })
})
const resolveProp = (f, prop) => maybe(Maybe.of(null), f, prop)
const getProductsByRequest = IO.of(async uid => {
  try {
    let result
    if (!Object.prototype.hasOwnProperty.call(productsMap, uid)) {
      await Promise.resolve([{
        name: 'anyone',
        sellPrice: '39.9',
      }, {
        name: 'aeorus',
        sellPrice: '49.9',
      }]).then(products => {
        result = productsMap[uid] = products
      })
    } else {
      result = productsMap[uid]
    }
    return result
  } catch (e) {
    return Promise.reject()
  }
})
const getProducts = uid => _map(
  compose(
    _then(
      _map(product => {
        Ap.of(resolveProduct(product))
          .ap(resolveProp(formatName, Maybe.of(product.name)))
          .ap(resolveProp(formatPrice, Maybe.of(product.sellPrice)))
      })
    ),
    _apply([uid])
  ),
  getProductsByRequest
)
getProducts(3211011).__value()
getProducts(3211012).__value()
setTimeout(() => {
  getProducts(3211012).__value()
}, 1000)

utils.js

export const curry = f => {
  return function inner(...args) {
    if (f.length === args.length) {
      return f.apply(this, args)
    } else {
      return function (...rest) {
        return inner(...args.concat(rest))
      }
    }
  }
}
const is = type => target => Object.prototype.toString.call(target) === `[object ${type}]`
export const isObject = is('Object')
export const isArray = is('Array')
export const isFunction = is('Function')
export const compose = (...args) => {
  args = args.reverse()
  return value => args.reduce((prev, curv) => curv(prev), value)
}
export const _prop = curry((key, target) => target[key])
export const _map = curry((f, target) => target.map(f))
export const trace = x => {
  console.log(x)
  return x
}
export const _filter = curry((f, target) => target.filter(f))
export const _split = curry((regex, target) => target.split(regex))
export const _apply = curry((args, target) => target.apply(this, args))
export const _then = curry((f, target) => target.then(f))

写在最后

至此,函数式编程算是都讲完了,尤其是函子我也就当都讲完了,虽然还有什么 Pointed函子 / Task函子 / Monad函子,但有一说一,这几个就够一壶了,可以了。
函数式编程这块儿大概看了三天,函子占了两天半,从完全搞不清应用场景到突然顿悟“大概应该这样这样用”花了二又四分之一天。我也不知道我的顿悟算不算真正的顿悟,也许我所谓的对的应用场景其实仍旧是错误的,看官们如果有独到的见解,也请帮忙指出,一起学习。
那就这样吧,咱下一章见。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值