函数式编程范式(5)函子

函数式编程建立在数学基础之上,比如函数式编程的纯函数就是数学中的纯函数,我们要学习的函子也是建立在数学中的范畴论。

为什么要学函子

我们需要在函数式编程中把副作用控制在可控范围内、异常处理、异步操作等。

什么是 Functor

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

Functor 函子

// 一个容器,包裹一个值
class Container {
  constructor (value) {
    this._value = value
  }

  map (fn) {
    return new Container(fn(this._value))
  }
}

let r = new Container(5)
  .map(x => x + 1)
  .map(x => x * x)

console.log(r)
// ouput Container { _value: 36 }

每次创建函子时,需要使用 new 关键字创建对象,我们可以使用 of 静态方法封装。

// 一个容器,包裹一个值
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 函子存在的问题

纯函数要满足相同的输入始终要获得相同的输出。

在 Functor 中如果我们传入 null 或 undefined 时,纯函数变得不纯。

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

这种情况下,传入的 null 或 undefined 就是副作用,我们要想办法控制这个副作用。

MayBe 函子

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

  constructor (value) {
    this._value = value
  }

  map (fn) {
    // 当 value 为 null 或 undefined,不会调用 fn,而直接返回一个值为 null 的函子
    return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this._value))
  }

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

let r1 = Maybe.of('Hello World').map(x => x.toUpperCase())
console.log(r)
// output: Maybe { _value: 'HELLO WORLD' }

let r2 = Maybe.of(null).map(x => x.toUpperCase())
// output: Maybe { _value: null }

MayBe 函子与 Container 函子相比,需要先判断 value,当 value 为 null 或 undefined 时,不会调用 fn,而直接返回一个值为 null 的函子。

MayBe 函子虽然可以处理空值的问题,但是如果我们多次调用 map 方法时,我们无法得知是哪一次出现了空值:

let r3 = Maybe.of('hello world').map(x => x.toUpperCase).map(x => null).map(x => x.split(' '))
console.log(r3)
// output: Maybe { _value: null }
// 是在哪一次出现了 null 呢?

Either 函子

  • Either 两者中的任何一个,类似于 if…else…的处理
  • 异常会让函数变的不纯,Either 函子可以用来做异常处理
class Left {
  static of (value) {
    return new Left(value)
  }

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

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 {
  // 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 result = IO.of(process).map(p => p.execPath)
console.log(result)
// 将不纯的操作延迟到调用的时候。
console.log(result._value())

Task 函子

Folktale

Task 函子用于处理异步执行,异步任务的实现过于复杂,我们使用 folktale 中的 Task 来演示。

Folktale 是一个标准的函数式编程库,提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、MayBe 等。

// Task 处理异步任务
const fs = require('fs')
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')
  .run()
  .listen({
    onRejected: err => {
      console.log(err)
    },
    onResolved: value => {
      console.log(value)
    }
  })

对返回的值进行处理,可以使用 map 方法。

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 函子

IO 函子的问题

IO 函子可能会存在函子嵌套。

/// ...
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')
console.log(r)

// IO { _value: [Function] }

虽然我们也可以通过 ._value()._value() 调用到值,但是这样较为麻烦。

// ...
let cat = fp.flowRight(print, readFile)
// IO(IO(x))
let r = cat('package.json')._value()._value()
console.log(r)

Monad 函子的 join 方法

Monad 单子可以帮助我们解决函数嵌套的问题。

Monad 函子是可以变扁的 Pointed 函子,IO(IO(x))。

Monad 函子具有 of 方法和 join 方法。

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()
  }
  
  // 先调用 map 再调用 join
  flatMap (fn) {
    return this.map(fn).join()
  }
}

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

将获取函子的值通过方法封装到函子内部,在封装时虽然复杂,但是实际开发便轻松了。

当一个函数返回函子时,便可以使用 Monad 函子。

Monad 函子可以帮助我们解决函子嵌套问题,使得代码更具可读性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值