乐趣区

关于javascript:函数式编程五-函子

  • Functor

    • 为什么要学函子?
    • 什么是 Functor
    • 了解 Functor
    • 总结
    • MyBe 函子
    • Either 函子
    • IO 函子
    • Task 函子(异步执行)

      *  folktale 的装置
      * folktale 中的 curry 函数
      * folktale 中的 compose 函数
      * Task 函子异步执行
      * 案例
    • Pointed 函子
    • Monad 函子(单子)

      * IO 函子的嵌套问题
      * 什么是 Monad 函子
      * 实现一个 Monad 函子
      * Monad 函子小结
          * 什么是 Monad?* 什么时候应用 Monad?
          
    • 【函数式编程总体设计】

之前讲了函数的前置常识 函数式编程(一)—— 前置常识

还有纯函数的常识 函数式编程(二)—— 纯函数

柯里化 函数式编程(三)—— 柯里化

函数组合 函数式编程(四)——函数组合

Functor

为什么要学函子?

函子 (representative functor) 是领域论里的概念,指从任意领域到汇合领域的一种非凡函子。
咱们没有方法防止副作用,然而咱们尽可能的将副作用管制在可控的范畴内,咱们能够通过函子去解决副作用,咱们也能够通过函子去解决异样,异步操作等。

什么是 Functor

  • 容器:蕴含值和值的变形关系(这个变形关系就是函数)
  • 函子:是一个非凡的容器,通过一个一般的对象来实现,该对象具备 map 办法,map 办法能够运行一个函数对值进行解决(变形关系)

了解 Functor

class Container {constructor (value) {
    // 这个函子的值是保留在外部的,不对外颁布
    // _下划线的成员都是公有成员,内部无法访问,值是初始化的传的参数
    this._value = value
  }
  
  // 有一个对外的办法 map,接管一个函数(纯函数),来解决这个值
  map (fn) {
    // 返回一个新的函子,把 fn 解决的值返回给函子,由新的函子来保留
    return new Container(fn(this._value))
  }
}

// 创立一个函子的对象
let r = new Container(5)
  .map(x => x + 1) // 6
  .map(x => x ** 2) // 36

// 返回了一个 container 函子对象,外面有值是 36,不对外颁布
console.log(r) //Container {_value: 36}

下面还是面向对象的编程思维,要批改成函数式编程的思维,须要防止应用 new

class Container {
  // 应用类的静态方法,of 代替了 new Container 的作用
  static of (value) {return new Container(value)
  }
  constructor (value) {this._value = value}
  
  map (fn) {return Container.of(fn(this._value))
  }
}

const r = Container.of(5)
            .map(x=>x+2) // 7
            .map(x=> x**2) // 49

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

总结

  • 函数式编程的运算不间接操作值,而是由函子实现
  • 函子就是一个实现了 map 契约的对象
  • 咱们能够把函子设想成一个盒子,这个盒子里封装了一个值
  • 想要解决盒子中的值,咱们须要给盒子的 map 办法传递一个解决值的函数(纯函数),由这个函数来对值进行解决
  • 最终 map 办法返回一个蕴含新值的盒子(函子)

遗留问题:如果 value 是 null undefined,怎么办?

Container.of(null)
  .map(x=>x.toUpper) // 报错,使得函数不纯

上面会有好几种函子,解决不同的问题

MyBe 函子

MayBe 函子的作用就是能够对外部的空值状况做解决(管制副作用在容许的范畴)

class MayBe {static of (value) {return new MayBe(value)
  }
  constructor (value) {this._value = value}

  map(fn) {
    // 判断一下 value 的值是不是 null 和 undefined,如果是就返回一个 value 为 null 的函子,如果不是就执行函数
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
  }

 // 定义一个判断是不是 null 或者 undefined 的函数,返回 true/false
  isNothing() {return this._value === null || this._value === undefined}
}

const r = MayBe.of('hello world')
  .map(x => x.toUpperCase())

console.log(r) //MayBe {_value: 'HELLO WORLD'}


// 如果输出的是 null,是不会报错的
const rnull = MayBe.of(null)
  .map(x => x.toUpperCase())
console.log(rnull) //MayBe {_value: null}

然而这里有一个问题就是,如果 map 两头有好几步,最初返回是 null,并不知道是哪一个步骤返回的。解决这个问题,须要看下一个函子。

Either 函子

  • Either 两者中的任何一个,相似于 if…else… 的解决
  • 当呈现问题的时候,Either 函子会给出提醒的无效信息,
  • 异样会让函数变的不纯,Either 函子能够用来做异样解决
// 因为是二选一,所以要定义 left 和 right 两个函子

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}
// 为什么后果会不一样?因为 Left 返回的是以后对象,并没有应用 fn 函数

// 那么这里如何解决异样呢?// 咱们定义一个字符串转换成对象的函数
function parseJSON(str) {
  // 对于可能出错的环节应用 try-catch
  // 失常状况应用 Right 函子
  try{return Right.of(JSON.parse(str))
  }catch (e) {
  // 谬误之后应用 Left 函子,并返回错误信息
    return Left.of({error: e.message})
  }
}

let rE = parseJSON('{name:xm}')
console.log(rE) // Left {_value: { error: 'Unexpected token n in JSON at position 1'} }
let rR = parseJSON('{"name":"xm"}')
console.log(rR) // Right {_value: { name: 'xm'} }

console.log(rR.map(x => x.name.toUpperCase())) // Right {_value: 'XM'}

IO 函子

  • IO 就是输入输出,IO 函子中的 _value 是一个函数,这里是把函数作为值来解决
  • IO 函子能够把不纯的动作存储到 _value 中,提早执行这个不纯的操作(惰性执行),包装以后的操

  • 把不纯的操作交给调用者来解决

因为 IO 函数须要用到组合函数,所以须要提前装置 Lodash

npm init -y

npm i lodash

const fp = require('lodash/fp')

class IO {
  // of 办法疾速创立 IO,要一个值返回一个函数,未来须要值的时候再调用函数
  static of(value) {return new IO(() => value)
  }
  // 传入的是一个函数
  constructor (fn) {this._value = fn}

  map(fn) {
    // 这里用的是 new 一个新的构造函数,是为了把以后_value 的函数和 map 传入的 fn 进行组合成新的函数
    return new IO(fp.flowRight(fn, this._value))
  }
}


// test
// node 执行环境能够传一个 process 对象(过程)// 调用 of 的时候把以后取值的过程包装到函数外面,再在须要的时候再获取 process
const r = IO.of(process)
  // map 须要传入一个函数,函数须要接管一个参数,这个参数就是 of 中传递的参数 process
  // 返回一下 process 中的 execPath 属性即以后 node 过程的执行门路
  .map(p => p.execPath)
console.log(r) // IO {_value: [Function] }


// 下面只是组合函数,如果须要调用就执行上面
console.log(r._value()) // C:\Program Files\nodejs\node.exe

Task 函子(异步执行)

  • 函子能够管制副作用,还能够解决异步工作,为了防止天堂之门。
  • 异步工作的实现过于简单,咱们应用 folktale 中的 Task 来演示
  • folktale 一个规范的函数式编程库。和 lodash、ramda 不同的是,他没有提供很多性能函数。只提供了一些函数式解决的操作,例如:compose、curry 等,一些函子 Task、Either、MayBe 等

folktale 的装置

首先装置 folktale 的库

npm i folktale

folktale 中的 curry 函数

const {compose, curry} = require('folktale/core/lambda')

// curry 中的第一个参数是函数有几个参数,为了防止一些谬误
const f = curry(2, (x, y) => x + y)

console.log(f(1, 2)) // 3
console.log(f(1)(2)) // 3

folktale 中的 compose 函数

const {compose, curry} = require('folktale/core/lambda')
const {toUpper, first} = require('lodash/fp')

// compose 组合函数在 lodash 外面是 flowRight
const r = compose(toUpper, first)
console.log(r(['one', 'two']))  // ONE

Task 函子异步执行

  • folktale(2.3.2) 2.x 中的 Task 和 1.0 中的 Task 区别很大,1.0 中的用法更靠近咱们当初演示的

函子

  • 这里以 2.3.2 来演示
const {task} = require('folktale/concurrency/task')
const fs = require('fs')
// 2.0 中是一个函数,函数返回一个函子对象
// 1.0 中是一个类

// 读取文件
function readFile (filename) {
  // task 传递一个函数,参数是 resolver
  // resolver 外面有两个参数,一个是 reject 失败的时候执行的,一个是 resolve 胜利的时候执行的
  return task(resolver => {
    //node 中读取文件,第一个参数是门路,第二个是编码,第三个是回调,谬误在先
    fs.readFile(filename, 'utf-8', (err, data) => {if(err) resolver.reject(err)
      resolver.resolve(data)
    })
  })
}

// 演示一下调用
// readFile 调用返回的是 Task 函子,调用要用 run 办法
readFile('package.json')
  .run()
  // 当初没有对 resolve 进行解决,能够应用 task 的 listen 去监听获取的后果
  // listen 传一个对象,onRejected 是监听谬误后果,onResolved 是监听正确后果
  .listen({onRejected: (err) => {console.log(err)
    },
    onResolved: (value) => {console.log(value)
    }
  })
 
 /** {
    "name": "Functor",
    "version": "1.0.0",
    "description": "","main":"either.js","scripts": {"test":"echo \"Error: no test specified\" && exit 1"},"keywords": [],"author":"",
    "license": "ISC",
    "dependencies": {
      "folktale": "^2.3.2",
      "lodash": "^4.17.20"
    }
  }
  */

案例

在 package.json 文件中提取一下 version 字段

const {task} = require('folktale/concurrency/task')
const fs = require('fs')
const {split, find} = require('lodash/fp')
// 2.0 中是一个函数,函数返回一个函子对象
// 1.0 中是一个类

// 读取文件
function readFile (filename) {
  // task 传递一个函数,参数是 resolver
  // resolver 外面有两个参数,一个是 reject 失败的时候执行的,一个是 resolve 胜利的时候执行的
  return task(resolver => {
    //node 中读取文件,第一个参数是门路,第二个是编码,第三个是回调,谬误在先
    fs.readFile(filename, 'utf-8', (err, data) => {if(err) resolver.reject(err)
      resolver.resolve(data)
    })
  })
}

// 演示一下调用
// readFile 调用返回的是 Task 函子,调用要用 run 办法
readFile('package.json')
  // 在 run 之前调用 map 办法,在 map 办法中会解决的拿到文件返回后果
  // 在应用函子的时候就没有必要想的实现机制
  .map(split('\n'))
  .map(find(x => x.includes('version')))
  .run()
  // 当初没有对 resolve 进行解决,能够应用 task 的 listen 去监听获取的后果
  // listen 传一个对象,onRejected 是监听谬误后果,onResolved 是监听正确后果
  .listen({onRejected: (err) => {console.log(err)
    },
    onResolved: (value) => {console.log(value) // "version": "1.0.0",
    }
  })

Pointed 函子

  • Pointed 函子是实现了 of 静态方法的函子

of 办法是为了防止应用 new 来创建对象,更深层的含意是of 办法用来把值放到上下文

  • Context(把值放到容器中,应用 map 来解决值)
class Container { 
// Point 函子
// 作用是把值放到一个新的函子外面返回,返回的函子就是一个上下文
    static of (value) {return new Container(value)
    }
    ……  
}

// 调用 of 的时候取得一个上下文,之后是在上下文中解决数据
Contanier.of(2)
 .map(x => x + 5)

Monad 函子(单子)

IO 函子的嵌套问题

  • 用来解决 IO 函子多层嵌套的一个问题
const fp = require('lodash/fp')
const fs = require('fs')

class IO {static of (value) {return new IO(() => {return value})
  }
  constructor (fn) {this._value = fn}

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

// 读取文件函数
let readFile = (filename) => {return new IO(() => {
    // 同步获取文件
    return fs.readFileSync(filename, 'utf-8')
  })
}

// 打印函数
// x 是上一步的 IO 函子
let print = (x) => {return new IO(()=> {console.log(x)
    return x
  })
}

// 组合函数,先读文件再打印
let cat = fp.flowRight(print, readFile)
// 调用
// 拿到的后果是嵌套的 IO 函子 IO(IO(x))
let r = cat('package.json')
console.log(r) 
// IO {_value: [Function] }
console.log(cat('package.json')._value()) 
// IO {_value: [Function] }
// IO {_value: [Function] }
console.log(cat('package.json')._value()._value())
// IO {_value: [Function] }
/**
 * {
  "name": "Functor",
  "version": "1.0.0",
  "description": "","main":"either.js","scripts": {"test":"echo \"Error: no test specified\" && exit 1"},"keywords": [],"author":"",
  "license": "ISC",
  "dependencies": {
    "folktale": "^2.3.2",
    "lodash": "^4.17.20"
  }
}
 */

下面遇到多个 IO 函子嵌套的时候,那么_value 就会调用很屡次,这样的调用体验很不好。所以进行优化。

什么是 Monad 函子

  • Monad 函子是能够变扁的 Pointed 函子,用来解决 IO 函子嵌套问题,IO(IO(x))
  • 一个函子如果具备 join 和 of 两个办法并恪守一些定律就是一个 Monad

实现一个 Monad 函子

理论开发中不会这么难,次要是晓得 monad 的实现

const fp = require('lodash/fp')
const fs = require('fs')

class IO {static of (value) {return new IO(() => {return value})
  }
  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 readFile = (filename) => {return new IO(() => {return fs.readFileSync(filename, 'utf-8')
  })
}

let print = (x) => {return new IO(()=> {console.log(x)
    return x
  })
}

let r = readFile('package.json')
          .flatMap(print)
          .join()     
// 执行程序
/**
 * readFile 读取了文件,而后返回了一个 IO 函子
 * 调用 flatMap 是用 readFile 返回的 IO 函子调用的
 * 并且传入了一个 print 函数参数
 * 调用 flatMap 的时候,外部先调用 map,以后的 print 和 this._value 进行合并,合并之后返回了一个新的函子
 *(this._value 就是 readFile 返回 IO 函子的函数:*      () => {return fs.readFileSync(filename, 'utf-8')
        }
 *)* flatMap 中的 map 函数执行完,print 函数返回的一个 IO 函子,外面包裹的还是一个 IO 函子
 * 上面调用 join 函数,join 函数就是调用返回的新函子外部的 this._value()函数
 * 这个 this._value 就是之前 print 和 this._value 的组合函数,调用之后返回的就是 print 的返回后果
 * 所以 flatMap 执行结束之后,返回的就是 print 函数返回的 IO 函子
 *  */
 
 r = readFile('package.json')
        // 解决数据,间接在读取文件之后,应用 map 进行解决即可
        .map(fp.toUpper)
        .flatMap(print)
        .join()  

// 读完文件之后想要解决数据,怎么办?// 间接在读取文件之后调用 map 办法即可

/**
 * {
  "NAME": "FUNCTOR",
  "VERSION": "1.0.0",
  "DESCRIPTION": "","MAIN":"EITHER.JS","SCRIPTS": {"TEST":"ECHO \"ERROR: NO TEST SPECIFIED\" && EXIT 1"},"KEYWORDS": [],"AUTHOR":"",
  "LICENSE": "ISC",
  "DEPENDENCIES": {
    "FOLKTALE": "^2.3.2",
    "LODASH": "^4.17.20"
  }
}
 */

Monad 函子小结

什么是 Monad?

具备动态的 IO 办法和 join 办法的函子

什么时候应用 Monad?
  • 当一个函数返回一个函子的时候,咱们就要想到 monad,monad 能够帮咱们解决函子嵌套的问题。
  • 当咱们想要返回一个函数,这个函数返回一个值,这个时候能够调用map 办法
  • 当咱们想要去合并一个函数,然而这个函数返回一个函子,这个时候咱们要用flatMap 办法

函数式编程总体设计

  • 函数式编程(一)—— 前置常识
  • 函数式编程(二)—— 纯函数
  • 函数式编程(三)—— 柯里化
  • 函数式编程(四)——函数组合
  • 函数式编程(五)——函子

退出移动版