前端进阶之路 - part1 -【函数式编程】

195 阅读5分钟
什么是函数式编程:
  1. 描述数据之间的映射关系
  2. 有输入有输出,且相同的输入会得到相同的输出(纯函数)
函数式编程的作用:
  1. 可以抛弃this
  2. 打包过程中更好的利用Tree shaking过滤无用代码,(什么是tree sharking,怎样更好的过滤无用代码)
  3. 函数式编程可以重用,也可以组合成功能更强大新的函数

函数相关概念:

一等公民:【函数为第一公民是函数式编程的必要条件】
  1. 可以作为参数
  2. 可以作为返回值
  3. 也可以赋值给变量
高阶函数:
  1. 接收函数作为参数或将函数作为返回值输出
闭包:
  1. 闭包就是能够读取其他函数内部变量的函数。
  2. 例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。
  3. 在本质上,闭包是将函数内部和函数外部连接起来的桥梁
纯函数:【优点:不复杂、更容易调试、易于组合、易于并行化】
  1. 应始终返回相同的值
  2. 自包含【只传参】
  3. 它不应修改程序的状态或引起副作用,【其实本质意思就是前面两个概念之和】
柯里化:【可以把任意一个多元函数转化成一元函数(有多个参数的函数就是多元函数)】
  1. 柯里化 是一种转换,将 f(a,b,c) 转换为可以被以 f(a)(b)(c) 的形式进行调用。JavaScript 实现通常都保持该函数可以被正常调用,并且如果参数数量不足,则返回偏函数
  2. 即当函数有多个参数时,先传递给函数一部分参数来调用它(这部分参数以后永远不变),让它返回一个函数去处理剩下的参数,返回结果
函数组合
  1. 将两个或多个函数组合在一起以产生新函数的过程。将函数组合在一起就像将一系列管道对齐在一起以使我们的数据流过
  2. 函数组合默认从右到左,由内而外执行

函子相关概念:

  1. 函子是一个容器,包含了值和值的变形关系(变形关系指的是函数)
  2. 函子( representative functor ) 是范畴论里的概念,我们没有办法避免副作用,但是可以通过函子让副作用控制在可控范围内,同时也可以通过函子处理异常,异步等
Functor函子
  1. Functor 是实现了 map 函数并遵守一些特定规则的容器类型。
  2. 只有一个属性对象(它可以是任意类型的值)
  3. 实现 map 函数,它接收一个函数作为参数,并返回一个新的函子对象
  // 构造函数,创建函子对象的时候接收任意类型的值,并把值赋给它的私有属性 _value
  constructor(value) { 
    this._value = value
  }
 
  // 接收一个函数,处理值的变形并返回一个新的函子对象
  map (fn) {
    return new Functor(fn(this._value))
  }
}

let num1 = new Functor(3).map(val => val + 2)

console.log(num1) // 输出:Functor { _value: 5 }

let num2 = new Functor(3).map(val => val + 2).map(val => val * 2)

console.log(num2) // 输出:Functor { _value: 10 }

// 改变了值类型
let num3 = new Functor('webpack').map(val => `${val}-cli`).map(val => val.length)

console.log(num3) // 输出:Functor { _value: 11 }
Pointed 函子
  1. 实现了 of 静态方法的函子,被称为 Pointed 函子。它避免了使用 new 来创建对象,更深层次的含义是 of 方法用来把值放到上下文 context 中(即:把一个值放到容器中,可以使用 map 方法来处理值)
class Pointed {
  // 静态方法,避免使用 new 来创建对象
  static of (value) {
    return new Pointed(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    return Pointed.of(fn(this._value))
  }
}

let pointed = Pointed.of(3).map(val => val + 1).map(val => val * 2)

console.log(pointed) // 输出:Pointed { _value: 8 }

MayBe函子
  1. Maybe函子 会先检查自己的值是否为空,然后才调用传进来的函数,这样处理空值就不会出错了。
  2. Maybe函子 常用在那些可能无法成功返回结果的函数中;可以避免使用命令式的 if...else 语句,可以用 Maybe(null) 来表示失败,但却不能告诉我们太多有效信息,譬如:失败的原因是什么?是哪儿造成失败的?Either 函子能帮助我们解决这样的问题。
class Maybe {
  static of (value) {
    return new Maybe(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    // 检查自己的值是否为空
    return this._value ? Maybe.of(fn(this._value)) : Maybe.of(null)
  }
}

let toUpper = Maybe.of(null).map(x => x.toUpperCase())

console.log(toUpper) // 输出:Maybe { _value: null }

// 用在可能会无法成功返回结果的函数中
function getFirst (arr) {
  return Maybe.of(arr[0])
}

let firstElement = getFirst([]).map(x => x + 3)

console.log(firstElement) // 输出:Maybe { _value: null }

Either 函子
  1. 在普通的面向对象编程中,我们通常使用条件运算语句 if…else… 进行异常等方面的判断。而在函数式编程中,异常会让函数变得不纯,我们是用 Either 函子 进行异常处理
  2. 函数式编程里面,使用 Either 函子代替条件运算(if...else);另一个用途是代替 try...catch
  3. Either函子内部一般有两个值:左值(Left)右值(Right)使用左值表示错误,右值则相反
class Left {
  static of (value) {
    return new Left(value)
  }

  constructor (value) {
    this._value = value
  }

  // 【注意】这里直接返回的 this
  map (fn) {
    return this
  }
}

class Right {
  static of (value) {
    return new Right(value)
  }

  constructor (value) {
    this._value = value
  }

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

// Left 无视 map 它的函数
let left = Left.of('rain').map(str => `b${str}`)

console.log(left) // 输出:Left { _value: 'rain' }


let right = Right.of('rain').map(str => `b${str}`)

console.log(right) // 输出:Right { _value: 'rain' }


// 解析 JSON 字符串
function parseJson (jsonStr) {
  try {
    return Right.of(JSON.parse(jsonStr))
  } catch (e) {
    return Left.of(e.message)
  }
}

let error = parseJson('{name: zs}')

console.log(error) // 输出:Left { _value: 'Unexpected token n in JSON at position 1' }


let success = parseJson('{"name": "zs"}')

console.log(success) // 输出:Right { _value: { name: 'zs' } }

Monad 函子
  1. 函子是一个容器,可以包含任何值。因此,函子之中再包含一个函子也是完全合法的。但是,这样会出现多层嵌套的函子。
  2. 总是返回一个单层的函子,避免出现嵌套的情况。 它有一个 flatMap 方法,如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的是一个单层容器,避免出现嵌套的情况。
  3. Monad 函子的应用,就是实现 IO(输入、输出)操作
class Monad {
  static of (value) {
    return new Monad(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    return Monad.of(fn(this._value))
  }

  join() {
    return this._value
  }

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

// map 返回了一个函子,就会产生嵌套
let nested = Monad.of(3).map(val => Monad.of(val + 2))

// 输出:Monad { _value: Monad { _value: 5 } }
console.log(nested)

// 如果 map 返回的是函子,需要使用 flatMap 将其平铺
let monad = Monad.of(3).flatMap(val => Monad.of(val + 2))

// 输出:Monad { _value: 5 }
console.log(monad)
IO函子
  1. 在程序运算过程中,有很多函数会依赖外部环境,从而使函数变得不纯(副作用),IO函子就是解决这个问题的
  2. IO 函子跟之前的函子不同的地方在于,它内部的 _value 总是一个函数。把函数作为值来处理。
  3. IO函子可以把不纯的数据存储到_value中来处理,延迟执行这个不纯的操作,可以认为,IO 包含的是被包裹的操作的返回值。
  4. 把不纯的操作交给调用者来处理
const { compose } = require('ramda')
const fs = require('fs')

class IO {
  static of (value) {
    return new IO(function () {
      return value
    })
  }

  constructor (fn) {
    this._value = fn
  }

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

  join () {
    // 注意这行跟上面示例的区别,因为 _value 是一个函数,所以需要去调用
    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 () {
    return x
  })
}

// 如果 IO 函子是一个 Monad,具有 flatMap 方法,那么我们就可以像下面这样调用这两个函数
let file = readFile('package.json').flatMap(print).join()

console.log(file) // 会输出 package.json 文件的内容


// 另一种调用方式
let content = IO.of('package.json').flatMap(readFile).flatMap(print).join()

console.log(content) // 输出 package.json 文件的内容