函数式编程入门

127 阅读11分钟

为什么要学习函数式编程


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

什么是函数式编程


  • 是一种编程范式,与面向对象编程是同一级别的
  • 掌握一种编程范式需要很长时间

作用与意义


  • 提高代码的复用率
  • 可以更好的 Tree-Shaking,其原理就是依赖es6的模块化的语法。 举个例子:之前在 vue2 的组件中我们使用 watch 和 computed 等模块时,我们不需要手动的去引入,而在 vue3 的组件中我们需要使用某个模块都需要去手动的引入。把vue本身当一个对象去操作。那所有的这些API全部要用import的方式import进来。

函数式是一等公民


  1. 函数可以存储在变量中
  2. 函数作为参数
  3. 函数作为返回值

符合 2、3 两点的函数叫做高阶函数

// 把函数赋值给变量
let fn = function() {
  console.log('xxxx')
}
fn()

// 函数作为参数
function forEach (arr,fn) {
  for(let i = 0; i < arr.length; i++) {
    fn(arr[i])
  }
}
// 测试
forEach(['a', 'b', 'c'], function(item) {
  console.log('GodX------>log',item);
})

// 函数作为返回值
function makeFn () {
  return function() {
    console.log('GodX------>log');
  }
}
makeFn ()()

使用高阶函数的意义

  • 抽象可以帮我们来屏蔽细节,我们只需要关注于我们的目标 比如:使用 es6 的 filter 高阶函数,我们不需要知道它内部是怎么实现的,只需要知道会返回我们需要的数组即可。
  • 高阶函数是用来抽象通用的问题

常用的高阶函数(手写)


forEach

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

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

map

function map(arr,fn) {
  let result = [];
  for(let i of arr) {
    result.push(fn(i))
  }
  return result;
}

every

function every(arr,fn) {
  let result;
  for(let i of arr) {
    result = fn(i);
    if(!result) {
      break
    }
  }
  return result;
}

some

function every(arr,fn) {
  let result;
  for(let i of arr) {
    result = fn(i);
    if(result) {
      break
    }
  }
  return result;
}

闭包


闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除(包括函数内部的变量),但是堆上的作用域成员(函数内部声明的变量)因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。

优点

延伸函数内部变量的作用域

缺点

  • 常驻内存,增加内存使用量。
  • 使用不当会很容易造成内存泄露。

内存泄漏:是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

 function makePower (power) {
      return function (number) {
        return Math.pow(number, power) // 函数调用时,会产生 power 参数的闭包,见下图
      }
    }

    // 求平方
    let power2 = makePower(2)
    let power3 = makePower(3)

    console.log(power2(4))
    console.log(power2(5))
    console.log(power3(4))
image-20220808221025503

纯函数


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

// 纯函数和不纯的函数
// slice / splice

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

// 纯函数
console.log(array.slice(0, 3))
console.log(array.slice(0, 3))
console.log(array.slice(0, 3))

// 不纯的函数
console.log(array.splice(0, 3))
console.log(array.splice(0, 3))
console.log(array.splice(0, 3))

好处

  • 可缓存

    • 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
    // 记忆函数
    const _ = require('lodash')
    
    function getArea (r) {
      console.log(r)
      return Math.PI * r * r
    }
    
    function memoize(fn) {
      let cache = {}
      return function() {
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || fn.apply(fn, arguments)
        return cache[key]
      }
    }
    
    let getAreaWithMemory = memoize(getArea)
    console.log(getAreaWithMemory(4))
    console.log(getAreaWithMemory(4))
    console.log(getAreaWithMemory(4))
    

    因为计算过的结果会缓存在内存中,所以不用重新计算,控制台输出结果如下:

    image-20220405153039989

  • 可测试

    • 因为每个纯函数都有输入与输出,所以让测试更方便
  • 并行处理

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

副作用


副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互

副作用可能包含,但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

概括来讲,只要是跟函数外部环境发生的交互就都是副作用——这一点可能会让你怀疑无副作用编程的可行性。函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。

// 不纯的
let mini = 18
function checkAge (age) {
  return age >= mini // 外部变量会影响函数的返回值
}
// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge (age) {
  let mini = 18
  return age >= mini // 外部变量不会影响函数的返回值
}

柯里化


只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

Lodash中的柯里化函数

  • 功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提 供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
  • 参数:需要柯里化的函数,如果参数是一个纯函数,返回的函数也将会是纯函数
// lodash 中的 curry 基本使用 
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))

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

好处

  • 让函数变得更灵活,让函数的粒度更小
  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
  • 解决硬编码的问题
function checkAge (min, age) {
  return age >= min
}

硬编码:是指将可变变量用一个固定值来代替的方法。简单来说就是目标固定值写死,不可改变。

硬编码的危害:到后期维护修改的成本就会很高,代码的拓展灵活性就会很差,不易于有效性维护。

解决方案:统一写在一个文件中。

  • 延迟执行
Function.prototype.myBind = function(context,...arg) {
  return (...rest) => this.call(context,...arg,...rest);
}
function sum(a, b, c) {
  return a + b + c;
}
const test = sum.myBind(this,2);
const test2 = test.myBind(this,3)
console.log('zx------>log',test2(3)); // 8

实现原理

  • 就是使用了闭包对参数实现一个缓存。
function curry(fn) {
  return function curried(...args) {
    if(args.length < fn.length) {  // 如果是实参的个数小于形参的个数
      return function () {
        return curried(...args,...arguments)
      }
    }
    return fn(...args)
  }
}

函数组合


就是将一个复杂的函数分解成多个函数,一方面可以方便的定位到问题所在的位置,另一方面可以提高代码的复用率。

// 函数组合演示
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, 2, 3, 4]))

lodash中的组合函数

// lodash 中的函数组合的方法 _.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'])) // THREE

手写组合函数

const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
// 普通函数版
function compose (...args) {
  return function (value) {
    return args.reverse().reduce(function (acc, fn) {
      return fn(acc)
    }, value)
  }
}

// 箭头函数版
const compose = (...args) => value => args.reverve().reduce((acc,fn) => fn(acc),value);

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

函数组合要满足结合律

// 函数组合要满足结合律
const _ = require('lodash')

// const f = _.flowRight(_.toUpper, _.first, _.reverse)
// const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))


console.log(f(['one', 'two', 'three']))

函数组合中如何调试代码

  • 函数组合中都是函数优先,所以要对lodash原先的函数进行处理
  • 当然也可以接受一个函数,但是最终的返回结果必须要是下一个函数所需要的数据类型,调试也是基于这一点的
// 函数组合 调试 
// NEVER SAY DIE  --> never-say-die

const _ = require('lodash')

// const log = v => {
//   console.log(v)
//   return v
// }

const trace = _.curry((tag, v) => {
  console.log(tag, v)
  return v
})

// _.split()
const split = _.curry((sep, str) => _.split(str, sep))

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

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

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

console.log(f('NEVER SAY DIE'))

lodash中的fp模块

  • fp 模块提供了实用的函数式编程的友好方法
  • fp 模块中的函数都是基于函数优先,数据滞后的准则
// lodash 和 lodash/fp 模块中 map 方法的区别
const _ = require('lodash')

console.log(_.map(['23', '8', '10'], parseInt)) // [23,NaN,2]
// parseInt('23', 0, array)
// parseInt('8', 1, array)
// parseInt('10', 2, array)


const fp = require('lodash/fp')

console.log(fp.map(parseInt, ['23', '8', '10'])) // [23,8,10]

Point Free

我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数

  • 不需要指明处理的数据
  • 只要合成运算过程
  • 需要定义一些辅助的基本运算函数(lodash中fp模块函数)
// 非 point free
function(word) {
  return word.toLowerCase().replace(/\s+/g, '_')
}

// point free
// Hello     World => hello_world
const fp = require('lodash/fp')

const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)

console.log(f('Hello     World')) // hello_world

Functor(函子)


为什么要学函子

函子的出现是为了把副作用控制在可控范围内,比如异常处理、异步操作等等。

什么是函子

是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理

  • 函数式编程的运算不直接操作值,而是由函子完成
  • 函子就是一个实现了 map 契约的对象
  • 我们可以把函子想象成一个盒子,这个盒子里面封装了一个值
  • 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数 (纯函数),由这个函数来对值进行处理
  • 最终 map 方法返回一个包含新值的盒子 (函子)
// 定义一个函子
class Container {
  constructor (value) {
    this._value = value;
  }
  // 为了更像函数式编程,封装面向对象的定义方式
  static of (value) {
    return new Container(value)
  }
  map(fn) {
    return new Container(fn(this._value))
  }
}

// 函子的使用
let r = Container.of(5);
r.map(x => x + 100)
 .map(x => {
  console.log(x); // 105
 })

static 关键字用来定义一个类的一个静态方法。调用静态方法不需要实例化 (en-US)该类,但不能通过一个类实例调用静态方法。静态方法通常用于为一个应用程序创建工具函数。

MayBe 函子(处理空值)


  • 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
  • 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())
 console.log(r) // MayBe { _value: 'HELLO WORLD' }

// 传入空值
let r = MayBe.of(null)
          .map(x => x.toUpperCase())
console.log(r) // MayBe { _value: null }

Either 函子


  • Either 函子的出现是为了解决 MayBe 函子不会处理 map 的参数 fn ,仅仅返回一个null 的函子,也不会返回任何有效的信息,比如哪一块 map 出错
  • Either 函子用来处理异常,主要利用了 try-catch 语法
// 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))
  }
}

let r1 = Right.of(12).map(x => x + 2)
let r2 = Left.of(12).map(x => x + 2)

 console.log(r1) // Right { _value: 14 }
 console.log(r2) // Left { _value: 12 }


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) // Left { _value: { error: 'Unexpected token n in JSON at position 2' } }

 let r = parseJSON('{ "name": "zs" }')
           .map(x => x.name.toUpperCase())
 console.log(r) // Right { _value: 'ZS' }

IO 函子


  • IO 函子中的 ₋value是一个函数,这里是吧函数作为值来处理
  • IO 函子可以把不纯的动作存储到 -value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作
  • 把不纯的操作交给调用者来处理
// IO 函子
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({execPath:"../../path"}).map(p => p.execPath)
console.log(r._value())

folktale库


官网:folktale.origamitower.com/

// folktale 中的 compose、curry
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))
console.log(f(1)(2))


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

Task 函子


利用 Task 函子来执行异步任务

// Task 处理异步任务:获取pakage.json 文件中的版本号
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')

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 来处理值)
class Container {
  static of (value) {
    return new Container(value)
  }
  ......
}
Contanier.of(2)
  .map(x => x + 5)

Monad 函子(单子)


  • 一个函子如果具有 join 和 of 两个方法并遵循一些定律就是一个 Monad
  • 该函子解决函子嵌套的问题
  • 使用场景:当一个函数返回一个函子时,就使用 Monad(flatMap);如果函数返回一个值,则使用 map方法
// 函子嵌套的问题
// IO 函子的问题
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))
  }
}

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)
// IO(IO(x))
let r = cat('package.json')._value()._value() // 函子嵌套
console.log(r)
// 利用 Monad 解决函子嵌套的问题
// IO 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 r = readFile('package.json')
          // .map(x => x.toUpperCase())
          .map(fp.toUpper)
          .flatMap(print)
          .join()

console.log(r)

参考目录