1.JavaScript 深度剖析-1.函数式编程

233 阅读9分钟

函数式编程 Function Program FP;

函数式编程就是编程的一种思想,跟面向对象编程,面向过程编程,类似,平级;

把现实世界的事物和事物之间的联系抽象到程序世界; (对运算过程进行抽象,比如抽离函数呀,代码复用)


★ 1前置概念

1.1前置概念-函数是一等公民

MDN first class function

  • 函数可以作为变量

  • 函数可以作为参数

  • 函数可以作为返回值

//两种写法一致,都是  函数赋值给变量
index(post){ return Views.index(post) }

index: Views.index

1.2前置概念-高阶函数【higher-order function】

  • 可以把函数作为参数传递给另一个函数

  • 可以把函数作为另一个函数的返回结果

函数作为参数

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

函数作为返回值

once函数【默写】

// once
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)

高阶函数的意义

  • 抽象可以帮我们屏蔽细节,只需要关注与我们的目标
  • 高阶函数是用来抽象通用的问题
// 面向过程
for (let i = 0; i < array.length; i++) { 
  console.log(array[i]) 
}
// 高阶函数
forEach(array, item => console.log(item))

常用的高阶函数

  • forEach
  • map
  • filter
  • every
  • some
  • find/findIndex
  • reduce
  • sort
  • ...

1.3前置概念-闭包

可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员

闭包的本质:

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


★ 2.纯函数

2.1纯函数概念

  • 相同的输入永远会得到相同的输出

  • lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法

  • 数组的 slice 和 splice 分别是:纯函数和不纯的函数

    slice 返回数组中的指定部分,不会改变原数组splice 对数组进行操作返回该数组,会改变原数组

  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)

  • 我们可以把一个函数的执行结果交给另一个函数去处理

2.2纯函数好处

  • 可缓存
  • memoize函数
  • 可测试
  • 并行处理

memoize【默写】

const _ = require('lodash')

function getArea (r) {
  console.log(r)
  return Math.PI * r * r
}

// 模拟 memoize 方法的实现
function memoize (f) {
  let cache = {}
  return function () {
    let key = JSON.stringify(arguments)
    cache[key] = cache[key] || f.apply(f, arguments)
    return cache[key]
  }
}

let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))

2.3纯函数副作用

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

副作用来源:【所有外部交互都可能带来副作用】

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

副作用会让一个函数变的不纯,副作用不可能避免,因为代码难免会依赖外部的配置文件、数据库等,只能最大程度上控制副作用在可控的范围内发生


★ 3.柯里化

3.1柯里化-概念

函数的降维处理,将多元函数转换成一元函数,几个参数就叫几元函数

当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果

是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术

tips:以上是两个概念,自己理解吧,反正就是多个参数变成一个参数

3.2Lodash中的柯里化

_.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))

3.3模拟_.curry实现


★ 4.函数组合

纯函数和柯里化很容易写出洋葱模型h(g(f(x))) 【 _.toUpper(_.first(_.reverse(array)))】

函数组合可以让我们把 细粒度的函数 重新组合 生成新的函数

4.1函数组合-管道

fn函数比较复杂的时候可以把fn拆分成多个小函数;

处理数据的管道,一个数据经过这个管道就得到另外一个结果

4.2函数组合

如果把多个一元函数组合合并成一个功能更强大的函数,默认是从右到左

compose【默写】

lodash中的函数组合是叫flowRIght;folktale中的函数组合是叫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, 2, 3, 4]))

lodash中组合函数 flow() 或者 flowRight()

  • flow()从左到右
  • flowRight()从右到左,使用的更多些

函数组合要满足结合律

  • compose(compose(f, g), h) == compose(f, compose(g, h)) // true

4.3函数组合-调试

1.定义log函数,放在组合函数

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

2.lodash.fp模块

  • lodash的fp模块提供了对函数式编程友好的方法

  • 提供了不可变 auto-curried(已经被柯里化的) iteratee-first(函数优先) data-last(数据之后) 的方法

const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))

lodash.map和lodash/fp.map中的区别

接受的参数不一样

  • lodash.map 三个参数value,index\key,原数组

  • lodash/fp.map 只接受一个参数 value

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

// parseInt('23', 0, array)  0 就是10进制
// parseInt('8', 1, array)   
// parseInt('10', 2, array)

parseInt第二个参数是进制 取值范围是2-36

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

4.4函数组合-Point Free 【是一种编程的风格,具体实现是函数的组合】

const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))


★ 5.函子

把函子想象成一个盒子,盒子里面包裹了一个值,想要对这个值进行处理的话,就要调用这个盒子的map方法,接受一个函数类型的参数,传递的这个函数就是去处理值的函数

Functor 函子

函子是具有map方法的对象,函子里面要维护一个值,这个值永远不对外公布,调用map方法处理这个值,通过map方法传递处理值的函数,map方法执行完之后会返回新的函子对象,所以可以不停的使用.map().map()进行链式调用

// 一个容器包裹一个值
class Container {
  // 静态方法可以省略new关键字
  static of (value) {
    return new Container(value)
  }

  constructor (value) {
    this._value = value
  }
  // map 方法,传入变形关系,将容器里的每一个值映射到另一个容器
  map (fn) {
    return Container.of(fn(this._value))
  }
}

let r = Container.of(5)
          .map(x => x + 2)

函数式编程的运算不直接操作值,而是由函子完成

函子就是一个实现了 map 契约的对象

我们可以把函子想象成一个盒子,这个盒子里封装了一个值

想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理

最终 map 方法返回一个包含新值的盒子(函子)

Maybe函子

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

  //保留值
  constructor (value) {
    this._value = value
  }
 //map里面处理,给map传的参数是纯函数,返回的是函子,所以调用MayBe.of
  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

Maybe函子中很难确认是哪一步产生的空值问题

  • Either 两者中的任何一个,类似于 if...else...的处理
  • 异常会让函数变的不纯,Either 函子可以用来做异常处理
// 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)) // 正常逻辑走Right函子
  } catch (e) {
    return Left.of({ error: e.message })  // 异常逻辑走Left函子
  }
}

let r = parseJSON('{ "name": "zs" }')
          .map(x => x.name.toUpperCase())

IO函子

_value是函数,不纯的操作存储到_value中,函子内部没调用这个函数,延迟执行这个操作,需要的时候再执行,不纯的操作,交给调用者来处理

// IO 函子
const fp = require('lodash/fp')

class IO {
  static of (value) {
    return new IO(function () {  //之前函子of里面传的都是value,现在是Function,我感觉是因为constructor里面value也是Function
      return value
    })
  }

  constructor (fn) {
    this._value = fn  // value里面存储的是函数
  }

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

// 调用
let r = IO.of(process).map(p => p.execPath)
// console.log(r)   // IO {_value: [Function]}
console.log(r._value())


//IO.of(process) 这个时候  执行of方法,5行参数value是process,执行new IO ,那么久执行到constructor里面了,这个时候12行this._value = fn 就是等于传进来的process
// .map(p => p.execPath)  执行.map  map(fn) 参数fn = p => p.execPath箭头函数,return  new IO(fp.flowRight(fn, this._value),
// 其中fn是 p => p.execPath   this._value是process上面解释的,
// fp.flowRight(fn, this._value)就是p => p.execPath 和process组合函数的结果
// 所以.map里面return IO函子,,new IO,传的参数是  组合函数之后的 结果函数
// 所以 r 是一个IO函子,里面的value是个函数,是组合函数,因为constructor里面有value,所以输出IO {_value: [Function]}
// 调用的时候直接 r._value() 调用

folkTale

npm install folkTale

folktale 一个标准的函数式编程库和 lodash、ramda 不同的是,他没有提供很多功能函数只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、MayBe 等

// 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, 2))
// console.log(f(1)(2))


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

folkTale2.3.2中的Task函子 执行异步任务

// Task 处理异步任务
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) => { //readFile(路径,编码,回调)
      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 函子【就是个概念,一直都是这样用的】

具有静态of方法的函子就叫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(单子)(解决函子嵌套的问题)

【太难了不会,原理可以不用了解那么深入,会使用就行,使用的多了熟悉了就好了】

解决IO函子嵌套问题,.value().value()调用

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

//IO Monad
let r = readFile('package.json')
          // .map(x => x.toUpperCase())
          .map(fp.toUpper)
          .flatMap(print)
          .join()