函数式编程高阶概念-函子(Functor)

1,116 阅读9分钟

        函数式编程的函数指的就是纯函数,纯函数是没有可观察的副作用。但是副作用是不可避免的,我们只能将副作用控制在可控范围内。函子就是用来控制副作用的容器,除了这些以外,我们还可以通过函子来控制异常和异步操作。理解函子之前,首先要理解容器。容器就是包含值和值的变形关系,这个变形关系就是函数。换句话说,容器就是包含了值以及处理值的函数。

FP 认为异常处理不应该打断一段逻辑的执行,所以采用 try-catch 语句来抓错是不可行的。为了做到在处理异常的同时不打断执行,我们需要通过一种「容器」将函数的结果包装起来,用来标明一个结果是不是存在异常。

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

函子基本结构:

        函子是一个普通的对象,这个对象里面应该维护一个值。并且要对外公布一个map方法,所以可以通过一个类来描述函子。

class Container {
    constructor (value) {
        this._value = value
    }
    map (fn) { // 接收一个处理值的函数,这个函数是一个纯函数,因为我们要把_value传给函数,由fn真正处理_value,在map这个方法中我们要处理_value并且最终要返回一个新的函子。
        return new Container(fn(this._value))
    }
}
let r = new Container(5).map(x => x + 1).map(x => x * x)
/** map方法返回的不是值,而是一个新的函子对象。
* 在这个新的函子对象里面去保存新的值,我们始终不把值对外公布,
* 我们想要处理值的话我们就要给map方法传递一个处理值的函数。
* 我们可以把 new 这个操作封装一下。
* 我们可以通过链式调用,反复点用 map 方法多数据进行加工。
*/
class Container {
    static of (value) {
        return new Container(value)
    }
    constrauctor (value) {
        this._value = value
    }
    map () {
        return new Container(fn(this._value))
    }
}
let r = Container.of(5).map(x => x + 1).map(x => x * x)

总结:

        函子是一个具有map方法的普通对象,我们在函子里面要维护一个值,这个值永远不对外公布。就像这个值包裹在一个盒子里面,想要对这个值进行处理的话必须调用map方法,然后通过map方法传递一个处理值的函数。map方法执行完毕之后会返回一个新的函子,所以我们可以通过这种方式进行链式调用。

  • 函数式编程的运算不直接操作值,而是由函子完成
  • 函子就是一个实现了 map 契约的对象
  • 我们可以把函子想像成一个盒子,这个盒子里面封装了一个值
  • 如果想要处理盒子里的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数对值进行处理
  • 最终,map方法返回一个包含新值的盒子(函子)所以我们可以通过 .map 进行链式调用,因为 map方法始终返回的是一个函子,所有的函子都有 map 方法。因为我们可以把不同的运算方法封装到函子中,所以我们可以衍生出很多不同类型的函子。我们有多少运算就有多少函子,最终我们可以使用不同的函子来解决实际的问题。
  • 函子执行出现异常时比如 null 、undefined 这类空值,会使函数变得不纯。所以我们要在执行的时候控制这个副作用。

MayBe 函子:

        我们在编程过程中可能会遇到很多错误,需要对这些错误做相应的处理。MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围内)。这里我们把这种容器命名为 Maybe,因为它具有某种未知性。这个容器可以具有如下类似我们熟悉的 AJAX 响应数据的结构:

interface Maybe<T> { // Maybe 的结构可以像一个 AJAX 请求的响应数据一样
  error?: boolean; // error 表明执行过程是不是有异常
  data?: T; // 成功时返回的执行结果
}

/** * 首先需要提醒的是,以下的全部实现都不一定需要依赖类的概念,只是在 ES 中类更易于组织 *//** * Maybe 的类实现 * 这里的实现参考了 mostly adequate guide 一书中的定义 * 采用的是一种不定义 Just 和 Nothing 构造器的定义方法 * 这种方式对 ES 而言更简单自然 */export class Maybe<T> {    private value: T;      static of<T>(x: T) {      return new Maybe(x);    }      constructor(x: T) {      this.value = x;    }      get isNothing() {      return this.value == null;    }      public map<U>(fn: (x: T) => U): Maybe<U> {      return this.isNothing ? this as Maybe<null> : Maybe.of(fn(this.value));    }      public join(): T | Maybe<null> {      return this.isNothing ? this as Maybe<null> : this.value;    }      public chain<U>(fn: (x: T) => Maybe<U>): Maybe<U> {      return this.map(fn).join();    }      public toString() {      return this.isNothing ? 'Nothing' : `Just ${this.value}`;    }  }

class MayBe {
    static of (value) {
        return new MayBe (value)
    }
    constructor (value) {
        this._value = value
    }
    isNothing () {
        return this._value === null || typeOf this._value === undefined
    }
    map (fn) {
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }
}
const r = MayBe.of('hello world')
            .map(x => x.toUppercase())
            .map(x => null)
            .map(x => x.split(''))
// 这里展示了MayBe的一个不断就是无法判断执行哪一行出现了空值

Either函子:

        Either 两者中的任何一个,类似于 if ··· else ··· 的处理,也是我们处理异常的一种方式。

/** * Either 的实现 * 这里通过 Either 例举了具有两个构造器的 Maybe 的情况 * 注意,这个 Either 不是一个 Monad,它只是一个 Functor */export class Either<T> {    static of<T>(x: T) {      return x == null ? new Left(x as null) : new Right(x);    }      protected value: T;      constructor(x: T) {      this.value = x;    }  }    class Left extends Either<null> {    map(f: any): Left {      return this;    }      toString() {      return 'Left';    }  }    class Right<T> extends Either<T> {    map<U>(f: (x: T) => U): Either<U> {      return Either.of(f(this.value));    }      toString() {      return `Right ${this.value}`;    }  }

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))
    }
}
function parseJSON (str) {
    try {
        return Right.of(JSON.parse(str))
    } catch (e) {
        return Left.of({error: e.message})
    }
}
let r = parseJSON('{name:zs}')
let r = parseJSON('{"name":"zs"}')
            .map(x => x.name.toUpperCase())IO

IO(Input Output)函子:

IO函子和之前函子不同的是,IO函子中的 _value 是一个函数,这里是把函数作为值来处理。IO函子可以把不纯的操作存储到 _value 中,在_value中存储的是函数在函子内部并没有调用这个函数,所以通过IO函子延迟执行这个不纯的操作(惰性执行),我们通过IO函子先包装一些函数,当我们需要的时候再来调用执行这些函数,来保证当前的操作是个纯的操作。通过IO函子将不纯的操作包装起来,并放到调用的时候执行。有了IO函子我们就可以把各种不纯的操作装进笼子里,但是这些不纯的操作最终都是要去执行的我们可以把这些不纯的操作交给调用者来处理。

class IO {
    static of (value) {
        return new IO(() => {
            return value
        })
    }
    constructor (fn) {
        this._value = fn
    }
    map (fn) {
        return IO.of(fp.flowRight(fn,this._value))
    }
}
const r = IO.of(process).map(p => p.execPath)

总结:IO函子内部帮我们包装了一些函数,当我们在传递一些函数的时候有可能这个函数是一个不纯的操作,但IO函子返回的始终是一个纯的操作,把不纯的操作延续到调用的时候,也就是IO函子控制了副作用在可控的范围内发生。

Task 函子:

Task 函子是用来处理异步操作的。

Maybe.js

function isNil (value) {
    return value == null
}

class Just {
    static of (value) {
        return new Just(value)
    }
    constructor (value) {
        this.value = value
    }
    filter (fn) {
        if (fn(this.value)) {
            return this
        } else {
            return nothing
        }
    }
    flatMap (fn) {
        const result = fn(this.value)
        return isNil(result) ? reault : nothing
    }
    forEach (fn) {
        fn (this.value)
    }
    isJust () {
        return true
    }
    isNothing () {
        return false
    }
    just () {
        return this.value
    }
    map (fn) {
        new Just(fn(this.value))
    }
    orElse () {
        return this
    }
    orJust () {
        return this.value
    }
}
class Nothing {
    static of (value) {
        return new Nothing (value)
    }
    filter () {
        return this
    }
    flatMap () {
        return this
    }
    forEach () {}
    isJust () {
        return false
    }
    isNothing () {
        return true
    }
    just () {
        throw Error('Can not call just() on a Nothing')
    }
    map () {
        return this
    }
    orElse (m) {
        return m
    }
    orJust (value) {
        return  value
    }
}
export function just (value) {
    if (isNil(value)) {
        throw Error('Can not create Just with an empty value: use flowType!')
    }
    return new Just(value)
}
export const nothing = Objext.freez(new Nothing())
export fonction maybe (value) {
    return isNil(value) ? nothing : new Just (value)
}

Task.js — Full Implementation (SemiGroup, Monoid, Functor, Monad, Applicative)

import { compose, add, map, range } from 'lodash/fp'
import { just, nothing } from './Maybe'
class Task {
  constructor(fork) {
    this.fork = fork
  }
  static of(x) {
    return Task.resolve(x)
  }
  static resolve(x) {
    return new Task((_, resolve) => resolve(x))
  }
  static reject(x) {
    return new Task((reject, _) => reject(x))
  }
  static empty() {
    return new Task((_, resolve) => resolve())
  }
  /**  * use the first Task to resolve
  * @summary concat :: Task a b -> Task a b -> Task a b
  */
  concat(task) {
    return new Task((reject, resolve) => { 
     let done = false
      const guard = f => x => {
        if (!done) {
          done = true
          f(x)
        }
      }
      task.fork(guard(reject), guard(resolve))
      this.fork(guard(reject), guard(resolve))
    })
  }
  /**  * @summary map :: (a -> b) -> Task Err a -> Task Err b  */
  map(f) {
    return new Task((reject, resolve) => this.fork(
      reject,
      compose(resolve, f)
    ))
  }
  /**  * @summary chain :: (a -> Task Err b) -> Task Err a -> Task Err b  */
  chain(f) {
    return new Task((reject, resolve) => this.fork(
      reject,
      x => f(x).fork(reject, resolve)
    ))
  }
  /**  * @summary ap :: Task Err (a -> b) -> Task Err a -> Task Err b  */
  ap(task) {
    return new Task((reject, resolve) => {
      this.fork(
        reject,
        f => task.fork(reject, compose(resolve, f))
      )
    })
  }
  /**  * fold the Rejected or Resolved Task into a Resolved Task by applying a
  * different mapper whether its Rejected or Resolved.
  * @summary fold :: (a -> b) -> (c -> b) -> Task a c -> Task _ b
  */
  fold(f, g) {
    return new Task((reject, resolve) => this.fork(
      compose(resolve, f),
      compose(resolve, g)
    ))
  }
  /**
  * like fold but using pattern matching
  * @summary cata :: { Rejected :: a -> c, Resolved :: b -> c } -> Task a b -> Task _ c
  */
  cata({ Rejected, Resolved }) {
    return this.fold(Rejected, Resolved)
  }
  /**
  * transform a Rejected to a Resolved and vice versa
  * @summary swap :: _ -> Task a b -> Task b a
  */
  swap() {
    return new Task((reject, resolve) => this.fork(resolve, reject))
  }
  /**
  * apply a mapper to the rejected or resolved Task
  * @summary bimap :: (a -> b) -> (c -> d) -> Task a c -> Task b d
  */
  bimap(f, g) {
    return new Task((reject, resolve) => this.fork(
      compose(reject, f),
      compose(resolve, g)
    ))
  }
  /**
  * @summary rejectedMap :: (a -> b) -> Task a x -> Task b x
  */
  rejectMap(f) {
    return new Task((reject, resolve) => this.fork(
      compose(reject, f),
      resolve
    ))
  }
  /**
  * @summary rejectedChain :: (a -> Task b x) -> Task a x -> Task b x
  */
  rejectChain(f) {
    return new Task((reject, resolve) => this.fork(
      x => f(x).compose(reject, f),
      resolve
    ))
  }
  /**
  * @summary toMaybe :: _ -> Task a b -> Task Nothing (Just b)
  */
  toMaybe() {
    return new Task((reject, resolve) => this.fork(
      compose(resolve, nothing.of),
      compose(resolve, just.of)
    ))
  }
  /**
  * @summary fromMaybe :: a -> Maybe x -> Task a x
  */
  static fromMaybe(defaultValue, maybe) {
    return new Task((reject, resolve) => {
      cata({
        Nothing: () => reject(defaultValue),
        Just: x => resolve(x)
      })(maybe)
    })
  }
  /**
  * @summary toPromise :: Task a b -> Promise b a
  */
  toPromise() {
    return new Promise((resolve, reject) => this.fork(reject, resolve))
  }
  /**
  * @summary fromPromise :: Promise b a -> Task a b
  */
  static fromPromise(promise) {
    return new Task((reject, resolve) => promise.then(resolve, reject))
  }}
/* ----------------------------------------- *        Let's use that* ----------------------------------------- */
const compose = (...fns) => x => fns.reduceRight((acc, f) => f(acc), x)
const map = f => xs => xs.map(f)
const range = (a, b) => a === b  ? [b]  : [a, ...range(a + 1, b)]
const add = a => b => a + b
const noop = () => {}
const delay = (duration, value) => new Task((_, resolve) => setTimeout(() => resolve(value), duration))
const getUsers = count => new Task((_, resolve) => {
  setTimeout(() => {
    resolve(range(0, count).map(x => ({ id: x, username: 'Bob' })))
  }, 200)})
const userComponent = ({ id, username }) => `
  <div>
    <p>userID: ${id}</p>
    <p>user name: ${username}</p>
  </div>`
const div = x => `<div>${[].concat(x).join('')}</div>`
Task
  .of(add)
  .ap(Task.of(3))
  .ap(Task.of(8))
  .map(x => x / 10)
  .map(add)
  .ap(delay(1000, 70))
  .map(Math.ceil)
  .chain(getUsers)
  .map(map(userComponent))
  .map(div)
  .fork(noop, x => console.log(x))
/* => `<div>
  <div>
    <p>userID: 1</p>
    <p>user name: Bob</p>
  </div>
  <div>
    <p>userID: 2</p>
    <p>user name: Bob</p>
  </div>
  ...</div>`
*/

Pointed 函子:

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

Monad 函子:

Monad 函子是可以变扁的 Pointed 函子 IO(IO(x)),一个函子如果具有 joinof两个方法并遵守一些定律,就是一个 Monad。如果函数嵌套的话我们可以使用函数组合,函子嵌套可以 Monad

// IO Monad
class IO {
    static of (value) {
        return new IO(function(){
            return value
        })
    }
    constructor (fn) {
        this._value = fn
    }
    map (fn) {
        return IO.of(fp.flowRight(this._value, fn))
    }
    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 (value) {
    return new IO (function () {
        console.log(value)
        return value
    })
}
let r = readFile('package.json')
            .map(fp.toUpper)
            .flatMap(print)
            .join()

总结:Monad 是一个具有静态 of 方法和 join 方法的函子。当我们遇到一个函数返回一个函子的时候,可以使用MonadMonad 可以帮助我们解决函子嵌套的问题,当我们想要合并一个函数并且这个函数只有一个返回值,这时候我们可以调用 map 方法。当我们想要合并一个函数,但是这个函数返回一个函子,这个时候我们要用 flatMapchain)方法。