js - 函数式编程

ru shui 2021-09-05 Javascript
  • Javascript
  • Functional programming
About 11 min

# 1.函数式编程优势

  • 更少的时间
  • 更少的 BUG
  • 更好的测试性
  • 更方便调试
  • 适合并发执行
  • 更高的复用性
  • 支持 tree-shaking
  • React 和 Vue3 大量使用函数式编程

# 2.什么是函数式编程

  • 函数式编程是一种编程范式
// 面向过程
let a = 1
let b = 2
let result = a + b

// 面向对象
class Sum {
  add(a, b) {
    return a + b
  }
}
let sum = new Sum()
sum.add(1, 2)

// 函数式编程,这里的函数指的是一种映射关系 y=f(x)

function add(a, b) {
  return a + b
}
add(1, 2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3.First-class Function(头等函数)

  • 函数是头等函数 (opens new window)
    • 函数可以赋值给变量
    • 函数可以作为参数
    • 函数可以作为返回值
  • 作为参数和返回值的函数被称为高阶函数,高阶函数是函数式编程的基础
function add(a, b) {
  return a + b
}
//1. 函数可以赋值给变量
let add1 = add

//2. 函数可以作为参数
function exec(fn, a, b) {
  fn(a, b)
}

//3. 函数可以作为返回值
function exec(fn) {
  return function (a, b) {
    return fn(a, b)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 4.闭包(closure)

  • 一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包 (opens new window)
  • 闭包让你可以在一个内层函数中访问到其外层函数的作用域的变量
function init() {
  var name = 'hello'
  function showName() {
    debugger
    console.log(name)
  }
  return showName
}
let showName = init()
showName()
1
2
3
4
5
6
7
8
9
10

# 5.纯函数

# 5.1 什么是纯函数

  • 函数的返回结果只依赖于它的参数,相同的输入始终得到相同的输出
  • 函数执行过程里面没有副作用(一个函数执行过程对产生了外部可观察的变化那么就可以说这个函数是有副作用)
function add(a, b) {
  return a + b
}

let c = 1
let d = 2
function add2(a, b) {
  d++ // 修改了外部变量
  return a + b + c // 计算结果依赖外部变量
}
add2()
console.log(d)
1
2
3
4
5
6
7
8
9
10
11
12

# 5.2 优点

# 5.2.1 可缓存

  • lodash.memoize (opens new window)
  • 创建一个会缓存 func 结果的函数。如果提供了 resolver,就用 resolver 的返回值作为 key 缓存函数的结果。默认情况下用第一个参数作为缓存的 key。func 在调用时 this 会绑定在缓存函数上。
npm i lodash -S
1
let _ = require('lodash')
const add = (a, b) => {
  console.log('add')
  return a + b
}
const resolver = (...args) => JSON.stringify(args)

var memoize = function (func, resolver) {
  let cache = {}
  let memoized = (...args) => {
    const key = resolver ? resolver(...args) : JSON.stringify(args)
    if (typeof cache[key] !== 'undefined') {
      return cache[key]
    } else {
      cache[key] = func(...args)
      return cache[key]
    }
  }
  memoized.cache = cache
  return momized
}

// let momoizedAdd = _.memoize(add, resolver);
let memoizedAdd = memoize(add, resolver)
console.log(memoizedAdd(1, 2))
console.log(memoizedAdd(1, w))
console.log(memoizedAdd.cache)
module.exports = momoizedAdd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 5.2.2 可测试

cnpm install jest --save-dev
1

# 5. test.js

const sum = require('./5.js')
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2).toBe(3))
  expect(sum(1, 2).toBe(3))
})
1
2
3
4
5
{
  "scripts": {
    "test": "jest"
  }
}
1
2
3
4
5

# 6.柯里化

  • lodash.curry (opens new window)创建一个函数,该函数接收 func 的参数,要么调用 func 返回的结果,如果 func 所需参数已经提供,则直接返回 func 所执行的结果。或返回一个函数,接受余下的 func 参数的函数,可以使用 func.length 强制需要积累的参数个数
let _ = require('lodash')
function add(a, b, c) {
  return a + b + c
}
function curry(func) {
  let curried = (...args) => {
    if (args.length < func.length) {
      return (...rest) => curried(...args, ...rest)
    }
    return func(...args)
  }
  return curried
}
let curriedAdd = curry(add)
console.log(curriedAdd(1, 2, 3))
console.log(curriedAdd(1)(2, 3))
console.log(curriedAdd(1)(2)(3))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7.组合

手工组合

function add1(str) {
  return str + 1
}
function add2(str) {
  return str + 2
}
function add3(str) {
  return str + 3
}
// 手工组合
console.log(add3(add2(add1('hello'))))
1
2
3
4
5
6
7
8
9
10
11

flow

// let {flow} = require("lodash");
function add1(str) {
  return str + 1
}
function add2(str) {
  return str + 2
}
function add3(str) {
  return str + 3
}
// 手工组合
function flow(...fns) {
  if (fns.length == 0) {
    return fns[0]
  }
  return fns.reduceRight(
    (a, b) =>
      (...args) =>
        a(b(...args)),
  )
}
let flowed = flow(add3, add2, add1)
console.log(flowed('zhufeng'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

flowRight

let {flowRight} = require("lodash");
function add1(str) {
    return str + 1;
}
function add2(str) {
    return str + 2
}
function add3(str) {
    return str + 3;
}
function flowRight(...fns) {
    if(fns.length == 0) {
        return fn[0];
    }
    return fns.reduce((a, b) = (...args) => a(b(...args)));
}
let flowed = flowRight(add3, add2, add1);
console.log(flowed("zhufeng"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

带参数的函数组合

let { split, toUpper, join } = require('lodash')
let str = 'click button' // CLICK_BUTTON

let r1 = split(str, ' ')
console.log(r1) //['click', 'button']
let r2 = toUppser(r1)
console.log(r2) // CLICK, BUTTON
let r3 = split(r2, ',')
console.log(r3) //['CLICK', 'BUTTON']
let r4 = join(r3, '_')
console.log(r4) // CLICK_BUTTON
1
2
3
4
5
6
7
8
9
10
11

数据先放

let { split, toUppser, join, flowRight } = require('lodash')
let str = 'click button' // CLICK_BUTTON
// lodash数据放前面
const func = flowRight(join('_'), split(','), toUpper, split(' '))
console.log(func(str))
1
2
3
4
5

数据后放

let { split, toUpper, join, flowRight } = require('lodash/fp')
let str = 'click button' // CLICK_BUTTON
// fp数据放后
const func = flowRight(join('_'), split(','), toUpper, split(' '))
console.log(func(str))
1
2
3
4
5

过程跟踪

let { split, toUpper, join, flowRight } = require('lodash/fp')
let str = 'click button' // CLICK_BUTTON
const logger = name => value => {
  console.log(name, value)
  return value
}
const func = flowRight(
  join('_'),
  logger('afterSplit'),
  split(','),
  logger('afterToUpper'),
  toUpper,
  split(' '),
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 8.Pointfree

  • 把数据处理的过程先定义成一种与参数无关的合成运算就叫 Pointfree
  • 先想怎么花钱
const { compose } = require('lodash/fp')
let num = 1
// Pointed
function calcu(num) {
  return num + 1 + 2 + 3
}
function add1(num) {
  return num + 1
}
function add2(num) {
  return num + 2
}
function add3(num) {
  return num + 3
}
// Pointfree
let fn = compose(add3, add2, add1)
console.log(fn(1))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 9.函子

  • 可以用来管理值和值的变化过程
  • 把异常和异步操作等副作用在可控的范围之内

# 9.1 Context

  • 如果一个对象内部持有一个值那么它就可以称为容器(Container)

# 9.2 Pointed Container

  • 如果它有 of 方法可称为有指向的容器
class PointedContainer {
  constructor(value) {
    this.value = value
  }
  static of(value) {
    return new PointedContainer(value)
  }
}
1
2
3
4
5
6
7
8

# 9.3 Functor

  • 如果它有 map 方法可称为 Functor(函子)
  • 函子一定有一个静态的 of 方法,用来生成实例
  • 函子内部会保存一个值 value
  • 函子提供 map 方法,接入各种运算函数,从而引发值的变化
class Functor {
  constructor(value) {
    this.value = value
  }
  static of(value) {
    return new Functor(value)
  }
  map(fn) {
    return new Functor(fn(this.value))
  }
}

let result = Functor.of('a')
  .map(x => x + 1)
  .map(x => x + 2)
  .map(x => x + 3)
console.log(result.value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 9.4 Maybe

  • 容器内部的值可能是一个空值,而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错
  • Maybe 函子可以过滤空值,能过滤空值的函子被称为 Maybe 函子
class Maybe {
  constructor(value) {
    this.value = value
  }
  static of(value) {
    return new Maybe(value)
  }
  map(fn) {
    return this.value ? new Maybe(fn(this.value)) : this
  }
}
Maybe.of(null).map(x => x.toString())
1
2
3
4
5
6
7
8
9
10
11
12

# 9.5 Either

  • Either 函子内部有两个值: 左值 Left)和右值(Right),右值是正常情况下使用的值,左值是右值不存在时使用的默认值
  • 常用来设置默认值和处理异常
class Either {
  constructor(left, right) {
    this.left = left
    this.right = right
  }
  static of = function (left, right) {
    return new Either(left, right)
  }
  map(fn) {
    return this.right
      ? Either.of(this.left, fn(this.right))
      : Either.of(fn(this.left), this.right)
  }
  get value() {
    return this.right || this.left
  }
}
//处理默认值
let user = { gender: null }
let result = Either.of('男', user.gender).map(x => x.toUpperCase())
console.log(result.value)

//处理异常
function parse(str) {
  try {
    return Either.of(null, { data: JSON.parse(str) })
  } catch (error) {
    return Either.of({ error: error.message }, null)
  }
}
console.log(parse('{}').value)
console.log(parse('{x}').value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 9.6 ap

  • ap(applicative)的函子拥有 ap 方法
  • ap 方法可以让一个函子内的函数使用另一个函子的值进行计算
  • ap 方法的参数不是函数,而是另一个函子
class Ap {
  constructor(value) {
    this.value = value
  }
  static of(value) {
    return new Ap(value)
  }
  map(fn) {
    return new Ap(fn(this.value))
  }
  ap(functor) {
    return Ap.of(this.value(functor.value))
  }
}

const A = Ap.of(x => x + 1)
const B = Ap.of(1)
let result = A.ap(B)
console.log(result)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 9.7 Monad 函子

  • 函子的值也可以是函子,这样会出现多层函子嵌套的情况
  • Monad(单子[不可分割的实体]) 函子的作用是,总是返回一个单层的函子
  • 它有一个 flatMap 方法,与 map 方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况

函子嵌套

class Functor {
  constructor(value) {
    this.value = value
  }
  static of(value) {
    return new Functor(value)
  }
  map(fn) {
    return new Functor(fn(this.value))
  }
}

let result = Functor.of('a')
  .map(x => Functor.of(x + 1))
  .map(x => Functor.of(x.value + 2))
  .map(x => Functor.of(x.value + 3))
console.log(result.value.value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let r1 = [1, 2, 3].map(item => [item + 1])
console.log(r1) //[ [ 1, 1 ], [ 2, 2 ], [ 3, 3 ] ]
console.log(r1[0][0])
let r2 = [1, 2, 3].flatMap(item => [item + 1])
console.log(r2)
console.log(r2[0])
1
2
3
4
5
6
class Monad {
  constructor(value) {
    this.value = value
  }
  static of(value) {
    return new Monad(value) //a
  }
  map(fn) {
    return new Monad(fn(this.value))
  }
  join() {
    return this.value
  }
  flatMap(fn) {
    return this.map(fn).join()
  }
}

/* let result = Monad.of('a')
   .flatMap(x=>{
       let r = Monad.of(x+1);
       r.name = 'x1';
       return r;
   })//new Monad('a1')); 返回的Monad.value=Monad.of(x+1),后面用的是Monad.of(x+1)
console.log(result); */

let result = Monad.of('a')
  .flatMap(x => Monad.of(x + 1))
  .flatMap(x => Monad.of(x + 2))
  .flatMap(x => Monad.of(x + 3))
console.log(result.value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组
let r1 = Array.of(1, 2, 3).map(x => x + 1)
console.log(r1)
let r2 = Array.of(1, 2, 3).map(x => [x + 1])
console.log(r2)
let r3 = Array.of(1, 2, 3).flatMap(x => [x + 1])
console.log(r3)
1
2
3
4
5
6
7

# 9.8 IO 函子与副作用

  • 副作用就是程序和外部世界的交互,比如读取文件或调用接口
  • 由于外部世界不可控,包含副作用的逻辑往往不要预测
  • 函数式编程提倡把副作用分离出来,让没有副作用的纯逻辑们放在一起远离包含副作用的逻辑,这时就需要 IO Monad
  • IO 就是 Input/Output,副作用无非是对外部世界的 Input(读)和 Output(写)
  • IO 函子通过推迟执行的方式来实现对副作用的管理和隔离 过程调用
const { compose } = require('lodash/fp')
let localStorage = {
  getItem(key) {
    if (key === 'data') return `{"code":0,"userId":"1"}`
    else if (key === '1') {
      return `{"id":1,"name":"zhangsan","age":18}`
    }
  },
}
function printUsers() {
  const response = localStorage.getItem('data')
  const data = JSON.parse(response)
  const users = data.userId
  const user = localStorage.getItem(data.userId)
  console.log(user) //输出副作用
}
printUsers()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

IO 函子

const { compose } = require('lodash/fp')
let localStorage = {
  getItem(key) {
    if (key === 'data') return `{"code":0,"userId":"1"}`
    else if (key === '1') {
      return `{"id":1,"name":"zhangsan","age":18}`
    }
  },
}
class IO {
  constructor(value) {
    this.value = value
  }
  map(fn) {
    return new IO(compose(fn, this.value))
  }
  join() {
    return this.value()
  }
  start(callback) {
    callback(this.value())
  }
}
const readByKey = key => new IO(() => localStorage.getItem(key))
const parseJSON = string => JSON.parse(string)
const writeToConsole = console.log
readByKey('data').map(parseJSON).start(writeToConsole)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

链式调用

const { compose } = require('lodash/fp')
let localStorage = {
  getItem(key) {
    if (key === 'data') return `{"code":0,"userId":"1"}`
    else if (key === '1') {
      return `{"id":1,"name":"zhangsan","age":18}`
    }
  },
}
class IO {
  constructor(value) {
    this.value = value
  }
  map(fn) {
    return new IO(compose(fn, this.value))
  }
  flatMap(fn) {
    return new IO(compose(x => x.value(), fn, this.value))
  }
  start(callback) {
    callback(this.value())
  }
}
const readByKey = key => new IO(() => localStorage.getItem(key))
const parseJSON = string => JSON.parse(string)
const writeToConsole = console.log
let ret = readByKey('data')
  .map(parseJSON)
  .map(item => item.userId)
  .flatMap(readByKey)
  .map(parseJSON)
  .start(writeToConsole)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 9.9 task 函子

  • Task 函子通过类似 Promise 的 resolve 的风格来声明一个异步流程
  • FP 中除了容器(Container),也可以用上下文(Context)来称呼包裹了一个值的结构
  • Promise 的任务是立刻执行的,而 Task 是在调用的时候才开始执行 异步执行任务
const Task = execute => ({
  execute,
})
function get(url) {
  return Promise.resolve({ code: 0, userId: '1' })
}
const request = url =>
  Task((resolve, reject) => get(url).then(resolve, reject))
request('data').execute(
  user => console.log(user),
  error => console.error(error),
)
1
2
3
4
5
6
7
8
9
10
11
12

实现 map

const Task = execute => ({
  execute,
  map: fn => Task(resolve => execute(x => resolve(fn(x)))),
})
function get(url) {
  return Promise.resolve({ code: 0, userId: '1' })
}
const request = url =>
  Task((resolve, reject) => get(url).then(resolve, reject))
request('data')
  .map(x => x.userId)
  .execute(
    user => console.log(user),
    error => console.error(error),
  )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

实现 flatMap

const Task = execute => ({
  map: fn => Task(resolve => execute(x => resolve(fn(x)))),
  flatMap: fn => Task(resolve => execute(x => fn(x).execute(resolve))),
  execute,
})
function get(url) {
  if (url === 'data') return Promise.resolve({ code: 0, userId: '1' })
  else if (url === '1') {
    return Promise.resolve({ id: 1, name: 'zhangsan', age: 18 })
  }
}
const request = url => Task(resolve => get(url).then(resolve))
request('data')
  .map(x => x.userId)
  .flatMap(request)
  .map(x => x.name)
  .execute(user => console.log(user))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 10. 实际应用

# 10.1 react

# 10.2 vue

  • function-api (opens new window)
  • vue2 是将 mounted,data,computed,watch 之类的方法作为一个对象的属性进行导出。
  • vue3 新增了一个名为 setup 的入口函数,value, computed, watch, onMounted 等方法都需要从外部 import
<template>
  <div>
    <span>count is {{ count }}</span>
    <span>plusOne is {{ plusOne }}</span>
    <button @click="increment">count++</button>
  </div>
</template>
<script>
import { value, computed, watch, onMounted } from 'vue'
export default {
  setup() {
    // reactive state
    const count = value(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => {
      count.value++
    }
    // watch
    watch(
      () => count.value * 2,
      val => {
        console.log(`count * 2 is ${val}`)
      },
    )
    lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment,
    }
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Last update: September 10, 2021 20:58
Contributors: Laishuxin , laishuxin170