JS 函数式编程 02
- 函数组合
- 管道
- lodash 中的组合函数 flow() or flowRight()
- 函数结合律
- 函数组合的调试
- lodash中的fp模块
- Pointfree
- Functor
- 什么是函子,作用是什么?
- 什么是Functor
- 常见函子
- Maybe 函子
- Either函子
- IO函子
- Task函子(异步执行)
- Pointed函子
- Monad函子(单子)
函数组合
为什么要使用函数组合?
因为纯函数和柯里化会很容易就形成洋葱代码 ( 多个括号嵌套 )
比如: 获取数组中最后一个元素 并且把它转化为大写
1
2_.toUpper(_.first(_.reverse(array)))
函数组合可以把细粒度的函数重新组合成一个新的函数
管道
程序使用函数处理数据的过程可以看做是一个管道
比如: 数据a ——> 通过函数fn ——> 得到结果b
当fn比较复杂的时候,我们可以把fn拆分成多个小函数
比如: 数据a ——> 通过f1——>得到m——> 通过f2——> 得到b
类似下面这种代码
1
2
3fn = compose(f1,f2,f2) b = fn(a)
- 函数组合( compose ) :如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- 函数组合默认是从右到左执行
使用代码演示:虽然看起来似乎把问题复杂化了,但是要注意 我们使用函数组合 可以自由组合细粒度的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 函数组合演示 function compose(f, g) { return function (value) { return f(g(value)) } } // 数组翻转函数 function reverse (array) { return array.reverse() } // 获取函数第一个元素函数 function first (array) { return array[0] } // 组合函数,获取函数最后一个元素 const last = compose(first, reverse) console.log(last([1, 2, 3, 4])) // 4
lodash 中的组合函数 flow() or flowRight()
lodash 中组合函数 flow() 或者flowRight()
- flow() 是从左到右运行
- flowRight() 是从右到左运行
- 获取数组中最后一个元素并将它转化为大写
1
2
3
4
5
6
7const _ = require( "lodash" ) const reverse = arr=>arr.reverse() const first = arr=>arr[0] const toUpper = str => str.toUpperCase() const fn = _.flowRight(toUpper, first, reverse) console.log(fn(['tom','jerry','jim','lucy'])) //LUCY
- 模拟lodash中的 flowRight方法
- 定义一个函数接收不固定数量的形参 compose( …args )
- 返回一个新的函数 return function 需要接收一个要处理的值 作为形参
- 让args中的成员依次执行,这里可以使用数组的reduce方法
复制代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function compose(...args){ return function(value){ // args是要依次执行的函数数组,要从右到左执行,所以需要先reverse // reduce 是数组方法,第一个参数是一个回调,回调必须填入两个形参, // 形参1 表示初始值或者回调函数计算后的值 // 形参2 表示当前元素 // 比如 [1,2,3].reduce((a,b)=> a+b ) //第一次执行 a就是1,b就是2 第二次执行 a是3 b是3 // reduce第二个参数 是用于指定初始值 也就是指定回调的第一个形参的初始值 // 我们设置为value 这样第一次执行的时候 result就是value initFn就是args中的第一个成员函数 return args.reverse().reduce((result,initFn)=>{ return initFn(result) },value) } } // 可以通过箭头函数简化代码 const compose = (...args) => value => args.reverse().reduce((result,initFn)=>initFn(result),value)
函数结合律
函数的组合要满足结合律
比如 a 和 b 组合 再跟 c组合 等效与 b先跟c组合 再跟a组合
1
2(A,B),C 与 A,(B,C) 等效
函数组合的调试
在函数组合的管道中,每一个细粒度的函数只能接收一个参数即上一个函数的执行结果。所以如果某一个函数需要多个参数 那么我们需要对其做柯里化处理
我们采取定义一个打印函数用来调试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// NEVER SAY DIE --> nerver-say-die const _ = require('lodash') // 对需要多个参数的函数进行柯里化 const split = _.curry((sep, str) => _.split(str, sep)) const join = _.curry((sep, array) => _.join(array, sep)) // 我们需要对中间值进行打印,并且知道其位置,用柯里化输出一下 const log = _.curry((tag, v) => { console.log(tag, v) return v }) // 从右往左在每个函数后面加一个log,并且传入tag的值,就可以知道每次结果输出的是什么 const f = _.flowRight(join('-'), log('after toLower:'), _.toLower, log('after split:'), split(' ')) // 从右到左 //第一个log:after split: [ 'NEVER', 'SAY', 'DIE' ] 正确 //第二个log: after toLower: never,say,die 转化成小写字母的时候,同时转成了字符串,这里出了问题 console.log(f('NEVER SAY DIE')) //n-e-v-e-r-,-s-a-y-,-d-i-e // 修改方式,利用数组的map方法,遍历数组的每个元素让其变成小写 // 这里的map需要两个参数,第一个是数组,第二个是回调函数,需要柯里化 const map = _.curry((fn, array) => _.map(array, fn)) const f1 = _.flowRight(join('-'), map(_.toLower), split(' ')) console.log(f1('NEVER SAY DIE')) // never-say-die
lodash中的fp模块
因为函数组合中函数只能接收一个参数,这样需要对已有的函数做大量柯里化的工作,我们可以使用lodash的fp模块提供的一些函数
- lodash 的 fp 模块提供了实用的对函数式编程友好的方法
- 提供了不可变 auto-curried iteratee-first data-last (函数之先,数据之后)的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// lodash 模块 const _ = require('lodash') // 数据置先,函数置后 _.map(['a', 'b', 'c'], _.toUpper) // => ['A', 'B', 'C'] _.map(['a', 'b', 'c']) // => ['a', 'b', 'c'] // 数据置先,规则置后 _.split('Hello World', ' ') //BUT // lodash/fp 模块 const fp = require('lodash/fp') // 函数置先,数据置后 fp.map(fp.toUpper, ['a', 'b', 'c']) fp.map(fp.toUpper)(['a', 'b', 'c']) // 规则置先,数据置后 fp.split(' ', 'Hello World') fp.split(' ')('Hello World')
Pointfree
一种编程风格,就是上面的函数组合。
Point Free: 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
1
2
3
4
5
6
7// world wild web ---> World.Wild.Web // 先转成数组 // 再把首字母换成大写 const fp = require('lodash/fp') const firstLetterToUpper = fp.flowRight(fp.join('.'),fp.map(fp.upperFirst),fp.split(' ')) console.log(firstLetterToUpper('world wild web'));
Functor
什么是函子,作用是什么?
函子( representative functor ) 是范畴论里的概念,我们没有办法避免副作用,但是可以通过函子让副作用控制在可控范围内,同时也可以通过函子处理异常,异步等
什么是Functor
- 容器:包含 值 和 值的变形关系 ( 即 函数 函数处理值)
- 函子是一个特殊的容器,通过对象实现,具有map方法,map方法运行一个函数对值进行处理
1
2
3
4
5
6
7
8
9
10
11class Container { constructor(value){ this._value = value // 加_表示永远不暴露该属性 } map(fn){ // 调用value的变形关系 fn // 返回一个新的函子实例 其中的_value就是上一次fn运算的结果 return new Container(fn(this._value)) } }
因为还是有new的存在是面向对象思想 所以我们修改为函数式编程
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Container { static of(value){ return new Container(value) } constructor(value){ this._value = value // 加_表示永远不暴露该属性 } map(fn){ // 调用value的变形关系 fn // 返回一个新的函子实例 其中的_value就是上一次fn运算的结果 return Container.of(fn(this._value)) } }
- 函数式编程不直接操作值,而是由函子完成
- 函子就是一个容器 一个对象 实现了map契约
- 函子可以看做是一个容器里面装了一个值
- 想要处理其中的值,我们需要给盒子的map传递处理值的纯函数
- map最终会返回一个包含新值的容器 (函子)
常见函子
Maybe 函子
可以对外部的空值情况做处理(控制副作用在允许的范围)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class 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 }
Either函子
- Either 两者中的任何一个,类似于 if…else…的处理
- 当出现问题的时候,Either函子会给出提示的有效信息,
- 异常会让函数变的不纯,Either 函子可以用来做异常处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46// 因为是二选一,所以要定义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函子的map是纯函数 返回的都是IO函子对象 ,但是 IO 函子对象的_value 是不纯的 但是可以由使用者控制调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const 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)) } } // 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 Filesnodejsnode.exe
Task函子(异步执行)
- folktale 一个标准的函数式编程库。和 lodash、ramda 不同的是,他没有提供很多功能函数。只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、 MayBe 等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30const { task } = require('folktale/concurrency/task') const fs = require('fs') 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) } })
Pointed函子
- Pointed 函子是实现了 of 静态方法的函子
of 方法是为了避免使用 new 来创建对象,更深层的含义是of 方法用来把值放到上下文 - Context(把值放到容器中,使用 map 来处理值)
Monad函子(单子)
函子是一个容器,可以包含任何值。函子之中再包含一个函子,也可以。但是,这样就会出现多层嵌套的函子。
Monad 函子的作用是,总是返回一个单层的函子
Monad 主要通过 join 和 flatMap两个方法实现解决函子嵌套的问题。
- 当我们想要返回一个函数,这个函数返回一个值,这个时候可以调用map 方法
- 当我们想要去合并一个函数,但是这个函数返回一个函子,这个时候我们要用flatMap 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46const 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') // 得到一个_value值为 fs.readFileSync(filename, 'utf-8') 的IO函子 // return this.map(fn--就是print函数) //也就是把上面的文件读取函数执行结果作为x传入给print函数 //print函数.join() 其实就是执行 new IO(()=> {console.log(x); return x}) .flatMap(print) // 执行 ()=> {console.log(x); return x } .join() r = readFile('package.json') // 处理数据,直接在读取文件之后,使用map进行处理即可 .map(fp.toUpper) .flatMap(print) .join()
最后
以上就是秀丽小海豚最近收集整理的关于JS 函数式编程 02 —— 函数组合,Pointfree,Functor(函子)函数组合PointfreeFunctor的全部内容,更多相关JS内容请搜索靠谱客的其他文章。
发表评论 取消回复