为什么要学函子

到目前为止已经已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作用 控制在可控的范围内、异常处理、异步操作等

什么是 Functor

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

# Functor 函子

// 一个容器,包裹一个值
class Container {
  // of 静态方法,可以省略 new 关键字创建对象
  static of (value) {
    return new Container(value)
  }
  constructor (value) {
    this._value = value
  }
  // map 方法,传入变形关系,将容器里的每一个值映射到另一个容器
  map (fn) {
    return Container.of(fn(this._value))
  }
}

// 测试
Container.of(3)
.map(x => x + 2)
.map(x => x * x)

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

// 值如果不小心传入了空值(副作用)
Container.of(null)
.map(x => x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null

# MayBe 函子

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

class MayBe {
  static of (value) {
    return new MayBe(value)
  }
  constructor (value) {
    this._value = value
  }
  // 如果对空值变形的话直接返回 值为 null 的函子
  map (fn) {
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
  }
  isNothing () {
    return this._value === null || this._value === undefined
  }
}

// 传入具体值
MayBe.of('Hello World')
.map(x => x.toUpperCase())

// 传入 null 的情况
MayBe.of(null)
.map(x => x.toUpperCase())
// => MayBe { _value: null }

在 MayBe 函子中,我们很难确认是哪一步产生的空值问题,如下例:

MayBe.of('hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
// => MayBe { _value: null }

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

Either 用来处理异常

function parseJSON(json) {
  try {
    return Right.of(JSON.parse(json));
  } catch (e) {
    return Left.of({ error: e.message});
  }
}
let r = parseJSON('{ "name": "zs" }')
.map(x => x.name.toUpperCase())
console.log(r)

# IO 函子

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

const fp = require('lodash/fp')
class IO {
  static of (x) {
    return new IO(function () {
      return x
    })
  }
  constructor (fn) {
    this._value = fn
  }
  map (fn) {
    // 把当前的 value 和 传入的 fn 组合成一个新的函数
    return new IO(fp.flowRight(fn, this._value))
  }
}

// 调用
let io = IO.of(process).map(p => p.execPath)
console.log(io._value())

Task 异步执行

异步任务的实现过于复杂,我们使用 folktale 中的 Task 来演示 folktale 一个标准的函数式编程库 和 lodash、ramda 不同的是,他没有提供很多功能函数 只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、 MayBe 等

const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')
// 第一个参数是传入函数的参数个数
let f = curry(2, function (x, y) {
  console.log(x + y)
})
f(3, 4)
f(3)(4)
// 函数组合
let f = compose(toUpper, first)
f(['one', 'two'])

Task 异步执行 folktale(2.3.2) 2.x 中的 Task 和 1.0 中的 Task 区别很大,1.0 中的用法更接近我们现在演示的 函子 这里以 2.3.2 来演示

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)
    })
  })
}
// 调用 run 执行
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(单子)

在使用 IO 函子的时候,如果我们写出如下代码:

const fs = require('fs')
const fp = require('lodash/fp')
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
  })
}

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

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

Monad 函子是可以变扁的 Pointed 函子,IO(IO(x)) 一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad

const fp = require('lodash/fp')
// IO Monad
class IO {
  static of (x) {
    return new IO(function () {
      return x
    })
  }
  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 r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
上次更新: 2022/4/10 21:54:40