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) // 49console.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 -ynpm 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的时候把以后取值的过程包装到函数外面,再在须要的时候再获取processconst 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)) // 3console.log(f(1)(2)) // 3
folktale中的compose函数
const { compose, curry } = require('folktale/core/lambda')const { toUpper, first } = require('lodash/fp')// compose 组合函数在lodash外面是flowRightconst 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 办法
函数式编程总体设计
- 函数式编程(一)—— 前置常识
- 函数式编程(二)—— 纯函数
- 函数式编程(三)—— 柯里化
- 函数式编程(四)——函数组合
- 函数式编程(五)——函子