我是靠谱客的博主 秀丽小海豚,最近开发中收集的这篇文章主要介绍JS 函数式编程 02 —— 函数组合,Pointfree,Functor(函子)函数组合PointfreeFunctor,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

JS 函数式编程 02

  • 函数组合
    • 管道
    • lodash 中的组合函数 flow() or flowRight()
    • 函数结合律
    • 函数组合的调试
      • lodash中的fp模块
  • Pointfree
  • Functor
    • 什么是函子,作用是什么?
    • 什么是Functor
    • 常见函子
      • Maybe 函子
      • Either函子
      • IO函子
      • Task函子(异步执行)
      • Pointed函子
      • Monad函子(单子)

函数组合

为什么要使用函数组合?
因为纯函数和柯里化会很容易就形成洋葱代码 ( 多个括号嵌套 )
比如: 获取数组中最后一个元素 并且把它转化为大写

_.toUpper(_.first(_.reverse(array)))

函数组合可以把细粒度的函数重新组合成一个新的函数

管道

程序使用函数处理数据的过程可以看做是一个管道
比如: 数据a ——> 通过函数fn ——> 得到结果b
当fn比较复杂的时候,我们可以把fn拆分成多个小函数
比如: 数据a ——> 通过f1——>得到m——> 通过f2——> 得到b
类似下面这种代码

fn = compose(f1,f2,f2)
b = fn(a)
  • 函数组合( compose ) :如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
  • 函数组合默认是从右到左执行
    使用代码演示:虽然看起来似乎把问题复杂化了,但是要注意 我们使用函数组合 可以自由组合细粒度的函数
// 函数组合演示
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() 是从右到左运行
  • 获取数组中最后一个元素并将它转化为大写
const _ = 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方法
    1. 定义一个函数接收不固定数量的形参 compose( …args )
    2. 返回一个新的函数 return function 需要接收一个要处理的值 作为形参
    3. 让args中的成员依次执行,这里可以使用数组的reduce方法
    function 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组合

(A,B),C 与 A,(B,C) 等效

函数组合的调试

在函数组合的管道中,每一个细粒度的函数只能接收一个参数即上一个函数的执行结果。所以如果某一个函数需要多个参数 那么我们需要对其做柯里化处理
我们采取定义一个打印函数用来调试

// 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 (函数之先,数据之后)的方法
// 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: 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
// 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方法运行一个函数对值进行处理
class Container {
constructor(value){
this._value = value // 加_表示永远不暴露该属性
}
map(fn){
// 调用value的变形关系 fn 
// 返回一个新的函子实例 其中的_value就是上一次fn运算的结果
return new Container(fn(this._value))
}
}

因为还是有new的存在是面向对象思想 所以我们修改为函数式编程

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

可以对外部的空值情况做处理(控制副作用在允许的范围)


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 }

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函子的map是纯函数 返回的都是IO函子对象 ,但是 IO 函子对象的_value 是不纯的 但是可以由使用者控制调用
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))
}
}
// 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 等
const { 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 方法
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') // 得到一个_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 函数式编程 02 —— 函数组合,Pointfree,Functor(函子)函数组合PointfreeFunctor所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(43)

评论列表共有 0 条评论

立即
投稿
返回
顶部