函数式编程

182 阅读8分钟
  • 函数式编程是随着 React 的流行收到越来越多的关注
  • Vue3也开始拥抱函数式编程
  • 函数式编程可以抛弃 this
  • 打包过程中可以更好地利用 tree shaking 过滤无用代码
  • 方便测试,方便并行处理
  • 有很多库可以帮助我们进行函数式开发: lodash underscore ramda

函数式编程的概念

函数式编程的思维方式

把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)

  • 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
  • x->f(联系,映射)->y, y=f(x)
  • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,如:y=sin(x)
  • 相同的输入始终要的到相同的输出(纯函数)
  • 函数式编程用来描述数据(函数)之间的映射

高阶函数

什么是高阶函数

  • 可以把函数作为参数传递给另一个函数
  • 可以把函数作为另一个函数的返回结果
// 高阶函数-函数作为参数

function forEach (array, fn) {
    for (let i = 0; i < array.length; i++) {
        fn(array[i], i)
    }
}

function filter (array, fn) {
    let results = []
    for (let i = 0; i < array.length; i++) {
        if (fn(array[i], i)) {
            results.push(array[i])
        }
    }
    return results
}

// 高阶函数-函数作为返回值

function makeFn () {
    let msg = 'Hello Function'
    return function () {
        console.log(msg)
    }
}

function once (fn) {
    let done = false
    return function () {
        if (!done) {
            done = true
            return fn.apply(this, arguments)
        }
    }
}

let pay = once(function (money) {
    console.log(`支付: ${money} RMB`)
})

pay(5)
pay(5)
pay(5)
pay(5)

使用高阶函数的意义

  • 抽象可以帮我们屏蔽细节,只需要关注我们的目标
  • 高阶函数式用来抽象通用的问题
const map = (array, fn) => {
  let results = []
  for (value of array) {
    results.push(fn(value))
  }
  return results
}
// 测试
let arr = [1, 2, 3, 4]
arr = map(arr, v => v * v)

const every = (array, fn) => {
  let result = true
  for (let value of array) {
    result = fn(value)
    if (!result) {
      break
    }
  }
  return result
}
// 测试
let arr = [11, 12, 14]
let bool = every(arr, v => v > 10)


const some = (array, fn) => {
  let result = false
  for (let value of array) {
    result = fn(value)
    if (result) {
      break
    }
  }
  return result
}
// 测试
let arr = [1, 3, 4, 9]
let bool = some(arr, v => v % 2 === 0)

闭包

  • 闭包(Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
  • 可以在另一个作用域中调用一个函数内部函数并访问到该函数的作用域中的成员

闭包延长了外部函数变量的作用范围和时间

function makeFn () {
    let msg = 'Hello Function'
    return function () {
        console.log(msg)
    }
}

function once (fn) {
    let done = false
    return function () {
        if (!done) {
            done = true
            return fn.apply(this, arguments)
        }
    }
}

let pay = once(function (money) {
    console.log(`支付: ${money} RMB`)
})

闭包的本质

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

纯函数

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

let array = [1, 2, 3, 4, 5]

// 纯函数 slice
console.log(array.slice(0, 3)) // [1, 2, 3]
console.log(array.slice(0, 3)) // [1, 2, 3]
// 非纯函数 splice
console.log(array.splice(0, 3)) // [4, 5]
console.log(array.splice(0, 3)) // []
  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数处理

纯函数的好处

  • 可缓存 (纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来)
function getArea (r) {
  // 相同参数只会打印一次
  console.log(r)
  return Math.PI * r *r
}

function memoize (f) {
  let cache = {}
  return function () {
    let key = JSON.stringify(arguments)
    cache[key] = cache[key] || f.apply(f, arguments)
    console.log('cache', cache)
    return cache[key]
  }
}
let t = memoize(getArea)
console.log(t(4))
console.log(t(4))
  • 可测试 (纯函数让测试更方便)
  • 并行处理
    • 在多线程环境下并行操作共享的内存数据很可能出现意外情况
    • 纯函数不需要访问共享的内存数据,所以并行环境下可以任意运行纯函数(Web Worker)

副作用

副作用让一个函数变得不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用

副作用的来源

  • 配置文件
  • 数据库
  • 获取用户的输入
  • ......

所有的外部交互都有可能带有副作用,副作用也是的方法通用性下降不是和扩展和可重用性,同时副作用会给程序中带来安全隐患和不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生

柯里化

  • 当一个函数有多个参数的时候西安传递一部分参数调用它(这部分参数以后永远不变)
  • 然后返回一个新的函数接收剩余的参数,返回结果
function checkAge (age) {
  // 存在硬编码
  let min = 18
  return age >= min
}

// 普通纯函数
function checkAge (min, age) {
  return age >= min
}

// 柯里化
function checkAge (min) {
  return function (age) {
    return age >= min
  }
}

// es6 写法
let checkAge = min => age => age >= min

// console.log(checkAge(18)(20))
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)

checkAge18(24)
checkAge20(8)

柯里化案例

const _ = require('lodash')

const match = _.curry((reg, str) => {
  return str.match(reg)
})

const haveSpace = match(/\s+/g)
console.log(haveSpace('hello world'))
console.log(haveSpace('helloworld'))

const haveNumber = match(/\d+/g)
console.log(haveNumber('121abc'))
console.log(haveNumber('dsad'))

const filter = _.curry((func, array) => {
  return array.filter(func)
})

console.log(filter(haveSpace, ['John Connor', 'John_Donne']))

const findSpace = filter(haveSpace)
console.log(findSpace(['John Connor', 'John_Donne']))

模拟柯里化函数

function getSum (a, b, c) {
  return a + b + c
}

function curry (func) {
  return function curriedFn (...args) {
    // 判断实参和形参的个数
    if (args.length < func.length) {
      return function () {
        return curriedFn(...args.concat(Array.from(arguments)))
      }
    }
    return func(...args)
  }
}

const curried = curry(getSum)

console.log(curried(1)(2)(3))

柯里化总结

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对函数参数的缓存 (利用了闭包的特性)
  • 让函数变得更灵活,让函数的粒度更小
  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

函数组合(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, 3, 4, 5]))

模拟lodash中的flowRight

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']))

function compose (...args) {
  return function (value) {
    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 compose = (...args) => (value) => args.reverse().reduce((acc, fn) => fn(acc), value)

const f = compose(toUpper ,first, reverse)
console.log(f(['one', 'two', 'three']))

函数组合的调试

const _ =  require('lodash')

// 打印 调试函数
const trace = _.curry((tag, v) => {
   console.log(tag, v)
   return v
})
// _.split()
const split = _.curry((sep, str) => _.split(str, sep))

// _.toLower()

// _.join()
const join = _.curry((sep, array) => _.join(array, sep))

// _.map()
const map = _.curry((fn, array) => _.map(array, fn))

const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('split 之后'), split(' '))

console.log(f('KOBE BRYANT'))

lodash中的fp模块提供了大量已经柯里化方便我们直接进行函数组合的函数

函子(Functor)

什么是函子

  • 容器: 包含值和值的变形关系(这个变形关系就是函数)
  • 函子: 是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)

Fucntor 函子

// Functor 函子
class Container {

  static of (value) {
    return new Container(value)
  }

  constructor (value) {
    this._value = 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)

总结

  • 函数式编程的运算不直接操作值,而是有函子完成
  • 函子就是一个实现了map 契约的对象
  • 我们可以吧函子想象成一个盒子,这个盒子里封装了一个值
  • 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
  • 最终 map 方法返回一个包含新值的盒子(函子)

MayBe 函子

  • 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
  • MayBe 函子的作用就是可以对外部的空值做处理(控制副作用在允许的范围)
class MayBe {

  static of (value) {
    return new MayBe(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
  }

  isNothing () {
    return this._value === null || this._value === undefined
  }
}

let r = MayBe.of('hello world')
          .map(x => x.toUpperCase())
          .map(x => null)
          .map(x => x.split(' '))

console.log(r)

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))
  }
}


function parseJson (str) {
  try{
    return Right.of(JSON.parse(str))
  } catch (e) {
    return Left.of({ error: e.message })
  }
}

let r = parseJson('{name: zs}')
console.log(r)

let res = parseJson('{"name": "biki"}')
            .map(x => x.name.toUpperCase())
console.log(res)

IO 函子

  • IO 函子中的 _value 是一个函数,这里是把函数作为值来处理了
  • IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
  • 把不纯的操作交给调用者来处理
const fp = require('lodash/fp')

class IO {

  static of (value) {
    return new IO(function () {
      return 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())

Folktale

  • folktale 是一个标准的函数式编程库
  • 提供了一些函数式处理的操作,如 composecurry等,一些函子 TaskEitherMayBe

const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')

let f = curry(2, (x, y) => {
  return x + y
})

console.log(f(1, 3))
console.log(f(1)(2))

let fn = compose(toUpper, first)

console.log(fn(['one', 'two']))

Task 异步编程

const fs = require('fs')
const { split, find } =  require('lodash/fp')
const { task } = require('folktale/concurrency/task')

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()
  .listen({
    onRejected: (err) => {
      console.log(err)
    },
    onResolved: (value) => {
      console.log(value)
    }
  })

Pointed 函子

  • Pointed 函子是实现了 of 静态方法的函子
  • of 方法是为了避免使用 new 来创建对象,更深的含义是 of 方法用来把值放到上下文 Context (把值放到容器中, 使用 map 来处理值)

Monad 函子

  • Monad 函子是可以变扁的Pointed 函子
  • 一个函子如果具有 joinof 两个方法并遵守一些定律就是一个 Monad
const fs = require('fs')
const fp = require('lodash/fp')

class IO {

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

  constructor (fn) {
    this._value = fn
  }

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

  join () {
    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 () {
    console.log(x)
    return x
  })
}

// let cat = fp.flowRight(print, readFile)

// let r = cat('package.json')._value()._value()
// console.log(r)

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

总结

  • 当一个函数返回一个函子的时候,我们需要用到 monad,monad 可以帮我们解决函子嵌套的问题
  • 当我们想要合并一个函数,这个函数返回一个值,我们用 map,如果这个函数返回的是一个函子,我们用 flatMap

总结

截屏2021-10-01 03.26.07.png